diff --git a/.all-contributorsrc b/.all-contributorsrc index 2138fa98f0b..085f7a63829 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1127,7 +1127,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/81426024?v=4", "profile": "https://github.com/woin2ee", "contributions": [ - "doc" + "doc", + "code" ] }, { @@ -1273,6 +1274,251 @@ "contributions": [ "code" ] + }, + { + "login": "anlaital-oura", + "name": "Antti Laitala", + "avatar_url": "https://avatars.githubusercontent.com/u/133648611?v=4", + "profile": "https://github.com/anlaital-oura", + "contributions": [ + "code" + ] + }, + { + "login": "PushedCrayon", + "name": "PushedCrayon", + "avatar_url": "https://avatars.githubusercontent.com/u/37077444?v=4", + "profile": "https://github.com/PushedCrayon", + "contributions": [ + "code" + ] + }, + { + "login": "stefanomondino", + "name": "Stefano Mondino", + "avatar_url": "https://avatars.githubusercontent.com/u/1691903?v=4", + "profile": "https://stefanomondino.com", + "contributions": [ + "code" + ] + }, + { + "login": "leszko11", + "name": "Łukasz Lech", + "avatar_url": "https://avatars.githubusercontent.com/u/23533452?v=4", + "profile": "https://github.com/leszko11", + "contributions": [ + "code" + ] + }, + { + "login": "costapombo", + "name": "costapombo", + "avatar_url": "https://avatars.githubusercontent.com/u/31352351?v=4", + "profile": "https://github.com/costapombo", + "contributions": [ + "code" + ] + }, + { + "login": "isavynskyi", + "name": "Ihor Savynskyi", + "avatar_url": "https://avatars.githubusercontent.com/u/18377497?v=4", + "profile": "https://github.com/isavynskyi", + "contributions": [ + "doc" + ] + }, + { + "login": "kapitoshka438", + "name": "Eduard Miniakhmetov", + "avatar_url": "https://avatars.githubusercontent.com/u/3232401?v=4", + "profile": "https://github.com/kapitoshka438", + "contributions": [ + "code" + ] + }, + { + "login": "alexfilimon", + "name": "Alexander Filimonov", + "avatar_url": "https://avatars.githubusercontent.com/u/19904867?v=4", + "profile": "https://github.com/alexfilimon", + "contributions": [ + "code" + ] + }, + { + "login": "rofle100lvl", + "name": "Gorbenko Roman", + "avatar_url": "https://avatars.githubusercontent.com/u/45801227?v=4", + "profile": "https://github.com/rofle100lvl", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "lucasmpaim", + "name": "Lucas Mrowskovsky Paim", + "avatar_url": "https://avatars.githubusercontent.com/u/7849484?v=4", + "profile": "https://www.linkedin.com/in/lucas-paim/", + "contributions": [ + "code" + ] + }, + { + "login": "ActuallyTaylor", + "name": "Taylor Lineman", + "avatar_url": "https://avatars.githubusercontent.com/u/32944568?v=4", + "profile": "http://actuallytaylor.com", + "contributions": [ + "code" + ] + }, + { + "login": "nandodelauni", + "name": "Miguel Ferrando", + "avatar_url": "https://avatars.githubusercontent.com/u/1938501?v=4", + "profile": "https://github.com/nandodelauni", + "contributions": [ + "code" + ] + }, + { + "login": "BarredEwe", + "name": "BarredEwe", + "avatar_url": "https://avatars.githubusercontent.com/u/19188911?v=4", + "profile": "https://www.linkedin.com/in/barredewe", + "contributions": [ + "doc" + ] + }, + { + "login": "chris-livefront", + "name": "Chris Sessions", + "avatar_url": "https://avatars.githubusercontent.com/u/126101032?v=4", + "profile": "https://github.com/chris-livefront", + "contributions": [ + "doc" + ] + }, + { + "login": "ajkolean", + "name": "Andy Kolean", + "avatar_url": "https://avatars.githubusercontent.com/u/5394701?v=4", + "profile": "https://github.com/ajkolean", + "contributions": [ + "code" + ] + }, + { + "login": "Binlogo", + "name": "Binlogo", + "avatar_url": "https://avatars.githubusercontent.com/u/7845507?v=4", + "profile": "https://github.com/Binlogo", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "DevilDimon", + "name": "Dmitry Serov", + "avatar_url": "https://avatars.githubusercontent.com/u/10220441?v=4", + "profile": "https://github.com/DevilDimon", + "contributions": [ + "code" + ] + }, + { + "login": "darrarski", + "name": "Dariusz Rybicki", + "avatar_url": "https://avatars.githubusercontent.com/u/1384684?v=4", + "profile": "http://darrarski.pl", + "contributions": [ + "code" + ] + }, + { + "login": "dansinclair25", + "name": "Dan Sinclair", + "avatar_url": "https://avatars.githubusercontent.com/u/2573447?v=4", + "profile": "https://github.com/dansinclair25", + "contributions": [ + "doc" + ] + }, + { + "login": "KaiOelfke", + "name": "Kai Oelfke", + "avatar_url": "https://avatars.githubusercontent.com/u/1190948?v=4", + "profile": "https://www.kaioelfke.de", + "contributions": [ + "code" + ] + }, + { + "login": "InderKumarRathore", + "name": "Inder", + "avatar_url": "https://avatars.githubusercontent.com/u/352443?v=4", + "profile": "http://stackoverflow.com/users/468724/inder-kumar-rathore", + "contributions": [ + "code" + ] + }, + { + "login": "kyounh12", + "name": "kyounh12", + "avatar_url": "https://avatars.githubusercontent.com/u/25301615?v=4", + "profile": "https://github.com/kyounh12", + "contributions": [ + "code" + ] + }, + { + "login": "alvar-bolt", + "name": "Alvar Hansen", + "avatar_url": "https://avatars.githubusercontent.com/u/72379847?v=4", + "profile": "https://github.com/alvar-bolt", + "contributions": [ + "doc" + ] + }, + { + "login": "barakwei", + "name": "Barak Weiss", + "avatar_url": "https://avatars.githubusercontent.com/u/5232161?v=4", + "profile": "https://github.com/barakwei", + "contributions": [ + "code" + ] + }, + { + "login": "hiltonc", + "name": "Hilton Campbell", + "avatar_url": "https://avatars.githubusercontent.com/u/470753?v=4", + "profile": "https://github.com/hiltonc", + "contributions": [ + "code" + ] + }, + { + "login": "rgnns", + "name": "Gabriel Liévano", + "avatar_url": "https://avatars.githubusercontent.com/u/811827?v=4", + "profile": "https://github.com/rgnns", + "contributions": [ + "code" + ] + }, + { + "login": "vijaytholpadi", + "name": "Vijay Tholpadi", + "avatar_url": "https://avatars.githubusercontent.com/u/1171868?v=4", + "profile": "https://github.com/vijaytholpadi", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 62fc6531c89..5b625d6c2cc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ open_collective: tuistapp github: tuist +polar: tuist custom: ["https://polar.sh/tuist"] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b4c0b29d1a2..43c57fe4e5c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -Resolves https://github.com/tuist/tuist/issues/YYY +Resolves ### Short description 📝 @@ -6,11 +6,11 @@ Resolves https://github.com/tuist/tuist/issues/YYY ### How to test the changes locally 🧐 -> Include a set of steps for the reviewer to test the changes locally (see [the documentation](https://docs.tuist.io/contributors/testing-strategy) for reference). +> Include a set of steps for the reviewer to test the changes locally (see [the documentation](https://docs.tuist.io/contributors/get-started) for reference). ### Contributor checklist ✅ -- [ ] The code has been linted using run `mise run 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/deploy-tuist-docc-dry-run.yml b/.github/workflows/deploy-tuist-docc-dry-run.yml deleted file mode 100644 index 987500647b8..00000000000 --- a/.github/workflows/deploy-tuist-docc-dry-run.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Deploy Tuist DocC dry-run - -on: - pull_request: - paths: - - Sources/ProjectDescription/** - - docs/** - - .github/workflows/deploy-tuist-docc-dry-run.yml - -jobs: - deploy: - name: Deploy Documentation - runs-on: macos-13 - 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: bash ./make/tasks/github/build-docc.sh diff --git a/.github/workflows/deploy-tuist-docc.yml b/.github/workflows/deploy-tuist-docc.yml deleted file mode 100644 index ef75ee2c2bc..00000000000 --- a/.github/workflows/deploy-tuist-docc.yml +++ /dev/null @@ -1,59 +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 Tuist DocC - -on: - workflow_dispatch: {} - push: - branches: - - main - -concurrency: - group: tuist-docc-${{ github.head_ref }} - cancel-in-progress: true - -jobs: - deploy: - name: Deploy Documentation - runs-on: macos-13 - steps: - - name: Start deployment - uses: bobheadxi/deployments@v1 - id: deployment - with: - env: production - 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: bash ./make/tasks/github/build-docc.sh - - name: Fix permissions - run: 'sudo chown -R $USER .build/documentation' - - name: Publish to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - 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() - with: - env: ${{ steps.deployment.outputs.env }} - step: finish - status: ${{ job.status }} - deployment_id: ${{ steps.deployment.outputs.deployment_id }} - env_url: "https://docs.tuist.io" diff --git a/.github/workflows/tuist.yml b/.github/workflows/tuist.yml deleted file mode 100644 index 8b2c374de35..00000000000 --- a/.github/workflows/tuist.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Tuist - -on: - push: - branches: - - main - pull_request: - paths: - - .xcode-version - - Tuist/** - - Package.resolved - - Gemfile* - - Package.swift - - Project.swift - - Sources/** - - '!Sources/**/*.docc' - - Templates/** - - Tests/** - - fixtures/** - - .package.resolved - - .github/workflows/tuist.yml - -concurrency: - group: tuist-${{ github.head_ref }} - cancel-in-progress: true - -env: - TUIST_CONFIG_CLOUD_TOKEN: ${{ secrets.TUIST_CONFIG_CLOUD_TOKEN }} - -jobs: - test: - name: Test with Xcode - runs-on: macos-13 - steps: - - uses: actions/checkout@v3 - - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - - 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 - name: 'Cache fetched dependencies folder' - 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: Test - 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: 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 - name: 'Cache fetched dependencies folder' - 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 --xcframeworks - - name: 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: Run ${{ matrix.feature }} - runs-on: macos-13 - env: - TUIST_CONFIG_CLOUD_TOKEN: ${{ secrets.TUIST_CONFIG_CLOUD_TOKEN }} - strategy: - matrix: - feature: - [ - 'TuistAcceptanceTests', - 'TuistBuildAcceptanceTests', - 'TuistDependenciesAcceptanceTests', - 'TuistGenerateAcceptanceTests', - 'TuistTestAcceptanceTests', - ] - steps: - - uses: actions/checkout@v4 - - 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: - path: Tuist/Dependencies/SwiftPackageManager/.build - key: spm-v1-${{ hashFiles('Tuist/Package.resolved') }} - restore-keys: spm-v1-${{ hashFiles('Tuist/Package.resolved') }} - - name: Fetch dependencies - run: tuist fetch - - name: Run acceptance tests - run: tuist test --xcframeworks --result-bundle-path /tmp/tuist/test-${{ matrix.feature }} ${{ matrix.feature }} - - uses: actions/upload-artifact@v4 - if: ${{ always() }} - with: - name: test-${{ matrix.feature }} - path: | - /tmp/tuist/** - lint: - name: Lint - runs-on: macos-13 - steps: - - uses: actions/checkout@v3 - - 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: mise run lint - - lint-lockfiles: - name: Lint lockfiles - runs-on: macos-13 - steps: - - uses: actions/checkout@v3 - - name: Run - run: ./make/tasks/workspace/lint/lockfiles.sh diff --git a/.mise.toml b/.mise.toml index dabcf39a9d5..32844606127 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,43 +1,16 @@ [tools] -tuist = "3.42.2" +tuist = "4.24.0" swiftlint = "0.54.0" -swiftformat = "0.53.0" +swiftformat = "0.53.3" +pnpm = "8.15.6" +node = "20.12.0" +sourcedocs = "2.0.1" [plugins] python = 'https://github.com/asdf-community/asdf-tuist' +sourcedocs = "https://github.com/tuist/asdf-sourcedocs.git" -[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 +[settings] +jobs = 6 +http_timeout = 30 +experimental = true \ No newline at end of file diff --git a/.mise/tasks/.gitignore b/.mise/tasks/.gitignore new file mode 100644 index 00000000000..c917344d8d2 --- /dev/null +++ b/.mise/tasks/.gitignore @@ -0,0 +1 @@ +!fixture/ \ No newline at end of file diff --git a/.mise/tasks/benchmark/build b/.mise/tasks/benchmark/build new file mode 100755 index 00000000000..c30415c4a7b --- /dev/null +++ b/.mise/tasks/benchmark/build @@ -0,0 +1,5 @@ +#!/bin/bash +# mise description="Build the 'tuistbenchmark' tool" +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT --target tuistbenchmark $@ \ No newline at end of file diff --git a/.mise/tasks/benchmark/run b/.mise/tasks/benchmark/run new file mode 100755 index 00000000000..82357cf75d1 --- /dev/null +++ b/.mise/tasks/benchmark/run @@ -0,0 +1,41 @@ +#!/bin/bash +# mise description="Run the 'tuistbenchmark' tool" +set -euo pipefail + +FIXTURES_DIRECTORY=$MISE_PROJECT_ROOT/fixtures +source $MISE_PROJECT_ROOT/.mise/utilities/setup.sh + +temp_dir=$(mktemp -d) + +# Define a cleanup function +cleanup() { + echo "Deleting the temporary directory..." + rm -rf "$temp_dir" + echo "Temporary directory deleted." +} + +echo "$(format_section "Building the fixture generator and benchmark tools")" +swift build --package-path $MISE_PROJECT_ROOT --product tuistfixturegenerator -c release +swift build --package-path $MISE_PROJECT_ROOT --product tuistbenchmark -c release + +echo "$(format_section "Generating a Tuist project with 50 projects")" +DIRECTORY_50_PROJECTS=$temp_dir/50_projects +$MISE_PROJECT_ROOT/.build/release/tuistfixturegenerator generate --path $DIRECTORY_50_PROJECTS --projects 50 +echo "$(format_section "Generating a Tuist project with 2 projects and 2000 sources")" +DIRECTORY_2000_SOURCES=$temp_dir/50_projects +$MISE_PROJECT_ROOT/.build/release/tuistfixturegenerator generate --path $DIRECTORY_2000_SOURCES --projects 2 --sources 2000 + +FIXTURES_JSON_PATH=$temp_dir/fixtures.json +FIXTURES_LIST=($DIRECTORY_50_PROJECTS $DIRECTORY_2000_SOURCES $FIXTURES_DIRECTORY/ios_app_with_static_frameworks $FIXTURES_DIRECTORY/ios_app_with_framework_and_resources $FIXTURES_DIRECTORY/ios_app_with_transitive_framework $FIXTURES_DIRECTORY/ios_app_with_xcframeworks) + +echo "$(format_section "Writing the fixtures.json file")" +FIXTURES_JSON=$(jq -n --argjson arr "$(printf '%s\n' "${FIXTURES_LIST[@]}" | jq -R . | jq -s .)" '{"paths": $arr}') + +echo $FIXTURES_JSON > $FIXTURES_JSON_PATH + +echo "$(format_section "Building tuist")" +swift build --package-path $MISE_PROJECT_ROOT --product tuist -c release +swift build --package-path $MISE_PROJECT_ROOT --product ProjectDescription -c release + +echo "$(format_section "Benchmarking")" +$ROOT_DIR/.build/release/tuistbenchmark benchmark -b $ROOT_DIR/.build/release/tuist --fixture-list $FIXTURES_JSON_PATH --format markdown \ No newline at end of file diff --git a/.mise/tasks/bundle b/.mise/tasks/bundle new file mode 100755 index 00000000000..7604d8b45eb --- /dev/null +++ b/.mise/tasks/bundle @@ -0,0 +1,133 @@ +#!/bin/bash +# mise description="Bundles Tuist and outputs it along with its dynamic frameworks into the /build directory" +set -e + +source $MISE_PROJECT_ROOT/.mise/utilities/setup.sh +XCODE_PATH_SCRIPT_PATH=$MISE_PROJECT_ROOT/.mise/utilities/xcode_path.sh +BUILD_DIRECTORY=$MISE_PROJECT_ROOT/build +# Xcode 15 has a bug that causes the /var/folders... temporary directory, which is a symlink to +# /private/var/folders to crash Xcode. +TMP_DIR=/private$(mktemp -d) +trap "rm -rf $TMP_DIR" EXIT # Ensures it gets deleted +XCODE_VERSION=$(cat $MISE_PROJECT_ROOT/.xcode-version) +LIBRARIES_XCODE_VERSION=$(cat $MISE_PROJECT_ROOT/.xcode-version-libraries) +BUILD_DIR=$MISE_PROJECT_ROOT/build + +echo "$(format_section "Building release into $BUILD_DIRECTORY")" + +XCODE_PATH=$($XCODE_PATH_SCRIPT_PATH --version $XCODE_VERSION) +XCODE_LIBRARIES_PATH=$($XCODE_PATH_SCRIPT_PATH --version $LIBRARIES_XCODE_VERSION) + +echo "Static executables will be built with $XCODE_PATH" +echo "Dynamic bundles will be built with $XCODE_LIBRARIES_PATH" + +rm -rf $MISE_PROJECT_ROOT/Tuist.xcodeproj +rm -rf $MISE_PROJECT_ROOT/Tuist.xcworkspace +rm -rf $BUILD_DIRECTORY +mkdir -p $BUILD_DIRECTORY + +build_fat_release_library() { + ( + cd $MISE_PROJECT_ROOT || exit 1 + DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -resolvePackageDependencies + DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -scheme $1 -configuration Release -destination platform=macosx BUILD_LIBRARY_FOR_DISTRIBUTION=YES ARCHS='arm64 x86_64' BUILD_DIR=$TMP_DIR clean build + + # We remove the PRODUCT.swiftmodule/Project directory because + # this directory contains objects that are not stable across Swift releases. + rm -rf $TMP_DIR/Release/$1.swiftmodule/Project + cp -r $TMP_DIR/Release/PackageFrameworks/$1.framework $BUILD_DIRECTORY/$1.framework + mkdir -p $BUILD_DIRECTORY/$1.framework/Modules + cp -r $TMP_DIR/Release/$1.swiftmodule $BUILD_DIRECTORY/$1.framework/Modules/$1.swiftmodule + cp -r $TMP_DIR/Release/$1.framework.dSYM $BUILD_DIRECTORY/$1.framework.dSYM + ) +} + +build_xcframework_library() { + ( + cd $MISE_PROJECT_ROOT || exit 1 + DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -resolvePackageDependencies + DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -scheme $1 -configuration Release -destination platform=macosx BUILD_LIBRARY_FOR_DISTRIBUTION=YES ARCHS='arm64 x86_64' BUILD_DIR=$TMP_DIR clean build + + xcodebuild -create-xcframework \ + -framework $TMP_DIR/Release/PackageFrameworks/$1.framework \ + -output $BUILD_DIRECTORY/$1.xcframework + cp -r $TMP_DIR/Release/$1.framework.dSYM $BUILD_DIRECTORY/$1.xcframework.dSYM + ) +} + +build_fat_release_binary() { + ( + cd $MISE_PROJECT_ROOT || exit 1 + ARM64_TARGET=arm64-apple-macosx + X86_64_TARGET=x86_64-apple-macosx + + DEVELOPER_DIR=$XCODE_PATH swift build \ + --configuration release \ + --disable-sandbox \ + --product $1 \ + --package-path $2 \ + --build-path $TMP_DIR/$1 \ + --triple $ARM64_TARGET + + DEVELOPER_DIR=$XCODE_PATH swift build \ + --configuration release \ + --disable-sandbox \ + --product $1 \ + --package-path $2 \ + --build-path $TMP_DIR/$1 \ + --triple $X86_64_TARGET + + mkdir -p $3 + + DEVELOPER_DIR=$XCODE_PATH lipo -create \ + -output $3/$1 \ + $TMP_DIR/$1/$ARM64_TARGET/release/$1 \ + $TMP_DIR/$1/$X86_64_TARGET/release/$1 + ) +} + +echo "$(format_section "Building")" + +echo "$(format_subsection "Building ProjectDescription framework")" +build_fat_release_library "ProjectDescription" + +echo "$(format_subsection "Building ProjectAutomation framework")" +build_xcframework_library "ProjectAutomation" + +echo "$(format_subsection "Building tuist executable")" +build_fat_release_binary "tuist" $MISE_PROJECT_ROOT $BUILD_DIRECTORY + +echo "$(format_section "Copying assets")" + +echo "$(format_subsection "Copying Tuist's templates")" +cp -r $MISE_PROJECT_ROOT/Templates $BUILD_DIRECTORY/Templates + +echo "$(format_subsection "Copy Swift libraries into the Tuist binary")" +swift stdlib-tool --copy --scan-executable $BUILD_DIRECTORY/tuist --platform macosx --destination $BUILD_DIRECTORY + +echo "$(format_section "Bundling")" + +( + cd $BUILD_DIRECTORY || exit 1 + echo "$(format_subsection "Bundling tuist.zip")" + zip -q -r --symlinks tuist.zip tuist libswift_Concurrency.dylib ProjectAutomation.xcframework ProjectAutomation.xcframework.dSYM ProjectDescription.framework ProjectDescription.framework.dSYM Templates vendor + echo "$(format_subsection "Bundling ProjectDescription.framework.zip")" + zip -q -r --symlinks ProjectDescription.framework.zip ProjectDescription.framework ProjectDescription.framework.dSYM + echo "$(format_subsection "Bundling ProjectAutomation.xcframework.zip")" + zip -q -r --symlinks ProjectAutomation.xcframework.zip ProjectAutomation.xcframework ProjectAutomation.xcframework.dSYM + + rm -rf tuist ProjectAutomation.xcframework ProjectAutomation.xcframework.dSYM ProjectDescription.framework ProjectDescription.framework.dSYM Templates vendor + + : > SHASUMS256.txt + : > SHASUMS512.txt + + for file in *; do + if [ -f "$file" ]; then + if [[ "$file" == "SHASUMS256.txt" || "$file" == "SHASUMS512.txt" ]]; then + continue + fi + echo "$(shasum -a 256 "$file" | awk '{print $1}') ./$file" >> SHASUMS256.txt + echo "$(shasum -a 512 "$file" | awk '{print $1}') ./$file" >> SHASUMS512.txt + fi + done +) diff --git a/.mise/tasks/clean b/.mise/tasks/clean new file mode 100755 index 00000000000..6bd7017bd72 --- /dev/null +++ b/.mise/tasks/clean @@ -0,0 +1,21 @@ +#!/bin/bash +# mise description="Cleans SPM's .build directory and Tuist-** derived data directories" +set -euo pipefail + +DERIVED_DATA_PATH=$($MISE_PROJECT_ROOT/.mise/utilities/derived_data_path.sh) + +rm -rf $MISE_PROJECT_ROOT/.build +for dir in "$DERIVED_DATA_PATH"tuist-*; do + # Check if it is a directory before deleting + if [[ -d "$dir" ]]; then + echo "Deleting directory: $dir" + rm -rf "$dir" + fi +done +for dir in "$DERIVED_DATA_PATH"Tuist-*; do + # Check if it is a directory before deleting + if [[ -d "$dir" ]]; then + echo "Deleting directory: $dir" + rm -rf "$dir" + fi +done diff --git a/.mise/tasks/docs/build b/.mise/tasks/docs/build new file mode 100755 index 00000000000..9096ff3fe4b --- /dev/null +++ b/.mise/tasks/docs/build @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Builds the documentation website." + +set -euo pipefail + +pnpm run -C $MISE_PROJECT_ROOT/docs build \ No newline at end of file diff --git a/.mise/tasks/docs/deploy b/.mise/tasks/docs/deploy new file mode 100755 index 00000000000..e343fc70ce1 --- /dev/null +++ b/.mise/tasks/docs/deploy @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Deploys the documentation website." + +set -euo pipefail + +pnpm run -C $MISE_PROJECT_ROOT/docs deploy \ No newline at end of file diff --git a/.mise/tasks/docs/dev b/.mise/tasks/docs/dev new file mode 100755 index 00000000000..3beda3c3c54 --- /dev/null +++ b/.mise/tasks/docs/dev @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Dev the documentation website." + +set -euo pipefail + +pnpm run -C $MISE_PROJECT_ROOT/docs dev \ No newline at end of file diff --git a/.mise/tasks/docs/generate-cli-docs b/.mise/tasks/docs/generate-cli-docs new file mode 100755 index 00000000000..0927ccaeeec --- /dev/null +++ b/.mise/tasks/docs/generate-cli-docs @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Generates the markdown documentation for the cli files" + +set -euo pipefail + +$MISE_PROJECT_ROOT/docs/scripts/generate-cli-docs.mjs \ No newline at end of file diff --git a/.mise/tasks/docs/generate-manifests-docs b/.mise/tasks/docs/generate-manifests-docs new file mode 100755 index 00000000000..8c6509c55bd --- /dev/null +++ b/.mise/tasks/docs/generate-manifests-docs @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Generates the markdown documentation for the manifest files" + +set -euo pipefail + +$MISE_PROJECT_ROOT/docs/scripts/generate-manifest-docs.mjs \ No newline at end of file diff --git a/.mise/tasks/fixturegenerator/build b/.mise/tasks/fixturegenerator/build new file mode 100755 index 00000000000..a104b8e9d1a --- /dev/null +++ b/.mise/tasks/fixturegenerator/build @@ -0,0 +1,5 @@ +#!/bin/bash +# mise description="Build the 'tuistfixturegenerator' tool" +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT --target tuistfixturegenerator $@ \ No newline at end of file diff --git a/.mise/tasks/fixturegenerator/run b/.mise/tasks/fixturegenerator/run new file mode 100755 index 00000000000..450e98a7d94 --- /dev/null +++ b/.mise/tasks/fixturegenerator/run @@ -0,0 +1,40 @@ +#!/bin/bash +# mise description="Run the 'tuistfixturegenerator' tool" +set -euo pipefail + +source $MISE_PROJECT_ROOT/.mise/utilities/setup.sh + +path=$(pwd) +projects="" +targets="" +sources="" + +# Function to display usage +usage() { + echo "Usage: $0 --path --projects --targets --sources " + exit 1 +} + +while [[ "$#" -gt 0 ]]; do + case $1 in + --path) path="$2"; shift ;; + --projects) projects="$2"; shift ;; + --targets) targets="$2"; shift ;; + --sources) sources="$2"; shift ;; + *) echo "Unknown parameter passed: $1"; usage ;; + esac + shift +done + +if [ -z "$projects" ] || [ -z "$targets" ] || [ -z "$sources" ]; then + usage +fi + +echo "$(format_section "Generating fixture")" + +echo "Path: $path" +echo "Projects: $projects" +echo "Targets: $targets" +echo "Sources: $sources" + +swift run --package-path $MISE_PROJECT_ROOT tuistfixturegenerator generate --path $path --projects $projects --targets $targets --sources $sources \ No newline at end of file diff --git a/.mise/tasks/install b/.mise/tasks/install new file mode 100755 index 00000000000..15725dbc93c --- /dev/null +++ b/.mise/tasks/install @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Performs additional tasks that are necessary to work in this repository" + +set -euo pipefail + +pnpm install -C docs/ \ No newline at end of file diff --git a/.mise/tasks/lint b/.mise/tasks/lint new file mode 100755 index 00000000000..d1a523f1d47 --- /dev/null +++ b/.mise/tasks/lint @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Lint the workspace" +set -euo pipefail + +swiftformat $MISE_PROJECT_ROOT --lint +swiftlint lint --quiet --config $MISE_PROJECT_ROOT/.swiftlint.yml $MISE_PROJECT_ROOT/Sources diff --git a/.mise/tasks/lint-fix b/.mise/tasks/lint-fix new file mode 100755 index 00000000000..cfc0359599e --- /dev/null +++ b/.mise/tasks/lint-fix @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Lint the workspace fixing issues" +set -euo pipefail + +swiftformat $MISE_PROJECT_ROOT +swiftlint lint --fix --quiet --config $MISE_PROJECT_ROOT/.swiftlint.yml $MISE_PROJECT_ROOT/Sources diff --git a/.mise/tasks/spm/tuist/build b/.mise/tasks/spm/tuist/build new file mode 100755 index 00000000000..ac27d026db9 --- /dev/null +++ b/.mise/tasks/spm/tuist/build @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# mise description="Build the project using Tuist" + +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist install --path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist build --path $MISE_PROJECT_ROOT --generate $@ + diff --git a/.mise/tasks/spm/tuist/edit b/.mise/tasks/spm/tuist/edit new file mode 100755 index 00000000000..164f191cdd6 --- /dev/null +++ b/.mise/tasks/spm/tuist/edit @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# mise description="Edit the project using Tuist" + +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist edit --path $MISE_PROJECT_ROOT --only-current-directory \ No newline at end of file diff --git a/.mise/tasks/spm/tuist/generate b/.mise/tasks/spm/tuist/generate new file mode 100755 index 00000000000..407ce0cef39 --- /dev/null +++ b/.mise/tasks/spm/tuist/generate @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# mise description="Generate the project using Tuist" + +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist install --path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist generate --path $MISE_PROJECT_ROOT $@ diff --git a/.mise/tasks/spm/tuist/run b/.mise/tasks/spm/tuist/run new file mode 100755 index 00000000000..08c9460d9da --- /dev/null +++ b/.mise/tasks/spm/tuist/run @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# mise description="Run the project using Tuist" + +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist $@ diff --git a/.mise/tasks/spm/tuist/test b/.mise/tasks/spm/tuist/test new file mode 100755 index 00000000000..c4158fc5718 --- /dev/null +++ b/.mise/tasks/spm/tuist/test @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# mise description="Test the project using Tuist" + +set -euo pipefail + +swift build --package-path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist install --path $MISE_PROJECT_ROOT +$MISE_PROJECT_ROOT/.build/debug/tuist test --path $MISE_PROJECT_ROOT $@ \ No newline at end of file diff --git a/.mise/tasks/workspace/generate/server-openapi-code b/.mise/tasks/workspace/generate/server-openapi-code new file mode 100755 index 00000000000..2a1fabf4bc9 --- /dev/null +++ b/.mise/tasks/workspace/generate/server-openapi-code @@ -0,0 +1,6 @@ +#!/bin/bash +# mise description="Generates the Swift code off the Open API specification." + +set -euo pipefail + +mint run apple/swift-openapi-generator@0.1.5 generate --mode types --mode client --output-directory $MISE_PROJECT_ROOT/Sources/TuistServer/OpenAPI $MISE_PROJECT_ROOT/Sources/TuistServer/OpenAPI/server.yml diff --git a/.mise/utilities/derived_data_path.sh b/.mise/utilities/derived_data_path.sh new file mode 100755 index 00000000000..9f1df55cdee --- /dev/null +++ b/.mise/utilities/derived_data_path.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +DERIVED_DATA_PATH=$(defaults read com.apple.dt.Xcode IDECustomDerivedDataLocation 2>/dev/null) + +# Check if the path is set +if [ -z "$DERIVED_DATA_PATH" ]; then + echo "$HOME/Library/Developer/Xcode/DerivedData/" +else + echo "$DERIVED_DATA_PATH" +fi \ No newline at end of file diff --git a/make/utilities/setup.sh b/.mise/utilities/setup.sh similarity index 100% rename from make/utilities/setup.sh rename to .mise/utilities/setup.sh diff --git a/make/utilities/xcode_path.sh b/.mise/utilities/xcode_path.sh similarity index 89% rename from make/utilities/xcode_path.sh rename to .mise/utilities/xcode_path.sh index 3b5ad2a3b60..6ca00177dc9 100755 --- a/make/utilities/xcode_path.sh +++ b/.mise/utilities/xcode_path.sh @@ -7,10 +7,7 @@ set -e -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/root_dir.sh) - -source $ROOT_DIR/make/utilities/setup.sh +source $MISE_PROJECT_ROOT/.mise/utilities/setup.sh # Check for jq if ! command -v jq >/dev/null 2>&1; then diff --git a/.swift-version b/.swift-version deleted file mode 100644 index b883184b123..00000000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -5.9 \ No newline at end of file diff --git a/.swiftformat b/.swiftformat index 6446fec651d..4fa9546694b 100644 --- a/.swiftformat +++ b/.swiftformat @@ -2,10 +2,12 @@ --symlinks ignore --exclude Tests/XCTestManifests.swift,Sources/TuistSupport/Vendored,fixtures/Targets/SlothCreator,Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift ---exclude fixtures/tuist_plugin +--exclude fixtures/tuist_plugin,Sources/TuistServer/OpenAPI +--disable conditionalAssignment --disable hoistAwait --disable hoistTry ---swiftversion 5.7 +--disable redundantReturn +--swiftversion 5.10 --minversion 0.53.0 # format options diff --git a/.swiftformat-version b/.swiftformat-version deleted file mode 100644 index 7f422a161ae..00000000000 --- a/.swiftformat-version +++ /dev/null @@ -1 +0,0 @@ -0.53.0 diff --git a/.swiftlint.yml b/.swiftlint.yml index 45de5169d0c..038fde68ab1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -22,3 +22,5 @@ type_name: min_length: error: 1 warning: 1 +excluded: + - Sources/TuistServer/OpenAPI diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f44f655deed..00000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug tuist", - "program": "${workspaceFolder:tuist}/.build/debug/tuist", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Debug tuist" - }, - { - "type": "lldb", - "request": "launch", - "name": "Release tuist", - "program": "${workspaceFolder:tuist}/.build/release/tuist", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Release tuist" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug tuistenv", - "program": "${workspaceFolder:tuist}/.build/debug/tuistenv", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Debug tuistenv" - }, - { - "type": "lldb", - "request": "launch", - "name": "Release tuistenv", - "program": "${workspaceFolder:tuist}/.build/release/tuistenv", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Release tuistenv" - }, - { - "type": "lldb", - "request": "launch", - "name": "Test tuist", - "program": "/Applications/Xcode.app/Contents/Developer/usr/bin/xctest", - "args": [ - ".build/debug/tuistPackageTests.xctest" - ], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build All" - }, - { - "type": "lldb", - "request": "launch", - "sourceLanguages": [ - "swift" - ], - "name": "Debug tuistbenchmark", - "program": "${workspaceFolder:tuist}/.build/debug/tuistbenchmark", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Debug tuistbenchmark" - }, - { - "type": "lldb", - "request": "launch", - "sourceLanguages": [ - "swift" - ], - "name": "Release tuistbenchmark", - "program": "${workspaceFolder:tuist}/.build/release/tuistbenchmark", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Release tuistbenchmark" - }, - { - "type": "lldb", - "request": "launch", - "sourceLanguages": [ - "swift" - ], - "name": "Debug tuistfixturegenerator", - "program": "${workspaceFolder:tuist}/.build/debug/tuistfixturegenerator", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Debug tuistfixturegenerator" - }, - { - "type": "lldb", - "request": "launch", - "sourceLanguages": [ - "swift" - ], - "name": "Release tuistfixturegenerator", - "program": "${workspaceFolder:tuist}/.build/release/tuistfixturegenerator", - "args": [], - "cwd": "${workspaceFolder:tuist}", - "preLaunchTask": "swift: Build Release tuistfixturegenerator" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 485f538ae78..bffd0d7db2c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,9 +23,10 @@ }, "editor.defaultFormatter": "esbenp.prettier-vscode", "files.exclude": { + "**/.build/": true, + "**/.DS_Store": true, "**/.git": true, - "**/node_modules": true, - "**/.DS_Store": true + "**/node_modules": true }, "spellright.language": [ "en" diff --git a/.xcode-version b/.xcode-version index ccc2f3b87fe..8e0f41140bf 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -15.1 \ No newline at end of file +15.3 \ No newline at end of file diff --git a/.xcode-version-libraries b/.xcode-version-libraries index 9dc738e691e..dafb659a697 100644 --- a/.xcode-version-libraries +++ b/.xcode-version-libraries @@ -1 +1 @@ -15.0.1 \ No newline at end of file +15.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f6cc4531d..4c7a2d5020a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,802 @@ # Changelog +## 4.24.0 - 2024-08-19 + +### Tuist + +#### Changed + +- Favor TUIST_CONFIG_TOKEN over TUIST_CONFIG_CLOUD_TOKEN [#6610](https://github.com/tuist/tuist/pull/6610) by [@fortmarek](https://github.com/fortmarek) + +#### Added + +- Add classPrefix support [#6439](https://github.com/tuist/tuist/pull/6439) by [@darrarski](https://github.com/darrarski) +- Add Swift library sdk type [#6605](https://github.com/tuist/tuist/pull/6605) by [@fortmarek](https://github.com/fortmarek) +- Add support for updating Tuist project's default branch [#6589](https://github.com/tuist/tuist/pull/6589) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix missing external resource bundle in app extension [#6604](https://github.com/tuist/tuist/pull/6604) by [@fortmarek](https://github.com/fortmarek) +- Fix app not found when running tuist share with different target and product names [#6611](https://github.com/tuist/tuist/pull/6611) by [@fortmarek](https://github.com/fortmarek) +- Skip user token deprecation warning when new user tokens are available [#6614](https://github.com/tuist/tuist/pull/6614) by [@fortmarek](https://github.com/fortmarek) +- Fix missing linked system libraries in Tuist Cache [#6613](https://github.com/tuist/tuist/pull/6613) by [@fortmarek](https://github.com/fortmarek) +- Support #import with spaces prefix in umbrella headers. [#6630](https://github.com/tuist/tuist/pull/6630) by [@barakwei](https://github.com/barakwei) +- Prune schemes when expandVariableFromTarget is pruned [#6627](https://github.com/tuist/tuist/pull/6627) by [@hiltonc](https://github.com/hiltonc) +- Fix scheme archive action not respecting custom configurations [#6636](https://github.com/tuist/tuist/pull/6636) by [@kwridan](https://github.com/kwridan) + +### Tuist Cloud + +- no changes + +## 4.23.0 - 2024-08-05 + +### Tuist + +#### Added + +- Create tuist share command [#6527](https://github.com/tuist/tuist/pull/6527) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix clean printed twice in tuist --help [#6580](https://github.com/tuist/tuist/pull/6580) by [@fortmarek](https://github.com/fortmarek) +- Generate SPM Objective-C resource accessor only when a bundle is generated [#6584](https://github.com/tuist/tuist/pull/6584) by [@fortmarek](https://github.com/fortmarek) +- Deduplicate external resources [#6538](https://github.com/tuist/tuist/pull/6538) by [@KaiOelfke](https://github.com/KaiOelfke) +- Prevent Tuist authentication kickouts by retrying token refresh [#6594](https://github.com/tuist/tuist/pull/6594) by [@fortmarek](https://github.com/fortmarek) +- Fix unstable hashing of target dependencies [#6593](https://github.com/tuist/tuist/pull/6593) by [@fortmarek](https://github.com/fortmarek) +- Fix generate performance when using Tuist Cache xcframeworks [#6592](https://github.com/tuist/tuist/pull/6592) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.22.0 - 2024-07-30 + +### Tuist + +#### Changed + +- Update generated code templates to add `Sendable` Conformance [#6540](https://github.com/tuist/tuist/pull/6540) by [@waltflanagan](https://github.com/waltflanagan) +- Improve the help menu by grouping the flat list of commands and add `tuist project view` command [#6566](https://github.com/tuist/tuist/pull/6566) by [@pepicrft](https://github.com/pepicrft) + +#### Added + +- Validate target references in custom workspace schemes [#6536](https://github.com/tuist/tuist/pull/6536) by [@ajkolean](https://github.com/ajkolean) +- Support unit test depending on an app extension [#6561](https://github.com/tuist/tuist/pull/6561) by [@InderKumarRathore](https://github.com/InderKumarRathore) + +#### Fixed + +- Fix integrating external static frameworks with resource bundles [#6565](https://github.com/tuist/tuist/pull/6565) by [@fortmarek](https://github.com/fortmarek) +- Default to staticLibrary in product type mapping if automatic product type exists [#6559](https://github.com/tuist/tuist/pull/6559) by [@Arideno](https://github.com/Arideno) +- Fix the Homebrew installation instructions in the docs [#6571](https://github.com/tuist/tuist/pull/6571) by [@pepicrft](https://github.com/pepicrft) +- Fix storing test results of non-tested schemes [#6572](https://github.com/tuist/tuist/pull/6572) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.21.2 - 2024-07-19 + +### Tuist + +#### Changed + +- Mark the Cloud static initializer as deprecated [#6531](https://github.com/tuist/tuist/pull/6531) by [@fortmarek](https://github.com/fortmarek) + +#### Added + +- Add email and password auth options for non-interactive login [#6507](https://github.com/tuist/tuist/pull/6507) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix token not found when using TUIST_CONFIG_TOKEN environment variable [#6528](https://github.com/tuist/tuist/pull/6528) by [@fortmarek](https://github.com/fortmarek) +- Fix optionalAuthentication generation option in Config.swift [#6530](https://github.com/tuist/tuist/pull/6530) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.21.1 - 2024-07-17 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.21.0 - 2024-07-16 + +### Tuist + +#### Changed + +- Flatten cloud commands to tuist [#6473](https://github.com/tuist/tuist/pull/6473) by [@fortmarek](https://github.com/fortmarek) +- Flatten the tuist-cloud cache directory into the tuist cache directory [#6474](https://github.com/tuist/tuist/pull/6474) by [@fortmarek](https://github.com/fortmarek) +- Deprecate cloud in Config in favor of fullHandle and url [#6475](https://github.com/tuist/tuist/pull/6475) by [@fortmarek](https://github.com/fortmarek) +- Deprecate the usage TUIST_CONFIG_CLOUD_TOKEN in favor of TUIST_CONFIG_TOKEN [#6476](https://github.com/tuist/tuist/pull/6476) by [@fortmarek](https://github.com/fortmarek) +- Add support for new, more secure project and user tokens [#6500](https://github.com/tuist/tuist/pull/6500) by [@fortmarek](https://github.com/fortmarek) + +#### Added + +- Lint target references in a scheme [#6491](https://github.com/tuist/tuist/pull/6491) by [@rofle100lvl](https://github.com/rofle100lvl) +- Generate a `Tuist/Config.swift` by default when creating a new project [#6521](https://github.com/tuist/tuist/pull/6521) by [@pepicrft](https://github.com/pepicrft) + +#### Fixed + +- Fix generate failing due to duplicate keys [#6469](https://github.com/tuist/tuist/pull/6469) by [@fortmarek](https://github.com/fortmarek) +- Change tuist project subcommands to accept full handle [#6472](https://github.com/tuist/tuist/pull/6472) by [@fortmarek](https://github.com/fortmarek) +- Fix missing test targets when generating an SPM package [#6483](https://github.com/tuist/tuist/pull/6483) by [@woin2ee](https://github.com/woin2ee) +- Escape header search paths for external dependencies from SPM [#6513](https://github.com/tuist/tuist/pull/6513) by [@KaiOelfke](https://github.com/KaiOelfke) +- Respect --verbose for machine readable commands [#6518](https://github.com/tuist/tuist/pull/6518) by [@fortmarek](https://github.com/fortmarek) +- Fix linking static xcframeworks linked via dynamic xcframeworks [#6520](https://github.com/tuist/tuist/pull/6520) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.20.0 - 2024-07-02 + +### Tuist + +#### Added + +- Add support for enabling explicit modules for Xcode 16 [#6405](https://github.com/tuist/tuist/pull/6405) by [@waltflanagan](https://github.com/waltflanagan) + +#### Fixed + +- Print only JSON string for dump and when --json flag is present [#6440](https://github.com/tuist/tuist/pull/6440) by [@fortmarek](https://github.com/fortmarek) +- Handle unauthorized server errors [#6451](https://github.com/tuist/tuist/pull/6451) by [@fortmarek](https://github.com/fortmarek) +- Fix generation of Quick's SPM QuickObjcRuntime target [#6445](https://github.com/tuist/tuist/pull/6445) by [@simpers](https://github.com/simpers) +- Provide better error message for a missing Tuist server token [#6450](https://github.com/tuist/tuist/pull/6450) by [@fortmarek](https://github.com/fortmarek) +- Fix deleting Tuist project and organization [#6455](https://github.com/tuist/tuist/pull/6455) by [@fortmarek](https://github.com/fortmarek) +- Add missing target settings `MERGED_BINARY_TYPE` and `MERGEABLE_LIBRARY` [#6447](https://github.com/tuist/tuist/pull/6447) by [@woin2ee](https://github.com/woin2ee) + +### Tuist Cloud + +- no changes + +## 4.19.0 - 2024-06-25 + +### Tuist + +#### Changed + +- Surface current organization usage in cloud organization show [#6421](https://github.com/tuist/tuist/pull/6421) by [@fortmarek](https://github.com/fortmarek) + +#### Added + +- Add test targets to project when generating from `Package.swift` file. [#6424](https://github.com/tuist/tuist/pull/6424) by [@woin2ee](https://github.com/woin2ee) + +#### Fixed + +- Fix tuist clean when no category is provided [#6422](https://github.com/tuist/tuist/pull/6422) by [@fortmarek](https://github.com/fortmarek) +- Fix caching ProjectDescriptionHelpers across macOS versions [#6429](https://github.com/tuist/tuist/pull/6429) by [@fortmarek](https://github.com/fortmarek) +- Skip tests for schemes removed by selective testing [#6435](https://github.com/tuist/tuist/pull/6435) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.18.0 - 2024-06-18 + +### Tuist + +#### Changed + +- Finish early instead of failing when testing a scheme with no tests [#6398](https://github.com/tuist/tuist/pull/6398) by [@fortmarek](https://github.com/fortmarek) +- Allow iOS app extensions to depend on bundles [#6415](https://github.com/tuist/tuist/pull/6415) by [@DevilDimon](https://github.com/DevilDimon) + +#### Fixed + +- Fix sporadic errors when Tuist tries to preserve the SPM lockfile across project generations [#6394](https://github.com/tuist/tuist/pull/6394) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 4.17.0 - 2024-06-11 + +### Tuist + +#### Changed + +- Do not generate resource accessors for internal targets with resources and Objective-C source files [#6388](https://github.com/tuist/tuist/pull/6388) by [@fortmarek](https://github.com/fortmarek) +- Complete analytics uploads with extra test metadata [#6382](https://github.com/tuist/tuist/pull/6382) by [@fortmarek](https://github.com/fortmarek) + +#### Added + +- Upload test run result bundle objects [#6373](https://github.com/tuist/tuist/pull/6373) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix extra logs from Xcode commands [#6372](https://github.com/tuist/tuist/pull/6372) by [@waltflanagan](https://github.com/waltflanagan) +- Fix missing platform-specific settings for SPM packages [#6386](https://github.com/tuist/tuist/pull/6386) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.16.1 - 2024-06-05 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.16.0 - 2024-06-04 + +### Tuist + +#### Added + +- Support xcodebuild arguments in build and test commands [#6300](https://github.com/tuist/tuist/pull/6300) by [@nandodelauni](https://github.com/nandodelauni) +- Upload test result bundle [#6345](https://github.com/tuist/tuist/pull/6345) by [@fortmarek](https://github.com/fortmarek) +- Making `tuist run` interactive by selecting simulators [#6307](https://github.com/tuist/tuist/pull/6307) by [@nandodelauni](https://github.com/nandodelauni) +- Support environment variable configuration for commands [#6359](https://github.com/tuist/tuist/pull/6359) by [@ajkolean](https://github.com/ajkolean) + +#### Fixed + +- Fixes #6321: Handle formatting multiline text [#6322](https://github.com/tuist/tuist/pull/6322) by [@sabade-omkar](https://github.com/sabade-omkar) +- Fix detection of XCTest dependency [#6337](https://github.com/tuist/tuist/pull/6337) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 4.15.0 - 2024-05-23 + +### Tuist + +#### Added + +- Support "Code Sign on Copy" in Copy Files Actions [#6302](https://github.com/tuist/tuist/pull/6302) by [@ActuallyTaylor](https://github.com/ActuallyTaylor) +- Add capability to specify variable entitlements [#6293](https://github.com/tuist/tuist/pull/6293) by [@lucasmpaim](https://github.com/lucasmpaim) + +#### Fixed + +- Fix missing -package-name when a target has custom swiftSettings [#6299](https://github.com/tuist/tuist/pull/6299) by [@fortmarek](https://github.com/fortmarek) +- Fix target linter warnings for target products that allow dots and hyphens [#6290](https://github.com/tuist/tuist/pull/6290) by [@kapitoshka438](https://github.com/kapitoshka438) +- Fix --platform case sensitivity with test and build commands [#6268](https://github.com/tuist/tuist/pull/6268) by [@waltflanagan](https://github.com/waltflanagan) +- Fix the initialization of projects in Homebrew-managed installations [#6309](https://github.com/tuist/tuist/pull/6309) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 4.14.0 - 2024-05-20 + +### Tuist + +#### Added + +- Add Widget Extension support to AppClips [#6287](https://github.com/tuist/tuist/pull/6287) by [@pepicrft](https://github.com/pepicrft) + +#### Fixed + +- Fix integration of SPM packages with slashes in their targets names [#6260](https://github.com/tuist/tuist/pull/6260) by [@kapitoshka438](https://github.com/kapitoshka438) +- Fix tuist generate when a binary SPM dependency is removed [#6298](https://github.com/tuist/tuist/pull/6298) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.13.0 - 2024-05-14 + +### Tuist + +#### Added + +- Add defaultConfiguration generation option to Config.swift [#6255](https://github.com/tuist/tuist/pull/6255) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix integration of SPM packages with spaces in their name [#6264](https://github.com/tuist/tuist/pull/6264) by [@kapitoshka438](https://github.com/kapitoshka438) +- Align bundle name with dashes sanitizing with SPM [#6265](https://github.com/tuist/tuist/pull/6265) by [@danibachar](https://github.com/danibachar) +- Do not automatically add -ObjC flag when integrating Objective-C dependencies [#6244](https://github.com/tuist/tuist/pull/6244) by [@thedavidharris](https://github.com/thedavidharris) + +### Tuist Cloud + +- no changes + +## 4.12.1 - 2024-05-07 + +### Tuist + +#### Added + +- Add Linting to require conditions for multiplatform dependencies with mismatched platforms. [#6251](https://github.com/tuist/tuist/pull/6251) by [@waltflanagan](https://github.com/waltflanagan) + +#### Fixed + +- Remove trailing backslash from Cloud url if present [#6258](https://github.com/tuist/tuist/pull/6258) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.12.0 - 2024-05-06 + +### Tuist + +#### Added + +- Add On Demand Resources Support [#6178](https://github.com/tuist/tuist/pull/6178) by [@kapitoshka438](https://github.com/kapitoshka438) + +#### Fixed + +- Add (void) to generated Obj-C Bundle accessor [#6247](https://github.com/tuist/tuist/pull/6247) by [@freak4pc](https://github.com/freak4pc) + +### Tuist Cloud + +- no changes + +## 4.11.0 - 2024-04-29 + +### Tuist + +#### Fixed + +- Fix generating CoreData models in resourceSynthesizer .coreData() [#6201](https://github.com/tuist/tuist/pull/6201) by [@alexfilimon](https://github.com/alexfilimon) +- Align resource bundle accessor generation with SPM [#6146](https://github.com/tuist/tuist/pull/6146) by [@danibachar](https://github.com/danibachar) +- Align bundle name sanization with SPM [#6234](https://github.com/tuist/tuist/pull/6234) by [@danibachar](https://github.com/danibachar) +- Revert "Embed external xcframeworks regardless of linking type (#6217)" [#6237](https://github.com/tuist/tuist/pull/6237) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.10.2 - 2024-04-23 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.10.1 - 2024-04-22 + +### Tuist + +#### Fixed + +- Fix regression in ModuleMapMapper due to outdated Graph properties [#6221](https://github.com/tuist/tuist/pull/6221) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.10.0 - 2024-04-22 + +### Tuist + +#### Added + +- Add support for privacy manifest file generation [#6117](https://github.com/tuist/tuist/pull/6117) by [@Lilfaen](https://github.com/Lilfaen) +- Add simulated location support on testable target configuration [#6187](https://github.com/tuist/tuist/pull/6187) by [@woin2ee](https://github.com/woin2ee) + +#### Fixed + +- Fix resources filename mismatch when a dependency has a + [#6151](https://github.com/tuist/tuist/pull/6151) by [@fortmarek](https://github.com/fortmarek) +- Fix integration of Cuckoo [#6195](https://github.com/tuist/tuist/pull/6195) by [@danibachar](https://github.com/danibachar) +- Fix missing external target settings with config conditions [#6170](https://github.com/tuist/tuist/pull/6170) by [@fortmarek](https://github.com/fortmarek) +- Skip rewriting modulemaps if not changed to fix issues with pch [#6212](https://github.com/tuist/tuist/pull/6212) by [@waltflanagan](https://github.com/waltflanagan) +- Use relative paths instead of absolute paths for header search paths for modulemaps [#6218](https://github.com/tuist/tuist/pull/6218) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.9.0 - 2024-04-02 + +### Tuist + +#### Changed + +- Improve the error message when parsing malformed property list files. [#6122](https://github.com/tuist/tuist/pull/6122) by [@pepicrft](https://github.com/pepicrft) +- Filter out empty auxiliary groups [#6142](https://github.com/tuist/tuist/pull/6142) by [@kwridan](https://github.com/kwridan) + +#### Fixed + +- Fix SPM integration of the latest version of swift-testing [#6121](https://github.com/tuist/tuist/pull/6121) by [@fortmarek](https://github.com/fortmarek) +- Fix adding newline at the end of tuist version in `.mise.toml` file [#6127](https://github.com/tuist/tuist/pull/6127) by [@dxmvsh](https://github.com/dxmvsh) +- Fix running Tuist.graph() in a Tuist task [#6129](https://github.com/tuist/tuist/pull/6129) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.8.1 - 2024-03-27 + +### Tuist + +#### Fixed + +- Fix released binaries missing `x86_64` Swift module interfaces for `ProjectDescription.framework`. + +### Tuist Cloud + +- no changes + +## 4.8.0 - 2024-03-26 + +### Tuist + +#### Added + +- Updated init template with `.mise.toml` and `Package.swift` files [#6044](https://github.com/tuist/tuist/pull/6044) by [@dxmvsh](https://github.com/dxmvsh) + +#### Fixed + +- Fix testing search path build settings when forcing explicit dependencies [#5773](https://github.com/tuist/tuist/pull/5773) by [@alexanderwe](https://github.com/alexanderwe) +- Fix integration of latest Firebase [#6104](https://github.com/tuist/tuist/pull/6104) by [@fortmarek](https://github.com/fortmarek) +- Fix custom configurations from target when `enforceExplicitDependencies` option is enabled [#6080](https://github.com/tuist/tuist/pull/6080) by [@fdzsergio](https://github.com/fdzsergio) +- Fix Swift Macros when embedded apps [#6109](https://github.com/tuist/tuist/pull/6109) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 4.7.0 - 2024-03-19 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.6.0 - 2024-03-11 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.5.1 - 2024-03-05 + +### Tuist + +#### Fixes + +* Fix generation of SPM packages when some resources are missing by @fortmarek in https://github.com/tuist/tuist/pull/6027 +* fix: Previews crash when accessing resources from cached XCFrameworks by @anlaital-oura in https://github.com/tuist/tuist/pull/6028 + +### Tuist Cloud + +- no changes + +## 4.4.0 - 2024-02-28 + +### Tuist + +#### Added + +* Add docs about GCS support by @fortmarek in https://github.com/tuist/tuist/pull/5997 +* Add support for the new `package` access modifier by @pepicrft in https://github.com/tuist/tuist/pull/5983 + +#### Fixed + +* Fix spm bundle recursion by @fortmarek in https://github.com/tuist/tuist/pull/5999 +* Avoid infinite loop caused by symbolic links by @haifengkao in https://github.com/tuist/tuist/pull/5813 +* Prune targets with no destinations after platform narrowing by @fortmarek in https://github.com/tuist/tuist/pull/5979 + +#### Changed + +* Upgrade to xcbeautify 1.6.0 by @cpisciotta in https://github.com/tuist/tuist/pull/5996 +* Update docs for optional Cloud.Option by @fortmarek in https://github.com/tuist/tuist/pull/6002 +* SPM dependencies now implicitly support all platforms by @fortmarek in https://github.com/tuist/tuist/pull/5990 + +#### Removed + +* Remove version command by @pepicrft in https://github.com/tuist/tuist/pull/5995 + +### Tuist Cloud + +#### Fixed + +* Add support for pasting the token to enable Tuist Cloud authentication via Safari by @pepicrft + +## 4.3.4 - 2024-02-21 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.3.3 - 2024-02-21 + +### Tuist + +#### Added + +- Migrate CI pipelines to Codemagic [#5913](https://github.com/tuist/tuist/pull/5913) by [@pepicrft](https://github.com/pepicrft) +- Support visionOS from the default asset templates to synthesize resources [#5963](https://github.com/tuist/tuist/pull/5963) by [@Ethan-IS](https://github.com/Ethan-IS) + +### Tuist Cloud + +#### Fixed + +- Override existing artifacts in the cache that are invalid by [@pepicrft](https://github.com/pepicrft) + +## 4.3.2 - 2024-02-21 + +### Tuist + +#### Changed + +- Remove PackageSettings platforms property [#5953](https://github.com/tuist/tuist/pull/5953) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +#### Fixed + +- Improve retrying logic when interacting with the remote cache by [@pepicrft](https://github.com/pepicrft) + +## 4.3.1 - 2024-02-20 + +### Tuist + +#### Fixed + +- Add missing `region` parameter in static helper `RunActionOptions`.`options` [#5954](https://github.com/tuist/tuist/pull/5954) by [@mihaicris-adoreme](https://github.com/mihaicris-adoreme) +- Fix integration of local packages with Objective C targets [#5957](https://github.com/tuist/tuist/pull/5957) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +#### Changed + +- Improve error handling in the remote storage by [@pepicrft](https://github.com/pepicrft) +- Increase Tuist Cloud requests' timeout limit by [@pepicrft](https://github.com/pepicrft) + +## 4.3.0 - 2024-02-19 + +### Tuist + +#### Changed + +- Make Tuist/Package.swift a valid location for the SPM manifest [#5947](https://github.com/tuist/tuist/pull/5947) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix integration of AppCenter [#5935](https://github.com/tuist/tuist/pull/5935) by [@fortmarek](https://github.com/fortmarek) +- Fix integration of SPM dependencies with resources [#5945](https://github.com/tuist/tuist/pull/5945) by [@fortmarek](https://github.com/fortmarek) +- Fix DEFINES_MODULE warning [#5946](https://github.com/tuist/tuist/pull/5946) by [@fortmarek](https://github.com/fortmarek) +- Fix failing tuist install when a package is removed [#5948](https://github.com/tuist/tuist/pull/5948) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.2.5 - 2024-02-16 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Fixed + +- Fix storing of artifacts in Tuist Cloud by [@pepicrft](https://github.com/pepicrft) + +## 4.2.4 - 2024-02-16 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Fixed + +- Fix bug when compressing the artifacts to upload them to Tuist Cloud by [@pepicrft](https://github.com/pepicrft) + +## 4.2.3 - 2024-02-15 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Changed + +- Only show the progress bar when we fetch and store remote artifacts by [@pepicrft](https://github.com/pepicrft) + +#### Fixed + +- Reduce the network concurrency and add automatic retries when interacting with the network by [@pepicrft](https://github.com/pepicrft) + +## 4.2.2 - 2024-02-15 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Fixed + +- Fix runtime network error due to unlimited connections when persisting cache artifacts by [@pepicrft](https://github.com/pepicrft) + +## 4.2.1 - 2024-02-15 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Fixed + +- Fix generation with targets absent in the cache by [@pepicrft](https://github.com/pepicrft) + +## 4.2.0 - 2024-02-14 + +### Tuist + +#### Changed + +- Improve the error when users try to build or test multi-platform targets [#5919](https://github.com/tuist/tuist/pull/5919) by [@pepicrft](https://github.com/pepicrft) +- Remove Mockable from the list of targets that depend on XCTest [#5923](https://github.com/tuist/tuist/pull/5923) by [@pepicrft](https://github.com/pepicrft) +- Improve performance by replacing globing with FileManager when finding resource files [#5922](https://github.com/tuist/tuist/pull/5922) by [@anlaital-oura](https://github.com/anlaital-oura) +- Automatically group local packages to `Packages` group [#5876](https://github.com/tuist/tuist/pull/5876) by [@Lommelun](https://github.com/Lommelun) + +#### Fixed + +- Fix runtime crash when using Obj-C dependencies [#5929](https://github.com/tuist/tuist/pull/5929) by [@fortmarek](https://github.com/fortmarek) +- Fix external platform narrowing when there is a dependency platform condition [#5931](https://github.com/tuist/tuist/pull/5931) by [@fortmarek](https://github.com/fortmarek) +- Fix integration of Objective C dependencies with resources [#5932](https://github.com/tuist/tuist/pull/5932) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +#### Fixed + +- Do not skip a whole target if only a suite is passed by [@fortmarek](https://github.com/fortmarek) + +## 4.1.2 - 2024-02-13 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 4.1.1 - 2024-02-12 + +### Tuist + +#### Fixed + +- Fix caching package manifests [#5914](https://github.com/tuist/tuist/pull/5914) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.1.0 - 2024-02-12 + +### Tuist + +#### Changed + +- Improve verbose logging by logging the workspace, project, and workspace transformations [#5905](https://github.com/tuist/tuist/pull/5905) by [@pepicrft](https://github.com/pepicrft) + +#### Added + +- Support visionOS from the default templates to synthesize resources [#5892](https://github.com/tuist/tuist/pull/5892) by [@PushedCrayon](https://github.com/PushedCrayon) + +#### Fixed + +- Fix integration of Objective C dependencies [#5887](https://github.com/tuist/tuist/pull/5887) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 4.0.0-beta.4 - 2024-02-05 + +### Tuist + +#### Changed + +- Rename `fetch` to `install` [#5857](https://github.com/tuist/tuist/pull/5857) by [@pepicrft](https://github.com/pepicrft) +- Change Package.swift directory from Tuist to root [#5862](https://github.com/tuist/tuist/pull/5862) by [@fortmarek](https://github.com/fortmarek) +- Align the cache directory with the UNIX XDG Base Directory Specification [#5855](https://github.com/tuist/tuist/pull/5855) by [@pepicrft](https://github.com/pepicrft) +- Improve performance of resolving manifest file paths [#5871](https://github.com/tuist/tuist/pull/5871) by [@anlaital-oura](https://github.com/anlaital-oura) + +#### Fixed + +- Fix running tuist clean without a specific category [#5868](https://github.com/tuist/tuist/pull/5868) by [@fortmarek](https://github.com/fortmarek) +- Fix tuist clean dependencies [#5872](https://github.com/tuist/tuist/pull/5872) by [@fortmarek](https://github.com/fortmarek) +- Throw an error when focusing on a non-existent target [#5874](https://github.com/tuist/tuist/pull/5874) by [@fortmarek](https://github.com/fortmarek) +- Fix error message on missing dependencies [#5875](https://github.com/tuist/tuist/pull/5875) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +#### Changed + +- Fix tuist clean, remove --skip-cache by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fixed binary-caching of macOS targets might fail due to missing destinations by [@fortmarek](https://github.com/fortmarek) + +## 4.0.0-beta.3 - 2024-02-05 + +### Tuist + +#### Changed + +- Rename `fetch` to `install` [#5857](https://github.com/tuist/tuist/pull/5857) by [@pepicrft](https://github.com/pepicrft) +- Change Package.swift directory from Tuist to root [#5862](https://github.com/tuist/tuist/pull/5862) by [@fortmarek](https://github.com/fortmarek) +- Align the cache directory with the UNIX XDG Base Directory Specification [#5855](https://github.com/tuist/tuist/pull/5855) by [@pepicrft](https://github.com/pepicrft) +- Improve performance of resolving manifest file paths [#5871](https://github.com/tuist/tuist/pull/5871) by [@anlaital-oura](https://github.com/anlaital-oura) + +#### Fixed + +- Fix running tuist clean without a specific category [#5868](https://github.com/tuist/tuist/pull/5868) by [@fortmarek](https://github.com/fortmarek) +- Fix tuist clean dependencies [#5872](https://github.com/tuist/tuist/pull/5872) by [@fortmarek](https://github.com/fortmarek) +- Throw an error when focusing on a non-existent target [#5874](https://github.com/tuist/tuist/pull/5874) by [@fortmarek](https://github.com/fortmarek) +- Fix error message on missing dependencies [#5875](https://github.com/tuist/tuist/pull/5875) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +#### Changed + +- Fix tuist clean, remove --skip-cache by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fixed binary-caching of macOS targets might fail due to missing destinations by [@fortmarek](https://github.com/fortmarek) + +## 4.0.0-beta.2 - 2024-02-01 + +### Tuist + +#### Fixed + +- Fix install script [#5858](https://github.com/tuist/tuist/pull/5858) by [@svenmuennich](https://github.com/svenmuennich) + +#### Changed + +- Change Package.swift directory from Tuist to root [#5862](https://github.com/tuist/tuist/pull/5862) by [@fortmarek](https://github.com/fortmarek) +- Rename `fetch` to `install` [#5857](https://github.com/tuist/tuist/pull/5857) by [@pepicrft](https://github.com/pepicrft) +- Align the cache directory with the UNIX XDG Base Directory Specification [#5855](https://github.com/tuist/tuist/pull/5855) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +#### Changed + +- no changes + +## 4.0.0-beta-1 - 2024-01-31 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Changed + +- Add --no-selective-testing flag, rename --no-cache to --no-binary-cache by [@fortmarek](https://github.com/fortmarek) + ## 3.42.2 - 2024-01-30 ### Tuist diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8357643b34d..198832b7974 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,76 +1,3 @@ -# Contributor Covenant Code of Conduct +# Code of conduct -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at pedro@tuist.io. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +The code of conduct is available in [Tuist's handbook](https://handbook.tuist.io/people/code-of-conduct.html). \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index e8653e8167e..56dd65abd04 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "87bcc64457d6f46746dad13b0f06d8fab58d49517e8eda1547b0604ab357c8fe", "pins" : [ { "identity" : "aexml", @@ -28,30 +29,30 @@ } }, { - "identity" : "combineext", + "identity" : "difference", "kind" : "remoteSourceControl", - "location" : "https://github.com/CombineCommunity/CombineExt.git", + "location" : "https://github.com/krzysztofzablocki/Difference.git", "state" : { - "revision" : "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", - "version" : "1.8.1" + "revision" : "f627d00718033c3d7888acd5f4e3524a843db1cf", + "version" : "1.0.2" } }, { - "identity" : "cryptoswift", + "identity" : "filesystem", "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "location" : "https://github.com/tuist/FileSystem.git", "state" : { - "revision" : "db51c407d3be4a051484a141bf0bff36c43d3b1e", - "version" : "1.8.0" + "revision" : "0cbe28158a51ca6234dd00da59be88de2b7b22be", + "version" : "0.2.0" } }, { "identity" : "graphviz", "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftDocOrg/GraphViz.git", + "location" : "https://github.com/tuist/GraphViz.git", "state" : { - "revision" : "70bebcf4597b9ce33e19816d6bbd4ba9b7bdf038", - "version" : "0.2.0" + "branch" : "0.2.1", + "revision" : "e4e0796a8fa74b000aba54b0256601abf75d0307" } }, { @@ -81,6 +82,15 @@ "version" : "1.1.3" } }, + { + "identity" : "mockable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kolos65/Mockable.git", + "state" : { + "revision" : "81ccaead99a3c038c09345caa2888ae74b644ee9", + "version" : "0.0.9" + } + }, { "identity" : "packageconfig", "kind" : "remoteSourceControl", @@ -90,6 +100,15 @@ "version" : "1.1.3" } }, + { + "identity" : "path", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/Path.git", + "state" : { + "revision" : "4490da629937fc3994f72dd787dcc3f50d6973fe", + "version" : "0.3.0" + } + }, { "identity" : "pathkit", "kind" : "remoteSourceControl", @@ -149,8 +168,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.0" + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" } }, { @@ -158,8 +195,44 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "fc79798d5a150d61361a27ce0c51169b889e23de", + "version" : "2.68.0" + } + }, + { + "identity" : "swift-openapi-runtime", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/swift-openapi-runtime", + "state" : { + "branch" : "swift-tools-version", + "revision" : "863d51ca0b55a72143462f4517065d906a24e0fb" + } + }, + { + "identity" : "swift-openapi-urlsession", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/swift-openapi-urlsession", + "state" : { + "branch" : "swift-tools-version", + "revision" : "91b1694705584a09827049b690e07f9118cdab5d" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" } }, { @@ -180,14 +253,6 @@ "version" : "0.6.1" } }, - { - "identity" : "swifter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter.git", - "state" : { - "revision" : "1e4f51c92d7ca486242d8bf0722b99de2c3531aa" - } - }, { "identity" : "swiftgen", "kind" : "remoteSourceControl", @@ -202,8 +267,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/cpisciotta/xcbeautify", "state" : { - "revision" : "84d24a9854e6fdcd2c91122d50a3189b072e8136", - "version" : "1.4.0" + "revision" : "d7c9ce7df96118aaa56096f4c2b94265c8e28a7c", + "version" : "2.5.0" + } + }, + { + "identity" : "xcodegraph", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeGraph.git", + "state" : { + "revision" : "179fbd4fa5da6ddbdd67e541f4eb4afe6438f86d", + "version" : "0.10.0" } }, { @@ -211,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeProj", "state" : { - "revision" : "3797181813ee963fe305d939232bc576d23ddbb0", - "version" : "8.15.0" + "revision" : "75e787fb3eb5a8397c3d06d5a71e667ac2d20ac1", + "version" : "8.19.0" } }, { @@ -238,10 +312,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/weichsel/ZIPFoundation.git", "state" : { - "revision" : "a3f5c2bae0f04b0bce9ef3c4ba6bd1031a0564c4", - "version" : "0.9.17" + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 1ea02c28acd..cf06a4acdc1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,19 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.10 import PackageDescription let swiftToolsSupportDependency: Target.Dependency = .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core") +let pathDependency: Target.Dependency = .product(name: "Path", package: "Path") let loggingDependency: Target.Dependency = .product(name: "Logging", package: "swift-log") let argumentParserDependency: Target.Dependency = .product(name: "ArgumentParser", package: "swift-argument-parser") let swiftGenKitDependency: Target.Dependency = .product(name: "SwiftGenKit", package: "SwiftGen") -let swifterDependency: Target.Dependency = .product(name: "Swifter", package: "swifter") -let combineExtDependency: Target.Dependency = .byName(name: "CombineExt") var targets: [Target] = [ .executableTarget( name: "tuistbenchmark", dependencies: [ argumentParserDependency, + pathDependency, swiftToolsSupportDependency, ] ), @@ -21,35 +21,22 @@ var targets: [Target] = [ name: "tuistfixturegenerator", dependencies: [ argumentParserDependency, + pathDependency, swiftToolsSupportDependency, ] ), - .target( - name: "TuistGraph", - dependencies: [ - swiftToolsSupportDependency, - "AnyCodable", - "TuistSupport", - ] - ), - .target( - name: "TuistGraphTesting", - dependencies: [ - "TuistGraph", - "TuistSupportTesting", - swiftToolsSupportDependency, - "AnyCodable", - ], - linkerSettings: [.linkedFramework("XCTest")] - ), .target( name: "TuistCore", dependencies: [ - swiftToolsSupportDependency, + pathDependency, "ProjectDescription", "TuistSupport", - "TuistGraph", + "XcodeGraph", "XcodeProj", + "Mockable", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( @@ -57,8 +44,7 @@ var targets: [Target] = [ dependencies: [ "TuistCore", "TuistSupportTesting", - "TuistGraphTesting", - swiftToolsSupportDependency, + pathDependency, ], linkerSettings: [.linkedFramework("XCTest")] ), @@ -66,7 +52,7 @@ var targets: [Target] = [ name: "TuistKit", dependencies: [ "XcodeProj", - swiftToolsSupportDependency, + pathDependency, argumentParserDependency, "TuistSupport", "TuistGenerator", @@ -75,14 +61,21 @@ var targets: [Target] = [ "ProjectAutomation", "TuistLoader", "TuistScaffold", - "TuistSigning", "TuistDependencies", "GraphViz", "TuistMigration", "TuistAsyncQueue", "TuistAnalytics", "TuistPlugin", - "TuistGraph", + "XcodeGraph", + "Mockable", + "TuistServer", + "FileSystem", + "TuistCache", + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .executableTarget( @@ -91,20 +84,7 @@ var targets: [Target] = [ "TuistKit", "ProjectDescription", "ProjectAutomation", - ] - ), - .target( - name: "TuistEnvKit", - dependencies: [ - argumentParserDependency, swiftToolsSupportDependency, - "TuistSupport", - ] - ), - .executableTarget( - name: "tuistenv", - dependencies: [ - "TuistEnvKit", ] ), .target( @@ -117,21 +97,26 @@ var targets: [Target] = [ .target( name: "TuistSupport", dependencies: [ - combineExtDependency, - swiftToolsSupportDependency, + pathDependency, loggingDependency, + swiftToolsSupportDependency, "KeychainAccess", - swifterDependency, "ZIPFoundation", "ProjectDescription", + "Mockable", + "FileSystem", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistSupportTesting", dependencies: [ "TuistSupport", - "TuistGraph", - swiftToolsSupportDependency, + "XcodeGraph", + pathDependency, + "Difference", ], linkerSettings: [.linkedFramework("XCTest")] ), @@ -143,7 +128,7 @@ var targets: [Target] = [ "TuistSupport", "TuistSupportTesting", "XcodeProj", - swiftToolsSupportDependency, + pathDependency, ], linkerSettings: [.linkedFramework("XCTest")] ), @@ -151,53 +136,58 @@ var targets: [Target] = [ name: "TuistGenerator", dependencies: [ "XcodeProj", - swiftToolsSupportDependency, + pathDependency, "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", "GraphViz", swiftGenKitDependency, "StencilSwiftKit", + "Mockable", + "FileSystem", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistGeneratorTesting", dependencies: [ "TuistGenerator", - swiftToolsSupportDependency, + pathDependency, ], linkerSettings: [.linkedFramework("XCTest")] ), .target( name: "TuistScaffold", dependencies: [ - swiftToolsSupportDependency, + pathDependency, "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", "StencilSwiftKit", "Stencil", + "Mockable", + "FileSystem", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistAutomation", dependencies: [ "XcodeProj", - swiftToolsSupportDependency, + pathDependency, .product(name: "XcbeautifyLib", package: "xcbeautify"), "TuistCore", - "TuistGraph", - "TuistSupport", - ] - ), - .target( - name: "TuistSigning", - dependencies: [ - "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", - "CryptoSwift", - swiftToolsSupportDependency, + "Mockable", + "FileSystem", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( @@ -205,51 +195,67 @@ var targets: [Target] = [ dependencies: [ "ProjectDescription", "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", "TuistPlugin", - swiftToolsSupportDependency, + "Mockable", + pathDependency, + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistMigration", dependencies: [ "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", "XcodeProj", - swiftToolsSupportDependency, + "Mockable", + pathDependency, + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistAsyncQueue", dependencies: [ "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", "XcodeProj", - swiftToolsSupportDependency, + "Mockable", + pathDependency, "Queuer", + "FileSystem", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistLoader", dependencies: [ "XcodeProj", - swiftToolsSupportDependency, + pathDependency, "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistSupport", + "Mockable", "ProjectDescription", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistLoaderTesting", dependencies: [ "TuistLoader", - swiftToolsSupportDependency, + pathDependency, "TuistCore", - "TuistGraphTesting", "ProjectDescription", "TuistSupportTesting", ], @@ -261,23 +267,93 @@ var targets: [Target] = [ .byName(name: "AnyCodable"), "TuistAsyncQueue", "TuistCore", - "TuistGraph", + "XcodeGraph", "TuistLoader", - swiftToolsSupportDependency, + "Mockable", + pathDependency, + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), .target( name: "TuistPlugin", dependencies: [ - "TuistGraph", + "XcodeGraph", "TuistLoader", "TuistSupport", "TuistScaffold", - swiftToolsSupportDependency, + "Mockable", + "FileSystem", + pathDependency, + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), + ] + ), + .target( + name: "TuistServer", + dependencies: [ + "TuistCore", + "TuistSupport", + "FileSystem", + pathDependency, + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), + ], + exclude: ["OpenAPI/server.yml"], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), + ] + ), + .target( + name: "TuistHasher", + dependencies: [ + "TuistCore", + "TuistSupport", + "FileSystem", + pathDependency, + "XcodeGraph", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), + ] + ), + .target( + name: "TuistCache", + dependencies: [ + "TuistCore", + "TuistSupport", + "FileSystem", + pathDependency, + "XcodeGraph", + "TuistHasher", + ], + swiftSettings: [ + .define("MOCKING", .when(configuration: .debug)), ] ), ] +#if TUIST + import struct ProjectDescription.PackageSettings + + let packageSettings = PackageSettings( + productTypes: [ + "FileSystem": .staticFramework, + "TSCBasic": .staticFramework, + "TSCUtility": .staticFramework, + "TSCclibc": .staticFramework, + "TSCLibc": .staticFramework, + "ArgumentParser": .staticFramework, + "Mockable": .staticFramework, + "MockableTest": .staticFramework, + ], + baseSettings: .settings(base: ["GENERATE_MASTER_OBJECT_FILE": "YES"]) + ) + +#endif + let package = Package( name: "tuist", platforms: [.macOS(.v12)], @@ -285,7 +361,6 @@ let package = Package( .executable(name: "tuistbenchmark", targets: ["tuistbenchmark"]), .executable(name: "tuistfixturegenerator", targets: ["tuistfixturegenerator"]), .executable(name: "tuist", targets: ["tuist"]), - .executable(name: "tuistenv", targets: ["tuistenv"]), .library( name: "ProjectDescription", type: .dynamic, @@ -297,12 +372,8 @@ let package = Package( targets: ["ProjectAutomation"] ), .library( - name: "TuistGraph", - targets: ["TuistGraph"] - ), - .library( - name: "TuistGraphTesting", - targets: ["TuistGraphTesting"] + name: "ProjectAutomation-auto", + targets: ["ProjectAutomation"] ), .library( name: "TuistKit", @@ -340,10 +411,6 @@ let package = Package( name: "TuistAutomation", targets: ["TuistAutomation"] ), - .library( - name: "TuistSigning", - targets: ["TuistSigning"] - ), .library( name: "TuistDependencies", targets: ["TuistDependencies"] @@ -352,6 +419,18 @@ let package = Package( name: "TuistAcceptanceTesting", targets: ["TuistAcceptanceTesting"] ), + .library( + name: "TuistServer", + targets: ["TuistServer"] + ), + .library( + name: "TuistHasher", + targets: ["TuistHasher"] + ), + .library( + name: "TuistCache", + targets: ["TuistCache"] + ), /// TuistGenerator /// /// A high level Xcode generator library @@ -369,22 +448,26 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.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"), .package(url: "https://github.com/FabrizioBrancati/Queuer", from: "2.1.1"), .package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.7"), - .package(url: "https://github.com/weichsel/ZIPFoundation", from: "0.9.17"), - .package(url: "https://github.com/httpswift/swifter.git", revision: "1e4f51c92d7ca486242d8bf0722b99de2c3531aa"), + .package(url: "https://github.com/weichsel/ZIPFoundation", from: "0.9.19"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.8.0"), .package(url: "https://github.com/stencilproject/Stencil", exact: "0.15.1"), - .package(url: "https://github.com/SwiftDocOrg/GraphViz", exact: "0.2.0"), + .package(url: "https://github.com/tuist/GraphViz.git", branch: "0.2.1"), .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/cpisciotta/xcbeautify", from: "1.4.0"), + .package(url: "https://github.com/tuist/XcodeProj", exact: "8.19.0"), + .package(url: "https://github.com/cpisciotta/xcbeautify", .upToNextMajor(from: "2.5.0")), + .package(url: "https://github.com/krzysztofzablocki/Difference.git", from: "1.0.2"), + .package(url: "https://github.com/Kolos65/Mockable.git", from: "0.0.9"), + .package(url: "https://github.com/tuist/swift-openapi-runtime", branch: "swift-tools-version"), + .package(url: "https://github.com/tuist/swift-openapi-urlsession", branch: "swift-tools-version"), + .package(url: "https://github.com/tuist/Path", .upToNextMajor(from: "0.3.0")), + .package(url: "https://github.com/tuist/XcodeGraph.git", exact: "0.10.0"), + .package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.2.0")), ], targets: targets ) diff --git a/Project.swift b/Project.swift index d3aa4be0757..4bedb211f3e 100644 --- a/Project.swift +++ b/Project.swift @@ -13,525 +13,70 @@ func releaseSettings() -> SettingsDictionary { baseSettings } -func targets() -> [Target] { - let executableTargets = [ - Target.target( - name: "tuistenv", - product: .commandLineTool, - dependencies: [ - .target(name: "TuistEnvKit"), - ] - ), - Target.target( - name: "tuist", - product: .commandLineTool, - dependencies: [ - .target(name: "TuistKit"), - .target(name: "ProjectDescription"), - .target(name: "ProjectAutomation"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "GraphViz"), - .external(name: "ArgumentParser"), - ], - settings: .settings( - base: [ - "LD_RUNPATH_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", - ], - configurations: [ - .debug(name: "Debug", settings: [:], xcconfig: nil), - .release(name: "Release", settings: [:], xcconfig: nil), - ] - ) - ), - Target.target( - name: "tuistbenchmark", - product: .commandLineTool, - dependencies: [ - .external(name: "ArgumentParser"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ] - ), - Target.target( - name: "tuistfixturegenerator", - product: .commandLineTool, - dependencies: [ - .external(name: "ArgumentParser"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ] - ), - Target.target( - name: "TuistIntegrationTests", - product: .unitTests, - dependencies: [ - .target(name: "TuistGenerator"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistSupport"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistLoaderTesting"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "XcodeProj"), - ] - ), - ] - let moduleTargets = [ - Target.module( - name: "TuistSupport", - hasIntegrationTests: true, - dependencies: [ - .target(name: "ProjectDescription"), - .external(name: "AnyCodable"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "XcodeProj"), - .external(name: "KeychainAccess"), - .external(name: "CombineExt"), - .external(name: "Logging"), - .external(name: "ZIPFoundation"), - .external(name: "Swifter"), - ], - testingDependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - ] - ), - Target.module( - name: "TuistKit", - hasTesting: false, - hasIntegrationTests: true, - dependencies: [ - .target(name: "TuistSupport"), - .target(name: "TuistGenerator"), - .target(name: "TuistAutomation"), - .target(name: "ProjectDescription"), - .target(name: "ProjectAutomation"), - .target(name: "TuistLoader"), - .target(name: "TuistScaffold"), - .target(name: "TuistSigning"), - .target(name: "TuistDependencies"), - .target(name: "TuistMigration"), - .target(name: "TuistAsyncQueue"), - .target(name: "TuistAnalytics"), - .target(name: "TuistPlugin"), - .target(name: "TuistGraph"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "ArgumentParser"), - .external(name: "GraphViz"), - .external(name: "AnyCodable"), - ], - testDependencies: [ - .target(name: "TuistAutomation"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "ProjectDescription"), - .target(name: "ProjectAutomation"), - .target(name: "TuistLoaderTesting"), - .target(name: "TuistGeneratorTesting"), - .target(name: "TuistScaffoldTesting"), - .target(name: "TuistAutomationTesting"), - .target(name: "TuistSigningTesting"), - .target(name: "TuistDependenciesTesting"), - .target(name: "TuistMigrationTesting"), - .target(name: "TuistAsyncQueueTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistPlugin"), - .target(name: "TuistPluginTesting"), - .external(name: "ArgumentParser"), - .external(name: "GraphViz"), - .external(name: "AnyCodable"), - ], - integrationTestsDependencies: [ - .target(name: "TuistCoreTesting"), - .target(name: "TuistSupportTesting"), - .target(name: "ProjectDescription"), - .target(name: "ProjectAutomation"), - .target(name: "TuistLoaderTesting"), - .target(name: "TuistGraphTesting"), - .external(name: "XcodeProj"), - ] - ), - Target.module( - name: "TuistEnvKit", - hasTesting: false, - dependencies: [ - .target(name: "TuistSupport"), - .external(name: "ArgumentParser"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - ] - ), - Target.module( - name: "TuistGraph", - dependencies: [ - .target(name: "TuistSupport"), - .external(name: "AnyCodable"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ], - testDependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistSupport"), - .target(name: "TuistSupportTesting"), - .external(name: "XcodeProj"), - ], - testingDependencies: [ - .target(name: "TuistSupport"), - .target(name: "TuistSupportTesting"), - .external(name: "XcodeProj"), - ] - ), - Target.module( - name: "TuistCore", - hasIntegrationTests: true, - dependencies: [ - .target(name: "ProjectDescription"), - .target(name: "TuistSupport"), - .target(name: "TuistGraph"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "XcodeProj"), - ], - testDependencies: [ - .target(name: "TuistSupport"), - .target(name: "TuistGraph"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - ], - testingDependencies: [ - .target(name: "TuistSupport"), - .target(name: "TuistGraph"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - ], - integrationTestsDependencies: [ - .target(name: "TuistSupportTesting"), - ] - ), - Target.module( - name: "TuistGenerator", - hasIntegrationTests: true, - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "SwiftGenKit"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "PathKit"), - .external(name: "StencilSwiftKit"), - .external(name: "XcodeProj"), - .external(name: "GraphViz"), - ], - testDependencies: [ - .target(name: "TuistCoreTesting"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - .external(name: "XcodeProj"), - .external(name: "GraphViz"), - ], - testingDependencies: [ - .target(name: "TuistCoreTesting"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - .external(name: "XcodeProj"), - ], - integrationTestsDependencies: [ - .target(name: "TuistCoreTesting"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistSigningTesting"), - .external(name: "XcodeProj"), - ] - ), - Target.module( - name: "TuistScaffold", - hasIntegrationTests: true, - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "PathKit"), - .external(name: "StencilSwiftKit"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - ], - testingDependencies: [ - .target(name: "TuistGraphTesting"), - .target(name: "TuistGraph"), - ], - integrationTestsDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - ] - ), - Target.module( - name: "TuistLoader", - hasIntegrationTests: true, - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .target(name: "ProjectDescription"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "XcodeProj"), - ], - testDependencies: [ - .target(name: "TuistGraphTesting"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - ], - testingDependencies: [ - .target(name: "TuistCore"), - .target(name: "ProjectDescription"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistGraph"), - ], - integrationTestsDependencies: [ - .target(name: "TuistGraphTesting"), - .target(name: "TuistSupportTesting"), - .target(name: "ProjectDescription"), - ] - ), - Target.module( - name: "TuistAsyncQueue", - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "Queuer"), - .external(name: "XcodeProj"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - .external(name: "Queuer"), - ], - testingDependencies: [ - .target(name: "TuistGraphTesting"), - ] - ), - Target.module( - name: "TuistPlugin", - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistLoader"), - .target(name: "TuistSupport"), - .target(name: "TuistScaffold"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ], - testDependencies: [ - .target(name: "ProjectDescription"), - .target(name: "TuistLoader"), - .target(name: "TuistLoaderTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistSupport"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistScaffoldTesting"), - .target(name: "TuistCoreTesting"), - ], - testingDependencies: [ - .target(name: "TuistGraph"), - ] - ), - Target.module( - name: "ProjectDescription", - product: .framework, - hasTesting: false, - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistSupport"), - ] - ), - Target.module( - name: "ProjectAutomation", - product: .framework, - hasTests: false, - hasTesting: false, - dependencies: [] - ), - Target.module( - name: "TuistSigning", - hasIntegrationTests: false, - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "CryptoSwift"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - ], - testingDependencies: [ - .target(name: "TuistGraphTesting"), - ] - ), - Target.module( - name: "TuistAnalytics", - hasTests: false, - hasTesting: false, - dependencies: [ - .target(name: "TuistAsyncQueue"), - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistLoader"), - .target(name: "TuistSupport"), - .external(name: "AnyCodable"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistCoreTesting"), - ] - ), - Target.module( - name: "TuistMigration", - hasIntegrationTests: true, - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "PathKit"), - .external(name: "XcodeProj"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - ], - testingDependencies: [ - .target(name: "TuistGraphTesting"), - ], - integrationTestsDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - ] - ), - Target.module( - name: "TuistDependencies", - dependencies: [ - .target(name: "ProjectDescription"), - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ], - testDependencies: [ - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - .target(name: "TuistLoaderTesting"), - .target(name: "TuistSupportTesting"), - ], - testingDependencies: [ - .target(name: "TuistGraphTesting"), - .target(name: "ProjectDescription"), - ] - ), - Target.module( - name: "TuistAutomation", - hasIntegrationTests: true, - dependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistGraph"), - .target(name: "TuistSupport"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .external(name: "XcodeProj"), - .external(name: "XcbeautifyLib"), - ], - testDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistCoreTesting"), - .target(name: "TuistGraphTesting"), - ], - testingDependencies: [ - .target(name: "TuistCore"), - .target(name: "TuistCoreTesting"), - .target(name: "ProjectDescription"), - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - ], - integrationTestsDependencies: [ - .target(name: "TuistSupportTesting"), - .target(name: "TuistGraphTesting"), - ] - ), - ].flatMap { $0 } - - return executableTargets + moduleTargets + acceptanceTests.map(\.target) + [ - .target( - name: "TuistAcceptanceTesting", - 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), - ] - ), +func acceptanceTestsEnvironmentVariables() -> [String: EnvironmentVariable] { + [ + "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", + "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", + "TUIST_AUTH_EMAIL": "tuist@tuist.io", + "TUIST_AUTH_PASSWORD": "tuistrocks", ] } -let acceptanceTests: [(target: Target, scheme: Scheme)] = ["", "Build", "Dependencies", "Generate", "Test"].map { - ( - target: .target( - name: "Tuist\($0)AcceptanceTests", - product: .unitTests, - dependencies: [ - .target(name: "TuistAcceptanceTesting"), - .target(name: "TuistSupportTesting"), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ] +func schemes() -> [Scheme] { + var schemes: [Scheme] = [ + .scheme( + name: "Tuist-Workspace", + buildAction: .buildAction(targets: Module.allCases.flatMap(\.targets).map(\.name).sorted().map { .target($0) }), + testAction: .targets( + Module.allCases.flatMap(\.testTargets).map { .testableTarget(target: .target($0.name)) } + ), + runAction: .runAction( + arguments: .arguments( + environmentVariables: acceptanceTestsEnvironmentVariables() + ) + ) + ), + .scheme( + name: "TuistAcceptanceTests", + buildAction: .buildAction( + targets: Module.allCases.flatMap(\.acceptanceTestTargets).map(\.name).sorted() + .map { .target($0) } + ), + testAction: .targets( + Module.allCases.flatMap(\.acceptanceTestTargets).map { .testableTarget(target: .target($0.name)) } + ), + runAction: .runAction( + arguments: .arguments( + environmentVariables: acceptanceTestsEnvironmentVariables() + ) + ) ), - scheme: Scheme( - name: "Tuist\($0)AcceptanceTests", - buildAction: BuildAction(targets: ["Tuist\($0)AcceptanceTests"]), + .scheme( + name: "TuistUnitTests", + buildAction: .buildAction( + targets: Module.allCases.flatMap(\.unitTestTargets).map(\.name).sorted() + .map { .target($0) } + ), testAction: .targets( - [ - TestableTarget( - target: "Tuist\($0)AcceptanceTests", - parallelizable: true, - randomExecutionOrdering: true - ), - ] + Module.allCases.flatMap(\.unitTestTargets).map { .testableTarget(target: .target($0.name)) } ), runAction: .runAction( - arguments: Arguments( + arguments: .arguments( + environmentVariables: [ + "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", + "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", + ] + ) + ) + ), + ] + schemes.append(contentsOf: Module.allCases.filter(\.isRunnable).map { + .scheme( + name: $0.targetName, + buildAction: .buildAction(targets: [.target($0.targetName)]), + runAction: .runAction( + executable: .target($0.targetName), + arguments: .arguments( environmentVariables: [ "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", @@ -539,12 +84,29 @@ let acceptanceTests: [(target: Target, scheme: Scheme)] = ["", "Build", "Depende ) ) ) - ) + }) + + schemes.append(contentsOf: Module.allCases.compactMap(\.acceptanceTestsTargetName).map { + .scheme( + name: $0, + hidden: true, + buildAction: .buildAction(targets: [.target($0)]), + testAction: .targets([.testableTarget(target: .target($0))]), + runAction: .runAction( + arguments: .arguments( + environmentVariables: acceptanceTestsEnvironmentVariables() + ) + ) + ) + }) + + return schemes } let project = Project( name: "Tuist", options: .options( + automaticSchemesOptions: .disabled, textSettings: .textSettings(usesTabs: false, indentWidth: 4, tabWidth: 4) ), settings: .settings( @@ -553,13 +115,10 @@ let project = Project( .release(name: "Release", settings: releaseSettings(), xcconfig: nil), ] ), - targets: targets(), - schemes: acceptanceTests.map(\.scheme), + targets: Module.allCases.flatMap(\.targets), + schemes: schemes(), additionalFiles: [ "CHANGELOG.md", "README.md", - "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 80b562fa098..13831ffc9d8 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,337 @@
-
- Tuist - Latest Version - Commit Activity - Latest Commits - Twitter Followers - Slack Workspace - Sponsors - Backers - License - Powered by Tuist - +
+ header +
+ CI status + Commit Activity + Twitter Followers + Slack Workspace + Slack Workspace +
+ Book us with Cal.com +
-
- Book us with Cal.com -
- -## What's Tuist 🕺 +## 🕺 What's Tuist -Tuist is a command line tool that helps you **generate**, **maintain** and **interact** with Xcode projects. +Tuist is a command line tool that leverages **_project generation_** to abstract intricacies of Xcode projects, and uses it as a foundation to help teams **_maintain_** and **_optimize_** their large modular projects. It's open source and written in Swift. -## Install ⬇️ +## ⬇️ Install -### Recommended: [mise](https://github.com/jdx/mise) - -Install [mise](https://mise.jdx.dev/getting-started.html#quickstart) and then run the following command to install Tuist: - -```bash -mise install tuist -``` +The recommended installation method is to [install mise](https://mise.jdx.dev/getting-started.html#quickstart) and then run `mise install tuist` to 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. +You can check out [the documentation](https://docs.tuist.io/guides/quick-start/install-tuist) to learn more about the rationale behind our installation approach and alternative approaches. -## Bootstrap your first project 🌀 +## 🌀 Bootstrap your first project ```bash tuist init --platform ios +tuist edit # Customize your project manifest tuist generate # Generates Xcode project & workspace tuist build # Builds your project ``` -[Check out](https://docs.tuist.io/documentation/tuist) the project "Get Started" guide to learn more about Tuist and all its features. - -## Sample projects 🔬 - -You can find some sample projects in the [fixtures folder](fixtures) or the [awesome Tuist repo](https://github.com/tuist/awesome-tuist)! 🎉 - -## Want to contribute? - -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 - -The financial sustainability of the project is possible thanks to the ongoing contributions from our [GitHub Sponsors](https://github.com/sponsors/tuist) and [Open Collective Backers](https://opencollective.com/tuistapp). From them, we'd like to give a special mention to the following sponsors: - -### Gold Sponsors - -[Monday.com](https://monday.com?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&&utm_term=tuist) is a cloud-based work operating system (Work OS) that empowers teams to run projects and workflows with confidence. It's a versatile platform that combines features of project management, workflow automation, and team collaboration to streamline the way teams work together. - - - -[Lapse](https://lapse.com?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&&utm_term=tuist) is an app designed to reclaim how we take and share memories. A camera for living in the moment and a private photo journal for friends, not followers. - - - +Check out [the project "Create a new project" guide](https://docs.tuist.io/guides/start/new-project) to learn more about Tuist and all its features. -### Silver sponsors +## 📝 Documentation -[Stream](https://getstream.io/chat/sdk/ios/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_SwiftSDK&utm_term=tuist) helps build scalable in-app chat or activity feeds in days. Product teams trust Stream to launch faster, iterate more often, and ship a better user experience. +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). -[Runway](https://www.runway.team) streamlines collaboration and automation for mobile app releases, from kickoff to rollout. +### 🔬 Sample projects - +You can find some sample projects in the [fixtures folder](fixtures) or the [awesome Tuist repo](https://github.com/tuist/awesome-tuist)! 🎉 -[Emerge Tools](https://www.emergetools.com) is a suite of revolutionary products designed to supercharge mobile apps and the teams that build them - - +## 💰 Sponsors -[Codemagic](https://www.emergetools.com) is a CI/CD tool for building world-class mobile apps. +The financial sustainability of the project is possible thanks to the ongoing contributions from our [GitHub Sponsors](https://github.com/sponsors/tuist) and [Open Collective Backers](https://opencollective.com/tuistapp). From them, we'd like to give a special mention to the following sponsors: - +### 🥇 Gold Sponsors -### Bronze sponsor: [MacPaw](https://macpaw.com/) + + + + + + + + + + + +
+ + mondaycom_logo + + Monday.com is a cloud-based work operating system (Work OS) that empowers teams to run projects and workflows with confidence. It's a versatile platform that combines features of project management, workflow automation, and team collaboration to streamline the way teams work together.
+ + lapse_logo + + Lapse is an app designed to reclaim how we take and share memories. A camera for living in the moment and a private photo journal for friends, not followers.
- +### 🥈 Silver sponsors -### Bronze sponsor: [Asana](https://asana.com/) + + + + + + + + + + + + + + + + + + + +
+ + + + stream_logo + + + Stream helps build scalable in-app chat or activity feeds in days. Product teams trust Stream to launch faster, iterate more often, and ship a better user experience.
+ + runway_logo + + Runway streamlines collaboration and automation for mobile app releases, from kickoff to rollout.
+ + + + emerge_logo + + + Emerge Tools is a suite of revolutionary products designed to supercharge mobile apps and the teams that build them.
+ + codemagic_logo + + Codemagic is a CI/CD tool for building world-class mobile apps.
- +### 🥉 Bronze sponsors -## Companies using Tuist - - + + + + + + + +
+ + macpaw_logo + + + + asana_logo + +
-## Documentation 📝 +## 💪 Companies using Tuist -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/). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + tv2_logo + + + + depop_logo + + + + + + bendingspoons_logo + + + + + globekeeper_logo + + + + getyourguide_logo + +
+ + emplate_logo + + + + hh_logo + + + + trendyol_logo + + + + angrynerds_logo + + + + compass_logo + +
+ + wefox_logo + + + + hedvig_logo + + + + takeoutcentral_logo + + + + olx_logo + + + + justeattakeaway_logo + +
+ + dodopizza_logo + + + + olimpbet_logo + + + + vk_logo + + + + kinopoisk_logo + + + + qnips_logo + +
+ + telepass_logo + + + + crunchyroll_logo + + + + altel_logo + + + + altel_logo + + + + ozontech_logo + +
+ + smlab_logo + + + + izi_logo + + + + yandexTravel_logo + +
-## Supported by great companies +## 🙇‍ ️Supported by great companies -1Password, Bugsgnag Cal.com support the project by giving us access to their service through an open-source program. +Great companies support the project by giving us access to their service through an open-source program. -
- - - -
+ + + + + + + + + +
+ 1password_logo + + bugsnag_logo + + calcom_logo + + codemagic_logo +
-## Contribute 👩‍💻 +## 🧑‍💻 Want to 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/documentation/tuist/get-started-as-contributor). +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://console.algora.io/org/tuist) for some highly-valued issues. -## Core Team ✨ +## ✨ Core Team @@ -254,7 +343,7 @@ If you are interested in contributing to the project, our documentation has a se
-## Core Alumni 🚀 +## 🚀 Core Alumni The following people were once core contributors helping steer the project in the right direction and ensuring we have a reliable foundation we can build new features upon: @@ -270,7 +359,7 @@ The following people were once core contributors helping steer the project in th -## Contributors ✨ +## ✨ Contributors Thanks goes to these wonderful people: @@ -459,6 +548,41 @@ Thanks goes to these wonderful people:
한석호(MilKyo)

Hai Feng Kao
+ +
Antti Laitala
+
PushedCrayon
+
Stefano Mondino
+
Łukasz Lech
+
costapombo
+
Ihor Savynskyi
+
Eduard Miniakhmetov
+ + +
Alexander Filimonov
+
Gorbenko Roman
+
Lucas Mrowskovsky Paim
+
Taylor Lineman
+
Miguel Ferrando
+
BarredEwe
+
Chris Sessions
+ + +
Andy Kolean
+
Binlogo
+
Dmitry Serov
+
Dariusz Rybicki
+
Dan Sinclair
+
Kai Oelfke
+
Inder
+ + +
kyounh12
+
Alvar Hansen
+
Barak Weiss
+
Hilton Campbell
+
Gabriel Liévano
+
Vijay Tholpadi
+ diff --git a/SECURITY.md b/SECURITY.md index eca24451e55..a2e3cc6f43d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,7 +11,7 @@ The Tuist repo is scanned frequently for code and dependency vulnerabilities. No If you discover a potential security issue, do let us know as soon as possible. We'll work toward a resolution as quickly as possible, so please provide us with a reasonable amount of time before disclosure to the public or a third-party. -Contact us at [pedro@tuist.io](mailto:pedro@tuist.io) +Contact us at [security@tuist.io](mailto:security@tuist.io) Thank you for helping improve Tuist security! diff --git a/Sources/ProjectAutomation/Configuration.swift b/Sources/ProjectAutomation/Configuration.swift new file mode 100644 index 00000000000..2ab979ad370 --- /dev/null +++ b/Sources/ProjectAutomation/Configuration.swift @@ -0,0 +1,50 @@ +import Foundation + +// MARK: - Configuration + +// A the build Configuration of a target. + +public struct Configuration: Equatable, Codable, Sendable { + let settings: SettingsDictionary + + public init( + settings: SettingsDictionary + ) { + self.settings = settings + } +} + +// MARK: - BuildConfiguration + +public struct BuildConfiguration: Equatable, Codable, Hashable, Sendable { + public enum Variant: String, Codable, Hashable, Sendable { + case debug + case release + } + + public var name: String + public var variant: BuildConfiguration.Variant + + public init( + name: String, + variant: BuildConfiguration.Variant + ) { + self.name = name + self.variant = variant + } +} + +public typealias SettingsDictionary = [String: SettingValue] + +public enum SettingValue: Equatable, Codable, Sendable { + case string(value: String) + case array(value: [String]) + + public init(string: String) { + self = .string(value: string) + } + + public init(array: [String]) { + self = .array(value: array) + } +} diff --git a/Sources/ProjectAutomation/Graph.swift b/Sources/ProjectAutomation/Graph.swift index 4dd5500c8dc..937aaf18155 100644 --- a/Sources/ProjectAutomation/Graph.swift +++ b/Sources/ProjectAutomation/Graph.swift @@ -1,7 +1,7 @@ import Foundation /// The structure defining the output schema of the entire project graph. -public struct Graph: Codable, Equatable { +public struct Graph: Codable, Equatable, Sendable { /// The name of this graph. public let name: String diff --git a/Sources/ProjectAutomation/Package.swift b/Sources/ProjectAutomation/Package.swift index 944a616d78a..086b4e3b319 100644 --- a/Sources/ProjectAutomation/Package.swift +++ b/Sources/ProjectAutomation/Package.swift @@ -1,9 +1,9 @@ import Foundation /// The structure defining the output schema of the Swift package. -public struct Package: Codable, Equatable { +public struct Package: Codable, Equatable, Sendable { /// The type of the Swift package. - public enum PackageKind: String, Codable { + public enum PackageKind: String, Codable, Sendable { case remote case local } diff --git a/Sources/ProjectAutomation/Project.swift b/Sources/ProjectAutomation/Project.swift index 7f74428b872..3cc536f5924 100644 --- a/Sources/ProjectAutomation/Project.swift +++ b/Sources/ProjectAutomation/Project.swift @@ -1,14 +1,14 @@ import Foundation /// The structure defining the output schema of a Xcode project. -public struct Project: Codable, Equatable { +public struct Project: Codable, Equatable, Sendable { /// The name of the project. public let name: String /// The absolute path of the project. public let path: String - /// Indicates whether the project is imported through `Dependencies.swift`. + /// Indicates whether the project is imported through `Package.swift`. public let isExternal: Bool /// The Swift packages that this project depends on. diff --git a/Sources/ProjectAutomation/Scheme.swift b/Sources/ProjectAutomation/Scheme.swift index e38f4c25200..468e57976b0 100644 --- a/Sources/ProjectAutomation/Scheme.swift +++ b/Sources/ProjectAutomation/Scheme.swift @@ -1,7 +1,7 @@ import Foundation /// The structure defining the output schema of an Xcode scheme. -public struct Scheme: Codable, Equatable { +public struct Scheme: Codable, Equatable, Sendable { /// The name of the scheme. public let name: String diff --git a/Sources/ProjectAutomation/Settings.swift b/Sources/ProjectAutomation/Settings.swift new file mode 100644 index 00000000000..c8ddf55e51d --- /dev/null +++ b/Sources/ProjectAutomation/Settings.swift @@ -0,0 +1,13 @@ +import Foundation + +// A group of settings configurations. + +public struct Settings: Equatable, Codable, Sendable { + public var configurations: [ProjectAutomation.BuildConfiguration: ProjectAutomation.Configuration?] + + public init( + configurations: [ProjectAutomation.BuildConfiguration: ProjectAutomation.Configuration?] + ) { + self.configurations = configurations + } +} diff --git a/Sources/ProjectAutomation/Target.swift b/Sources/ProjectAutomation/Target.swift index ad5f2626ecf..03424ff580d 100644 --- a/Sources/ProjectAutomation/Target.swift +++ b/Sources/ProjectAutomation/Target.swift @@ -1,33 +1,43 @@ import Foundation /// The structure defining the output schema of a target. -public struct Target: Codable, Equatable { +public struct Target: Codable, Equatable, Sendable { /// The name of the target. public let name: String /// The product type the target produces. public let product: String + /// The bundleId of the target. + public let bundleId: String + /// List of file paths that are the target's sources. public let sources: [String] /// List of file paths that are the target's resources. public let resources: [String] + /// The target’s settings. + public let settings: Settings + /// The target’s dependencies. public let dependencies: [TargetDependency] public init( name: String, product: String, + bundleId: String, sources: [String], resources: [String], + settings: Settings, dependencies: [TargetDependency] ) { self.name = name self.product = product + self.bundleId = bundleId self.sources = sources self.resources = resources + self.settings = settings self.dependencies = dependencies } } diff --git a/Sources/ProjectAutomation/TargetDependency.swift b/Sources/ProjectAutomation/TargetDependency.swift index e9803936ba4..be5258eddce 100644 --- a/Sources/ProjectAutomation/TargetDependency.swift +++ b/Sources/ProjectAutomation/TargetDependency.swift @@ -1,16 +1,16 @@ import Foundation -public enum FrameworkStatus: String, Codable { +public enum FrameworkStatus: String, Codable, Sendable { case required case optional } -public enum SDKStatus: String, Codable { +public enum SDKStatus: String, Codable, Sendable { case required case optional } -public enum TargetDependency: Equatable, Hashable, Codable { +public enum TargetDependency: Equatable, Hashable, Codable, Sendable { case target(name: String) case project(target: String, path: String) case framework(path: String, status: FrameworkStatus) diff --git a/Sources/ProjectAutomation/Task.swift b/Sources/ProjectAutomation/Task.swift index 6a7659c0651..1fbc72a7437 100644 --- a/Sources/ProjectAutomation/Task.swift +++ b/Sources/ProjectAutomation/Task.swift @@ -1,16 +1,16 @@ import Foundation -public struct Task { +public struct Task: Sendable { public let options: [Option] - public let task: ([String: String]) throws -> Void + public let task: @Sendable ([String: String]) throws -> Void - public enum Option: Equatable { + public enum Option: Equatable, Sendable { case option(String) } public init( options: [Option] = [], - task: @escaping ([String: String]) throws -> Void + task: @Sendable @escaping ([String: String]) throws -> Void ) { self.options = options self.task = task @@ -19,10 +19,10 @@ public struct Task { } private func runIfNeeded() { - guard let taskCommandLineIndex = CommandLine.arguments.firstIndex(of: "--tuist-task"), + guard let taskCommandLineIndex = ProcessInfo.processInfo.arguments.firstIndex(of: "--tuist-task"), CommandLine.argc > taskCommandLineIndex else { return } - let attributesString = CommandLine.arguments[taskCommandLineIndex + 1] + let attributesString = ProcessInfo.processInfo.arguments[taskCommandLineIndex + 1] // swiftlint:disable force_try let attributes: [String: String] = try! JSONDecoder().decode( [String: String].self, diff --git a/Sources/ProjectAutomation/Tuist.swift b/Sources/ProjectAutomation/Tuist.swift index 4f8a0b5faeb..c408695f044 100644 --- a/Sources/ProjectAutomation/Tuist.swift +++ b/Sources/ProjectAutomation/Tuist.swift @@ -2,7 +2,7 @@ import Foundation /// Tuist includes all methods to interact with your tuist project public enum Tuist { - enum TuistError: Error { + enum TuistError: Error, Sendable { case signalled(command: String, code: Int32, standardError: Data) case terminated(command: String, code: Int32, standardError: Data) @@ -26,17 +26,13 @@ public enum Tuist { /// Loads and returns the graph at the given path. /// - parameter path: the path which graph should be loaded. If nil, the current path is used. - /// - parameter environmentKeys: the environment keys that should be copied. If empty, no environment variables will be - /// passed. - public static func graph(at path: String? = nil, environmentKeys: Set = []) throws -> Graph { - // If a task is executed via `tuist`, it gets passed the binary path as a last argument. - // Otherwise, fallback to go - let tuistBinaryPath = ProcessInfo.processInfo.environment["TUIST_CONFIG_BINARY_PATH"] ?? "tuist" + public static func graph(at path: String? = nil) throws -> Graph { let temporaryDirectory = try createTemporaryDirectory() do { let graphPath = temporaryDirectory.appendingPathComponent("graph.json") var arguments = [ + "tuist", "graph", "--format", "json", "--output-path", temporaryDirectory.path, @@ -44,17 +40,8 @@ public enum Tuist { if let path { arguments += ["--path", path] } - let forceConfigCacheDirectory = "TUIST_CONFIG_FORCE_CONFIG_CACHE_DIRECTORY" - var environment: [String: String] = [:] - for environmentKey in environmentKeys + [forceConfigCacheDirectory] { - if let value = ProcessInfo.processInfo.environment[environmentKey], !value.isEmpty { - environment[environmentKey] = value - } - } try run( - tuistBinaryPath, - arguments, - environment: environment + arguments ) let graphData = try Data(contentsOf: graphPath) return try JSONDecoder().decode(Graph.self, from: graphData) @@ -72,12 +59,10 @@ public enum Tuist { } private static func run( - _ launchPath: String, - _ arguments: [String], - environment _: [String: String] + _ arguments: [String] ) throws { let process = Process() - process.executableURL = URL(fileURLWithPath: launchPath) + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = arguments let outputPipe = Pipe() @@ -88,13 +73,10 @@ public enum Tuist { try process.run() process.waitUntilExit() - var command = [launchPath] - command.append(contentsOf: arguments) - if process.terminationStatus != 0 { let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() throw Tuist.TuistError.terminated( - command: command.joined(separator: ""), + command: arguments.joined(separator: " "), code: process.terminationStatus, standardError: errorData ) diff --git a/Sources/ProjectDescription/AnalyzeAction.swift b/Sources/ProjectDescription/AnalyzeAction.swift index d4f63e3520c..98789e2d860 100644 --- a/Sources/ProjectDescription/AnalyzeAction.swift +++ b/Sources/ProjectDescription/AnalyzeAction.swift @@ -3,7 +3,7 @@ import Foundation /// An action that analyzes the built products. /// /// It's initialized with the `.analyzeAction` static method -public struct AnalyzeAction: Equatable, Codable { +public struct AnalyzeAction: Equatable, Codable, Sendable { /// Indicates the build configuration the product should be analyzed with. public var configuration: ConfigurationName diff --git a/Sources/ProjectDescription/ArchiveAction.swift b/Sources/ProjectDescription/ArchiveAction.swift index 771ebaa57e1..e8e078b4373 100644 --- a/Sources/ProjectDescription/ArchiveAction.swift +++ b/Sources/ProjectDescription/ArchiveAction.swift @@ -3,7 +3,7 @@ import Foundation /// An action that archives the built products. /// /// It's initialized with the `.archiveAction` static method. -public struct ArchiveAction: Equatable, Codable { +public struct ArchiveAction: Equatable, Codable, Sendable { /// Indicates the build configuration to run the archive with. public var configuration: ConfigurationName /// If set to true, Xcode will reveal the Organizer on completion. diff --git a/Sources/ProjectDescription/Arguments.swift b/Sources/ProjectDescription/Arguments.swift index 7e5456fa048..91b60628120 100644 --- a/Sources/ProjectDescription/Arguments.swift +++ b/Sources/ProjectDescription/Arguments.swift @@ -1,31 +1,14 @@ import Foundation /// A collection of arguments and environment variables. -public struct Arguments: Equatable, Codable { +public struct Arguments: Equatable, Codable, Sendable { public var environmentVariables: [String: EnvironmentVariable] public var launchArguments: [LaunchArgument] - @available(*, deprecated, message: "please use environmentVariables instead") - public init( - environment: [String: String] = [:], - launchArguments: [LaunchArgument] = [] - ) { - environmentVariables = environment.mapValues { value in - EnvironmentVariable(value: value, isEnabled: true) - } - self.launchArguments = launchArguments - } - - public init( + public static func arguments( environmentVariables: [String: EnvironmentVariable] = [:], launchArguments: [LaunchArgument] = [] - ) { - self.environmentVariables = environmentVariables - self.launchArguments = launchArguments - } - - public init(launchArguments: [LaunchArgument]) { - environmentVariables = [:] - self.launchArguments = launchArguments + ) -> Self { + self.init(environmentVariables: environmentVariables, launchArguments: launchArguments) } } diff --git a/Sources/ProjectDescription/BuildAction.swift b/Sources/ProjectDescription/BuildAction.swift index 98149fac969..703af85c6dc 100644 --- a/Sources/ProjectDescription/BuildAction.swift +++ b/Sources/ProjectDescription/BuildAction.swift @@ -3,7 +3,7 @@ import Foundation /// An action that builds products. /// /// It's initialized with the `.buildAction` static method. -public struct BuildAction: Equatable, Codable { +public struct BuildAction: Equatable, Codable, Sendable { /// A list of targets to build, which are defined in the project. public var targets: [TargetReference] /// A list of actions that are executed before starting the build process. @@ -13,18 +13,6 @@ public struct BuildAction: Equatable, Codable { /// Whether the post actions should be run in the case of a failure public var runPostActionsOnFailure: Bool - public init( - targets: [TargetReference], - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - runPostActionsOnFailure: Bool = false - ) { - self.targets = targets - self.preActions = preActions - self.postActions = postActions - self.runPostActionsOnFailure = runPostActionsOnFailure - } - /// Returns a build action. /// - Parameters: /// - targets: A list of targets to build, which are defined in the project. diff --git a/Sources/ProjectDescription/BuildRule+CompilerSpec.swift b/Sources/ProjectDescription/BuildRule+CompilerSpec.swift index 8dec8b4c5a4..f53506fc508 100644 --- a/Sources/ProjectDescription/BuildRule+CompilerSpec.swift +++ b/Sources/ProjectDescription/BuildRule+CompilerSpec.swift @@ -4,7 +4,7 @@ extension BuildRule { /// The type of compiler spec which is used for a selected file type. /// All the values are taken from build rule options hidden under a pup-up button's menu next to a label `Using` in a target's /// `Build Rules` section. - public enum CompilerSpec: Codable { + public enum CompilerSpec: Codable, Sendable { case appIntentsMetadataExtractor case appShortcutStringsMetadataExtractor case appleClang diff --git a/Sources/ProjectDescription/BuildRule+FileType.swift b/Sources/ProjectDescription/BuildRule+FileType.swift index 6bb7b282b7e..14673f6bf9c 100644 --- a/Sources/ProjectDescription/BuildRule+FileType.swift +++ b/Sources/ProjectDescription/BuildRule+FileType.swift @@ -4,7 +4,7 @@ extension BuildRule { /// File types processed by a build rule. /// All the values are taken from build rule options hidden under a pup-up button's menu next to a label `Process` in a /// target's `Build Rules` section. - public enum FileType: Codable { + public enum FileType: Codable, Sendable { case instrumentsPackageDefinition case metalAIR case machO diff --git a/Sources/ProjectDescription/BuildRule.swift b/Sources/ProjectDescription/BuildRule.swift index 1e3c963c480..d90290b5b46 100644 --- a/Sources/ProjectDescription/BuildRule.swift +++ b/Sources/ProjectDescription/BuildRule.swift @@ -1,7 +1,7 @@ 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 { +public struct BuildRule: Codable, Equatable, Sendable { /// Compiler specification for element transformation. public var compilerSpec: CompilerSpec @@ -29,7 +29,7 @@ public struct BuildRule: Codable, Equatable { /// Build rule run once per architecture. public var runOncePerArchitecture: Bool? - public init( + public static func buildRule( name: String? = nil, fileType: FileType, filePatterns: String? = nil, @@ -39,15 +39,17 @@ public struct BuildRule: Codable, Equatable { outputFilesCompilerFlags: [String] = [], script: String? = nil, runOncePerArchitecture: Bool = false - ) { - self.name = name - self.fileType = fileType - self.filePatterns = filePatterns - self.compilerSpec = compilerSpec - self.inputFiles = inputFiles - self.outputFiles = outputFiles - self.outputFilesCompilerFlags = outputFilesCompilerFlags - self.script = script - self.runOncePerArchitecture = runOncePerArchitecture + ) -> Self { + self.init( + compilerSpec: compilerSpec, + filePatterns: filePatterns, + fileType: fileType, + name: name, + outputFiles: outputFiles, + inputFiles: inputFiles, + outputFilesCompilerFlags: outputFilesCompilerFlags, + script: script, + runOncePerArchitecture: runOncePerArchitecture + ) } } diff --git a/Sources/ProjectDescription/Cache.swift b/Sources/ProjectDescription/Cache.swift deleted file mode 100644 index e6e6bead769..00000000000 --- a/Sources/ProjectDescription/Cache.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -/// A cache configuration. -public struct Cache: Codable, Equatable { - /// A cache profile. - public struct Profile: Codable, Equatable { - /// The unique name of a profile - public var name: String - - /// The configuration to be used when building the project during a caching warmup - public var configuration: String - - /// The device to be used when building the project during a caching warmup - public var device: String? - - /// The version of the OS to be used when building the project during a caching warmup - public var os: String? - - /// Returns a `Cache.Profile` instance. - /// - /// - Parameters: - /// - name: The unique name of the cache profile - /// - configuration: The configuration to be used when building the project during a caching warmup - /// - device: The device to be used when building the project during a caching warmup - /// - os: The version of the OS to be used when building the project during a caching warmup - /// - Returns: The `Cache.Profile` instance - public static func profile( - name: String, - configuration: String, - device: String? = nil, - os: String? = nil - ) -> Profile { - Profile(name: name, configuration: configuration, device: device, os: os) - } - } - - /// A list of the cache profiles. - 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 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. - /// If no profile is provided in `tuist cache --profile` command, the first profile from the profiles list will be taken as - /// the default. - /// - Parameters: - /// - profiles: Profiles to be chosen from - /// - path: The path where the cache will be stored, if `nil` it will be a default location in a shared directory. - /// - Returns: The `Cache` instance - public static func cache(profiles: [Profile] = [], path: Path? = nil) -> Cache { - Cache(profiles: profiles, path: path) - } -} diff --git a/Sources/ProjectDescription/Cloud.swift b/Sources/ProjectDescription/Cloud.swift index 2ea2f3267a8..d85052920a4 100644 --- a/Sources/ProjectDescription/Cloud.swift +++ b/Sources/ProjectDescription/Cloud.swift @@ -1,23 +1,11 @@ import Foundation /// A cloud configuration, used for remote caching. -public struct Cloud: Codable, Equatable { +public struct Cloud: Codable, Equatable, Sendable { /// Options for cloud configuration. - public enum Option: String, Codable, Equatable { - /// Enables sending analytics to cloud dashboard. - @available( - *, - deprecated, - message: "Analytics are sent to the cloud backend by default. Use `disableAnalytics` to disable this feature." - ) - case analytics - - /// Disables sending analytics to cloud dashboard. - case disableAnalytics - - /// Marks whether cloud connection is optional. - /// If not present, tuist commands will fail regardless of whether an authentication token is available locally from - /// `tuist cloud auth` or not. + public enum Option: String, Codable, Equatable, Sendable { + /// Marks whether the Tuist server authentication is optional. + /// If present, the interaction with the Tuist server will be skipped (instead of failing) if a user is not authenticated. case optional } @@ -36,6 +24,7 @@ public struct Cloud: Codable, Equatable { /// - url: Base URL to the Cloud server. /// - options: Cloud options. /// - Returns: A Cloud instance. + @available(*, deprecated, message: "Use the `fullHandle` and `url` properties directly in the `Config`") public static func cloud(projectId: String, url: String = "https://cloud.tuist.io", options: [Option] = []) -> Cloud { Cloud(url: url, projectId: projectId, options: options) } diff --git a/Sources/ProjectDescription/CompatibleXcodeVersions.swift b/Sources/ProjectDescription/CompatibleXcodeVersions.swift index 0f70d25eed9..7e20b8d17f1 100644 --- a/Sources/ProjectDescription/CompatibleXcodeVersions.swift +++ b/Sources/ProjectDescription/CompatibleXcodeVersions.swift @@ -1,7 +1,7 @@ import Foundation /// Options of compatibles Xcode versions. -public enum CompatibleXcodeVersions: ExpressibleByArrayLiteral, ExpressibleByStringInterpolation, Codable, Equatable { +public enum CompatibleXcodeVersions: ExpressibleByArrayLiteral, ExpressibleByStringInterpolation, Codable, Equatable, Sendable { /// The project supports all Xcode versions. case all diff --git a/Sources/ProjectDescription/Config.swift b/Sources/ProjectDescription/Config.swift index e80920f3a9a..dfb1dde5008 100644 --- a/Sources/ProjectDescription/Config.swift +++ b/Sources/ProjectDescription/Config.swift @@ -24,10 +24,10 @@ /// /// let config = Config( /// compatibleXcodeVersions: ["14.2"], -/// swiftVersion: "5.7.0" +/// swiftVersion: "5.9.0" /// ) /// ``` -public struct Config: Codable, Equatable { +public struct Config: Codable, Equatable, Sendable { /// Generation options. public let generationOptions: GenerationOptions @@ -40,8 +40,11 @@ public struct Config: Codable, Equatable { /// Cloud configuration. public let cloud: Cloud? - /// Cache configuration. - public let cache: Cache? + /// The full project handle such as tuist-org/tuist. + public let fullHandle: String? + + /// The base URL that points to the Tuist server. + public let url: String /// The Swift tools versions that will be used by Tuist to fetch external dependencies. /// If `nil` is passed then Tuist will use the environment’s version. @@ -55,14 +58,14 @@ public struct Config: Codable, Equatable { /// - Parameters: /// - compatibleXcodeVersions: List of Xcode versions the project is compatible with. /// - cloud: Cloud configuration. - /// - cache: Cache configuration. /// - swiftVersion: The version of Swift that will be used by Tuist. /// - plugins: A list of plugins to extend Tuist. /// - generationOptions: List of options to use when generating the project. public init( compatibleXcodeVersions: CompatibleXcodeVersions = .all, cloud: Cloud? = nil, - cache: Cache? = nil, + fullHandle: String? = nil, + url: String = "https://cloud.tuist.io", swiftVersion: Version? = nil, plugins: [PluginLocation] = [], generationOptions: GenerationOptions = .options() @@ -71,7 +74,8 @@ public struct Config: Codable, Equatable { self.plugins = plugins self.generationOptions = generationOptions self.cloud = cloud - self.cache = cache + self.fullHandle = fullHandle + self.url = url self.swiftVersion = swiftVersion dumpIfNeeded(self) } diff --git a/Sources/ProjectDescription/ConfigGenerationOptions.swift b/Sources/ProjectDescription/ConfigGenerationOptions.swift index c27c04fc5b2..6cf71078eaa 100644 --- a/Sources/ProjectDescription/ConfigGenerationOptions.swift +++ b/Sources/ProjectDescription/ConfigGenerationOptions.swift @@ -1,11 +1,11 @@ extension Config { /// Options for project generation. - public struct GenerationOptions: Codable, Equatable { + public struct GenerationOptions: Codable, Equatable, Sendable { /** This enum represents the targets against which Tuist will run the check for potential side effects caused by static transitive dependencies. */ - public enum StaticSideEffectsWarningTargets: Codable, Equatable { + public enum StaticSideEffectsWarningTargets: Codable, Equatable, Sendable { case all case none case excluding([String]) @@ -31,19 +31,31 @@ extension Config { /// dependencies won't build until all dependencies are declared explicitly. public let enforceExplicitDependencies: Bool + /// The default configuration to be used when generating the project. + /// If not specified, Tuist generates for the first (when alphabetically sorted) debug configuration. + public var defaultConfiguration: String? + + /// Marks whether the Tuist server authentication is optional. + /// If present, the interaction with the Tuist server will be skipped (instead of failing) if a user is not authenticated. + public var optionalAuthentication: Bool + public static func options( resolveDependenciesWithSystemScm: Bool = false, disablePackageVersionLocking: Bool = false, clonedSourcePackagesDirPath: Path? = nil, staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets = .all, - enforceExplicitDependencies: Bool = false + enforceExplicitDependencies: Bool = false, + defaultConfiguration: String? = nil, + optionalAuthentication: Bool = false ) -> Self { self.init( resolveDependenciesWithSystemScm: resolveDependenciesWithSystemScm, disablePackageVersionLocking: disablePackageVersionLocking, clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, staticSideEffectsWarningTargets: staticSideEffectsWarningTargets, - enforceExplicitDependencies: enforceExplicitDependencies + enforceExplicitDependencies: enforceExplicitDependencies, + defaultConfiguration: defaultConfiguration, + optionalAuthentication: optionalAuthentication ) } } diff --git a/Sources/ProjectDescription/ConfigurationName.swift b/Sources/ProjectDescription/ConfigurationName.swift index caa3d52d0ab..b4d424bdeff 100644 --- a/Sources/ProjectDescription/ConfigurationName.swift +++ b/Sources/ProjectDescription/ConfigurationName.swift @@ -13,7 +13,7 @@ import Foundation /// } /// } /// ``` -public struct ConfigurationName: ExpressibleByStringLiteral, Codable, Equatable { +public struct ConfigurationName: ExpressibleByStringLiteral, Codable, Equatable, Sendable { /// The configuration name. public var rawValue: String diff --git a/Sources/ProjectDescription/CopyFileElement.swift b/Sources/ProjectDescription/CopyFileElement.swift new file mode 100644 index 00000000000..c802d131638 --- /dev/null +++ b/Sources/ProjectDescription/CopyFileElement.swift @@ -0,0 +1,33 @@ +import Foundation + +/// A file element from a glob pattern or a folder reference which is conditionally applied to specific platforms with an optional +/// "Code Sign On Copy" flag. +public enum CopyFileElement: Codable, Equatable, Sendable { + /// A file path (or glob pattern) to include with an optional PlatformCondition to control which platforms it applies. + /// "Code Sign on Copy" can be optionally enabled for the glob. + case glob(pattern: Path, condition: PlatformCondition? = nil, codeSignOnCopy: Bool = false) + + /// A directory path to include as a folder reference with an optional PlatformCondition to control which platforms it applies + /// to. "Code Sign on Copy" can be optionally enabled for the folder reference. + case folderReference(path: Path, condition: PlatformCondition? = nil, codeSignOnCopy: Bool = false) + + private enum TypeName: String, Codable, Sendable { + case glob + case folderReference + } + + private var typeName: TypeName { + switch self { + case .glob: + return .glob + case .folderReference: + return .folderReference + } + } +} + +extension CopyFileElement: ExpressibleByStringInterpolation { + public init(stringLiteral value: String) { + self = .glob(pattern: .path(value)) + } +} diff --git a/Sources/ProjectDescription/CopyFilesAction.swift b/Sources/ProjectDescription/CopyFilesAction.swift index f069de3edad..c96272ca887 100644 --- a/Sources/ProjectDescription/CopyFilesAction.swift +++ b/Sources/ProjectDescription/CopyFilesAction.swift @@ -5,7 +5,7 @@ import Foundation /// Copy files actions, represented as target copy files build phases, are useful to associate project files /// and products of other targets with the target and copies them to a specified destination, typically a /// subfolder within a product. This action may be used multiple times per target. -public struct CopyFilesAction: Codable, Equatable { +public struct CopyFilesAction: Codable, Equatable, Sendable { /// Name of the build phase when the project gets generated. public var name: String @@ -16,10 +16,10 @@ public struct CopyFilesAction: Codable, Equatable { public var subpath: String? /// Relative paths to the files to be copied. - public var files: [FileElement] + public var files: [CopyFileElement] /// Destination path. - public enum Destination: String, Codable, Equatable { + public enum Destination: String, Codable, Equatable, Sendable { case absolutePath case productsDirectory case wrapper @@ -37,7 +37,7 @@ public struct CopyFilesAction: Codable, Equatable { name: String, destination: Destination, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) { self.name = name self.destination = destination @@ -56,7 +56,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func productsDirectory( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -75,7 +75,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func wrapper( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -94,7 +94,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func executables( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -113,7 +113,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func resources( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -132,7 +132,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func javaResources( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -151,7 +151,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func frameworks( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -170,7 +170,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func sharedFrameworks( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -189,7 +189,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func sharedSupport( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, @@ -208,7 +208,7 @@ public struct CopyFilesAction: Codable, Equatable { public static func plugins( name: String, subpath: String? = nil, - files: [FileElement] + files: [CopyFileElement] ) -> CopyFilesAction { CopyFilesAction( name: name, diff --git a/Sources/ProjectDescription/CoreDataModel.swift b/Sources/ProjectDescription/CoreDataModel.swift index 8777e46b5c7..524d120b541 100644 --- a/Sources/ProjectDescription/CoreDataModel.swift +++ b/Sources/ProjectDescription/CoreDataModel.swift @@ -1,7 +1,7 @@ import Foundation /// A Core Data model. -public struct CoreDataModel: Codable, Equatable { +public struct CoreDataModel: Codable, Equatable, Sendable { /// Relative path to the model. public var path: Path @@ -14,11 +14,13 @@ public struct CoreDataModel: Codable, Equatable { /// - path: relative path to the Core Data model. /// - currentVersion: optional current version name (with or without the extension) /// By providing nil, it will try to read it from the .xccurrentversion file. - public init( + public static func coreDataModel( _ path: Path, currentVersion: String? = nil - ) { - self.path = path - self.currentVersion = currentVersion + ) -> Self { + self.init( + path: path, + currentVersion: currentVersion + ) } } diff --git a/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift b/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift deleted file mode 100644 index 833aa29e8d0..00000000000 --- a/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -/// A collection of Carthage dependencies. -public struct CarthageDependencies: Codable, Equatable { - /// List of dependencies that will be installed using Carthage. - public var dependencies: [Dependency] - - /// Creates `CarthageDependencies` instance. - /// - Parameter dependencies: List of dependencies that can be installed using Carthage. - public init(_ dependencies: [Dependency]) { - self.dependencies = dependencies - } -} - -// MARK: - ExpressibleByArrayLiteral - -extension CarthageDependencies: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: Dependency...) { - dependencies = elements - } -} - -// MARK: - CarthageDependencies.Dependency & CarthageDependencies.Requirement & CarthageDependencies.Options - -extension CarthageDependencies { - /// Specifies origin of Carthage dependency. - public enum Dependency: Codable, Equatable { - /// GitHub repositories (both GitHub.com and GitHub Enterprise). - case github(path: String, requirement: Requirement) - /// Other Git repositories. - case git(path: String, requirement: Requirement) - /// Dependencies that are only available as compiled binary `.framework`s. - case binary(path: String, requirement: Requirement) - } - - /// Specifies version requirement for Carthage dependency. - public enum Requirement: Codable, Equatable { - case exact(Version) - case upToNext(Version) - case atLeast(Version) - case branch(String) - case revision(String) - } -} diff --git a/Sources/ProjectDescription/Dependencies/Dependencies.swift b/Sources/ProjectDescription/Dependencies/Dependencies.swift deleted file mode 100644 index de03c5f6e2b..00000000000 --- a/Sources/ProjectDescription/Dependencies/Dependencies.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -/// A collection of external dependencies. -/// -/// Learn how to get started with `Dependencies.swift` manifest [here](https://docs.tuist.io/guides/dependencies). -/// -/// ```swift -/// import ProjectDescription -/// -/// let dependencies = Dependencies( -/// carthage: [ -/// .github(path: "Alamofire/Alamofire", requirement: .exact("5.0.4")), -/// ], -/// swiftPackageManager: [ -/// .remote(url: "https://github.com/Alamofire/Alamofire", requirement: / .upToNextMajor(from: "5.0.0")), -/// ], -/// platforms: [.iOS] -/// ) -/// ``` -public struct Dependencies: Codable, Equatable { - /// The description of dependencies that can be installed using Carthage. - public let carthage: CarthageDependencies? - - /// The description of dependencies that can be installed using Swift Package Manager. - public let swiftPackageManager: SwiftPackageManagerDependencies? - - /// List of platforms for which you want to install dependencies. - public let platforms: Set - - /// Creates a new `Dependencies` manifest instance. - /// - Parameters: - /// - carthage: The description of dependencies that can be installed using Carthage. Pass `nil` if you don't have - /// dependencies from Carthage. - /// - swiftPackageManager: The description of dependencies that can be installed using SPM. Pass `nil` if you don't have - /// dependencies from SPM. - /// - platforms: Set of platforms for which you want to install dependencies. - public init( - carthage: CarthageDependencies? = nil, - swiftPackageManager: SwiftPackageManagerDependencies? = nil, - platforms: Set = Set(PackagePlatform.allCases) - ) { - self.carthage = carthage - self.swiftPackageManager = swiftPackageManager - self.platforms = platforms - dumpIfNeeded(self) - } -} diff --git a/Sources/ProjectDescription/Dependencies/PackageSettings.swift b/Sources/ProjectDescription/Dependencies/PackageSettings.swift deleted file mode 100644 index aba504b2cbd..00000000000 --- a/Sources/ProjectDescription/Dependencies/PackageSettings.swift +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 1513576fa3f..00000000000 --- a/Sources/ProjectDescription/Dependencies/SwiftPackageManagerDependencies.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation - -public enum PackagesOrManifest: Codable, Equatable { - case packages([Package]) - case manifest -} - -/// A collection of Swift Package Manager dependencies. -/// -/// For example, to enabled resource accessors on projects generated from Swift Package Manager: -/// -/// ```swift -/// let packageManager = SwiftPackageManagerDependencies( -/// "Package.swift", -/// projectOptions: ["MySwiftPackage": .options(disableSynthesizedResourceAccessors: false)] -/// ) -/// ``` -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 var packagesOrManifest: PackagesOrManifest - - /// 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] - - /// Creates `SwiftPackageManagerDependencies` instance. - /// - Parameter packages: List of packages that will be installed using Swift Package Manager. - /// - 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. - @available(*, deprecated, message: "Use init without packages parameter instead") - public init( - _ packages: [Package], - productTypes: [String: Product] = [:], - baseSettings: Settings = .settings(), - targetSettings: [String: SettingsDictionary] = [:], - projectOptions: [String: Project.Options] = [:] - ) { - packagesOrManifest = .packages(packages) - self.productTypes = productTypes - self.baseSettings = baseSettings - self.targetSettings = targetSettings - self.projectOptions = projectOptions - } - - /// Creates `SwiftPackageManagerDependencies` instance using the package manifest at `Tuist/Package.swift`. - /// - 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. - public init( - productTypes: [String: Product] = [:], - baseSettings: Settings = .settings(), - targetSettings: [String: SettingsDictionary] = [:], - projectOptions: [String: Project.Options] = [:] - ) { - packagesOrManifest = .manifest - self.productTypes = productTypes - self.baseSettings = baseSettings - self.targetSettings = targetSettings - self.projectOptions = projectOptions - } -} diff --git a/Sources/ProjectDescription/DeploymentDevice.swift b/Sources/ProjectDescription/DeploymentDevice.swift deleted file mode 100644 index d382d44f0f5..00000000000 --- a/Sources/ProjectDescription/DeploymentDevice.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -// MARK: - DeploymentDevice - -/// A supported deployment device. -@available(*, deprecated, message: "Use `Destinations` to define supported devices") -public struct DeploymentDevice: OptionSet, Codable, Hashable { - /// An iPhone device. - public static let iphone = DeploymentDevice(rawValue: 1 << 0) - /// An iPad device. - public static let ipad = DeploymentDevice(rawValue: 1 << 1) - /// A Mac device. - public static let mac = DeploymentDevice(rawValue: 1 << 2) - /// An Apple Vision device. - public static let vision = DeploymentDevice(rawValue: iphone.rawValue | ipad.rawValue | mac.rawValue) - - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } -} diff --git a/Sources/ProjectDescription/DeploymentTarget.swift b/Sources/ProjectDescription/DeploymentTarget.swift deleted file mode 100644 index 989ccb3340c..00000000000 --- a/Sources/ProjectDescription/DeploymentTarget.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation - -// MARK: - DeploymentTarget - -/// A supported minimum deployment target. -@available( - *, - deprecated, - message: "Use `DeploymentTargets` to specify minimum OS Versions, and `Destinations` to define supported OSes" -) -public enum DeploymentTarget: Codable, Hashable { - /// The minimum iOS version, the list of devices your product will support, and whether or not the target should run on mac - /// devices. - case iOS(targetVersion: String, devices: DeploymentDevice, supportsMacDesignedForIOS: Bool = true) - /// The minimum macOS version your product will support. - case macOS(targetVersion: String) - /// The minimum watchOS version your product will support. - case watchOS(targetVersion: String) - /// The minimum tvOS version your product will support. - case tvOS(targetVersion: String) - /// The minimum visionOS version your product will support. - case visionOS(targetVersion: String) - - private enum Kind: String, Codable { - case iOS - case macOS - case watchOS - case tvOS - case visionOS - } - - /// The target platform version - public var targetVersion: String { - switch self { - case let .iOS(targetVersion, _, _), let .macOS(targetVersion), let .watchOS(targetVersion), - let .tvOS(targetVersion), let .visionOS(targetVersion): - return targetVersion - } - } -} diff --git a/Sources/ProjectDescription/DeploymentTargets.swift b/Sources/ProjectDescription/DeploymentTargets.swift index f9186e86940..cec4fb86d84 100644 --- a/Sources/ProjectDescription/DeploymentTargets.swift +++ b/Sources/ProjectDescription/DeploymentTargets.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - DeploymentTargets /// A struct representing the minimum deployment versions for each platform. -public struct DeploymentTargets: Hashable, Codable { +public struct DeploymentTargets: Hashable, Codable, Sendable { /// Minimum deployment version for iOS public var iOS: String? /// Minimum deployment version for macOS @@ -15,12 +15,21 @@ public struct DeploymentTargets: Hashable, Codable { /// Minimum deployment version for visionOS public var visionOS: String? - public init(iOS: String? = nil, macOS: String? = nil, watchOS: String? = nil, tvOS: String? = nil, visionOS: String? = nil) { - self.iOS = iOS - self.macOS = macOS - self.watchOS = watchOS - self.tvOS = tvOS - self.visionOS = visionOS + /// Multiplatform deployment target + public static func multiplatform( + iOS: String? = nil, + macOS: String? = nil, + watchOS: String? = nil, + tvOS: String? = nil, + visionOS: String? = nil + ) -> Self { + self.init( + iOS: iOS, + macOS: macOS, + watchOS: watchOS, + tvOS: tvOS, + visionOS: visionOS + ) } /// Convenience accessor to retreive a minimum version given a `Platform` diff --git a/Sources/ProjectDescription/Destination.swift b/Sources/ProjectDescription/Destination.swift index b9f10ee2d61..b4b3986e9eb 100644 --- a/Sources/ProjectDescription/Destination.swift +++ b/Sources/ProjectDescription/Destination.swift @@ -1,20 +1,20 @@ import Foundation -/// Set of deployment destinstions +/// Set of deployment destinations public typealias Destinations = Set -/// Convenience collections of desitions mapped to platforms terminology. +/// Convenience collections of destinations mapped to platforms terminology. extension Destinations { - public static var watchOS: Destinations = [.appleWatch] + public static let watchOS: Destinations = [.appleWatch] /// Currently we omit `.visionOSwithiPadDesign` from our default because `visionOS` is unreleased. - public static var iOS: Destinations = [.iPhone, .iPad, .macWithiPadDesign] - public static var macOS: Destinations = [.mac] - public static var tvOS: Destinations = [.appleTv] - public static var visionOS: Destinations = [.appleVision] + public static let iOS: Destinations = [.iPhone, .iPad, .macWithiPadDesign] + public static let macOS: Destinations = [.mac] + public static let tvOS: Destinations = [.appleTv] + public static let visionOS: Destinations = [.appleVision] } extension Destinations { - /// Convience set of platforms that are supported by a set of destinations + /// Convenience set of platforms that are supported by a set of destinations public var platforms: Set { let platforms = map(\.platform) return Set(platforms) @@ -22,7 +22,7 @@ extension Destinations { } /// A supported deployment destination representation. -public enum Destination: String, Codable, Equatable, CaseIterable { +public enum Destination: String, Codable, Equatable, CaseIterable, Sendable { /// iPhone support case iPhone /// iPad support @@ -39,7 +39,7 @@ public enum Destination: String, Codable, Equatable, CaseIterable { case appleTv /// visionOS support case appleVision - /// visionOS support useing iPad design + /// visionOS support using iPad design case appleVisionWithiPadDesign /// SDK Platform of a destination diff --git a/Sources/ProjectDescription/Dump.swift b/Sources/ProjectDescription/Dump.swift index 24501a53196..b3cd2fe8bb4 100644 --- a/Sources/ProjectDescription/Dump.swift +++ b/Sources/ProjectDescription/Dump.swift @@ -1,8 +1,8 @@ import Foundation func dumpIfNeeded(_ entity: some Encodable) { - guard CommandLine.argc > 0, - CommandLine.arguments.contains("--tuist-dump") + guard !ProcessInfo.processInfo.arguments.isEmpty, + ProcessInfo.processInfo.arguments.contains("--tuist-dump") else { return } let encoder = JSONEncoder() // swiftlint:disable:next force_try diff --git a/Sources/ProjectDescription/Entitlements.swift b/Sources/ProjectDescription/Entitlements.swift index 23a3474bbd5..09657d3ee7e 100644 --- a/Sources/ProjectDescription/Entitlements.swift +++ b/Sources/ProjectDescription/Entitlements.swift @@ -2,13 +2,35 @@ import Foundation // MARK: - Entitlements -public enum Entitlements: Codable, Equatable { +public enum Entitlements: Codable, Equatable, Sendable { /// The path to an existing .entitlements file. case file(path: Path) /// A dictionary with the entitlements content. Tuist generates the .entitlements file at the generation time. case dictionary([String: Plist.Value]) + /** + A user defined xcconfig variable map to .entitlements file. + + This should be used when the project has different entitlements files per config (aka: debug,release,staging,etc) + + ```` + .target( + ... + entitlements: .variable("$(ENTITLEMENT_FILE_VARIABLE)"), + ) + ```` + + Or as literal string + ```` + .target( + ... + entitlements: $(ENTITLEMENT_FILE_VARIABLE), + ) + ```` + */ + case variable(String) + // MARK: - Error public enum CodingError: Error { @@ -31,6 +53,10 @@ public enum Entitlements: Codable, Equatable { extension Entitlements: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self = .file(path: Path(value)) + if value.hasPrefix("$(") { + self = .variable(value) + } else { + self = .file(path: .path(value)) + } } } diff --git a/Sources/ProjectDescription/EnvironmentVariable.swift b/Sources/ProjectDescription/EnvironmentVariable.swift index 57454bf81d2..934148e609c 100644 --- a/Sources/ProjectDescription/EnvironmentVariable.swift +++ b/Sources/ProjectDescription/EnvironmentVariable.swift @@ -1,7 +1,7 @@ import Foundation /// It represents an environment variable that is passed when running a scheme's action -public struct EnvironmentVariable: Equatable, Codable, Hashable, ExpressibleByStringLiteral { +public struct EnvironmentVariable: Equatable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { // MARK: - Attributes /// The value of the environment variable @@ -11,11 +11,15 @@ public struct EnvironmentVariable: Equatable, Codable, Hashable, ExpressibleBySt // MARK: - Init - public init(value: String, isEnabled: Bool) { + init(value: String, isEnabled: Bool) { self.value = value self.isEnabled = isEnabled } + public static func environmentVariable(value: String, isEnabled: Bool) -> Self { + self.init(value: value, isEnabled: isEnabled) + } + public init(stringLiteral value: String) { self.value = value isEnabled = true diff --git a/Sources/ProjectDescription/ExecuteAction.swift b/Sources/ProjectDescription/ExecuteAction.swift index 777fface352..8cc29931336 100644 --- a/Sources/ProjectDescription/ExecuteAction.swift +++ b/Sources/ProjectDescription/ExecuteAction.swift @@ -1,7 +1,7 @@ import Foundation /// An action that can be executed as part of another action for pre or post execution. -public struct ExecutionAction: Equatable, Codable { +public struct ExecutionAction: Equatable, Codable, Sendable { public var title: String public var scriptText: String public var target: TargetReference? @@ -9,10 +9,17 @@ public struct ExecutionAction: Equatable, Codable { /// The path to the shell which shall execute this script. if it is nil, Xcode will use default value. public var shellPath: String? - public init(title: String = "Run Script", scriptText: String, target: TargetReference? = nil, shellPath: String? = nil) { - self.title = title - self.scriptText = scriptText - self.target = target - self.shellPath = shellPath + public static func executionAction( + title: String = "Run Script", + scriptText: String, + target: TargetReference? = nil, + shellPath: String? = nil + ) -> Self { + self.init( + title: title, + scriptText: scriptText, + target: target, + shellPath: shellPath + ) } } diff --git a/Sources/ProjectDescription/FileCodeGen.swift b/Sources/ProjectDescription/FileCodeGen.swift index c6814c01e70..1d3429e8eeb 100644 --- a/Sources/ProjectDescription/FileCodeGen.swift +++ b/Sources/ProjectDescription/FileCodeGen.swift @@ -1,7 +1,7 @@ import Foundation /// Options for source file code generation. -public enum FileCodeGen: String, Codable, Equatable { +public enum FileCodeGen: String, Codable, Equatable, Sendable { /// Public codegen case `public` /// Private codegen diff --git a/Sources/ProjectDescription/FileElement.swift b/Sources/ProjectDescription/FileElement.swift index 043905e2726..971a30a33cf 100644 --- a/Sources/ProjectDescription/FileElement.swift +++ b/Sources/ProjectDescription/FileElement.swift @@ -7,7 +7,7 @@ import Foundation /// /// Note: For convenience, an element can be represented as a string literal /// `"some/pattern/**"` is the equivalent of `FileElement.glob(pattern: "some/pattern/**")` -public enum FileElement: Codable, Equatable { +public enum FileElement: Codable, Equatable, Sendable { /// A file path (or glob pattern) to include. For convenience, a string literal can be used as an alternate way to specify /// this option. case glob(pattern: Path) @@ -32,7 +32,7 @@ public enum FileElement: Codable, Equatable { extension FileElement: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self = .glob(pattern: Path(value)) + self = .glob(pattern: .path(value)) } } @@ -48,6 +48,6 @@ extension [FileElement]: ExpressibleByStringLiteral { public typealias StringLiteralType = String public init(stringLiteral value: String) { - self = [.glob(pattern: Path(value))] + self = [.glob(pattern: .path(value))] } } diff --git a/Sources/ProjectDescription/FileHeaderTemplate.swift b/Sources/ProjectDescription/FileHeaderTemplate.swift index aba7798b05c..e289db76055 100644 --- a/Sources/ProjectDescription/FileHeaderTemplate.swift +++ b/Sources/ProjectDescription/FileHeaderTemplate.swift @@ -10,7 +10,7 @@ import Foundation /// - if your template doesn't start with comment and whitespace or newline, we add a space - otherwise your header would be /// glued to implicit comment slashes which you probably do not want /// - if your template has trailing newline, we remove it as it is implicitly added by Xcode -public enum FileHeaderTemplate: Codable, Equatable, ExpressibleByStringInterpolation { +public enum FileHeaderTemplate: Codable, Equatable, ExpressibleByStringInterpolation, Sendable { /// Load template stored in file case file(Path) /// Use inline string as template diff --git a/Sources/ProjectDescription/FileList.swift b/Sources/ProjectDescription/FileList.swift index 4462698cbcf..13159e023fc 100644 --- a/Sources/ProjectDescription/FileList.swift +++ b/Sources/ProjectDescription/FileList.swift @@ -4,7 +4,7 @@ import Foundation /// /// The list of files can be initialized with a string that represents the glob pattern, or an array of strings, which represents /// a list of glob patterns. -public struct FileList: Codable, Equatable { +public struct FileList: Codable, Equatable, Sendable { /// Glob pattern to the files. public let globs: [FileListGlob] diff --git a/Sources/ProjectDescription/FileListGlob.swift b/Sources/ProjectDescription/FileListGlob.swift index 4bcc88f18fc..4db2ed6f910 100644 --- a/Sources/ProjectDescription/FileListGlob.swift +++ b/Sources/ProjectDescription/FileListGlob.swift @@ -1,7 +1,7 @@ import Foundation /// A glob pattern that refers to files. -public struct FileListGlob: Codable, Equatable { +public struct FileListGlob: Codable, Equatable, Sendable { /// The path with a glob pattern. public var glob: Path @@ -33,6 +33,6 @@ public struct FileListGlob: Codable, Equatable { extension FileListGlob: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self.init(glob: Path(value), excluding: []) + self.init(glob: .path(value), excluding: []) } } diff --git a/Sources/ProjectDescription/Headers.swift b/Sources/ProjectDescription/Headers.swift index 2b240d2543f..d97a7838b4b 100644 --- a/Sources/ProjectDescription/Headers.swift +++ b/Sources/ProjectDescription/Headers.swift @@ -1,9 +1,9 @@ import Foundation /// A group of public, private and project headers. -public struct Headers: Codable, Equatable { +public struct Headers: Codable, Equatable, Sendable { /// Determine how to resolve cases, when the same files found in different header scopes - public enum AutomaticExclusionRule: Int, Codable { + public enum AutomaticExclusionRule: Int, Codable, Sendable { /// Project headers = all found - private headers - public headers /// /// Order of tuist search: diff --git a/Sources/ProjectDescription/InfoPlist.swift b/Sources/ProjectDescription/InfoPlist.swift index 0610e0cc6f5..0aecc6d8eba 100644 --- a/Sources/ProjectDescription/InfoPlist.swift +++ b/Sources/ProjectDescription/InfoPlist.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - InfoPlist /// A info plist from a file, a custom dictonary or a extended defaults. -public enum InfoPlist: Codable, Equatable { +public enum InfoPlist: Codable, Equatable, Sendable { /// The path to an existing Info.plist file. case file(path: Path) @@ -41,13 +41,6 @@ public enum InfoPlist: Codable, Equatable { extension InfoPlist: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self = .file(path: Path(value)) + self = .file(path: .path(value)) } } - -// MARK: - InfoPlist API compatibility - -extension InfoPlist { - @available(*, deprecated, message: "InfoPlist.Value was renamed to Plist.Value") - public typealias Value = Plist.Value -} diff --git a/Sources/ProjectDescription/LaunchArgument.swift b/Sources/ProjectDescription/LaunchArgument.swift index efda1fff436..9bd2954f8d1 100644 --- a/Sources/ProjectDescription/LaunchArgument.swift +++ b/Sources/ProjectDescription/LaunchArgument.swift @@ -1,7 +1,7 @@ import Foundation /// A launch argument, passed when running a scheme. -public struct LaunchArgument: Equatable, Codable { +public struct LaunchArgument: Equatable, Codable, Sendable { // MARK: - Attributes /// Name of argument @@ -15,9 +15,8 @@ public struct LaunchArgument: Equatable, Codable { /// - Parameters: /// - name: Name of argument /// - isEnabled: If enabled then argument is marked as active - public init(name: String, isEnabled: Bool) { - self.name = name - self.isEnabled = isEnabled + public static func launchArgument(name: String, isEnabled: Bool) -> Self { + self.init(name: name, isEnabled: isEnabled) } } diff --git a/Sources/ProjectDescription/LaunchStyle.swift b/Sources/ProjectDescription/LaunchStyle.swift index 93e06b7c9de..ed05491891c 100644 --- a/Sources/ProjectDescription/LaunchStyle.swift +++ b/Sources/ProjectDescription/LaunchStyle.swift @@ -1,6 +1,6 @@ import Foundation -public enum LaunchStyle: Codable { +public enum LaunchStyle: Codable, Sendable { case automatically case waitForExecutableToBeLaunched } diff --git a/Sources/ProjectDescription/MergedBinaryType.swift b/Sources/ProjectDescription/MergedBinaryType.swift index 2de57535084..9d3c0cc59bd 100644 --- a/Sources/ProjectDescription/MergedBinaryType.swift +++ b/Sources/ProjectDescription/MergedBinaryType.swift @@ -1,7 +1,7 @@ /// Represents the different options to configure a target for mergeable libraries /// /// https://developer.apple.com/documentation/xcode/configuring-your-project-to-use-mergeable-libraries -public enum MergedBinaryType: Equatable, Codable { +public enum MergedBinaryType: Equatable, Codable, Sendable { /// Target is never going to merge available dependencies case disabled diff --git a/Sources/ProjectDescription/OnDemandResourcesTags.swift b/Sources/ProjectDescription/OnDemandResourcesTags.swift new file mode 100644 index 00000000000..8fc26f1fbcf --- /dev/null +++ b/Sources/ProjectDescription/OnDemandResourcesTags.swift @@ -0,0 +1,15 @@ +/// On-demand resources tags associated with Initial Install and Prefetched Order categories +public struct OnDemandResourcesTags: Codable, Equatable, Sendable { + /// Initial install tags associated with on demand resources + public let initialInstall: [String]? + /// Prefetched tag order associated with on demand resources + public let prefetchOrder: [String]? + + /// Returns OnDemandResourcesTags. + /// - Parameter initialInstall: An array of strings that lists the tags assosiated with the Initial install tags category. + /// - Parameter prefetchOrder: An array of strings that lists the tags associated with the Prefetch tag order category. + /// - Returns: OnDemandResourcesTags. + public static func tags(initialInstall: [String]?, prefetchOrder: [String]?) -> Self { + OnDemandResourcesTags(initialInstall: initialInstall, prefetchOrder: prefetchOrder) + } +} diff --git a/Sources/ProjectDescription/Package.swift b/Sources/ProjectDescription/Package.swift index 5280868f20e..28fc9ada24b 100644 --- a/Sources/ProjectDescription/Package.swift +++ b/Sources/ProjectDescription/Package.swift @@ -4,7 +4,7 @@ /// - remote: A Git URL to the source of the package, /// and a requirement for the version of the package. /// - local: A relative path to the package. -public enum Package: Equatable, Codable { +public enum Package: Equatable, Codable, Sendable { case remote(url: String, requirement: Requirement) case local(path: Path) @@ -15,7 +15,7 @@ public enum Package: Equatable, Codable { } extension Package { - public enum Requirement: Codable, Equatable { + public enum Requirement: Codable, Equatable, Sendable { case upToNextMajor(from: Version) case upToNextMinor(from: Version) case range(from: Version, to: Version) diff --git a/Sources/ProjectDescription/PackageSettings.swift b/Sources/ProjectDescription/PackageSettings.swift new file mode 100644 index 00000000000..e9005b8ac71 --- /dev/null +++ b/Sources/ProjectDescription/PackageSettings.swift @@ -0,0 +1,71 @@ +import Foundation + +/// A custom Swift Package Manager configuration +/// +/// +/// ```swift +/// // swift-tools-version: 5.9 +/// import PackageDescription +/// +/// #if TUIST +/// import ProjectDescription +/// import ProjectDescriptionHelpers +/// +/// let packageSettings = PackageSettings( +/// productTypes: [ +/// "Alamofire": .framework, // default is .staticFramework +/// ] +/// ) +/// #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] + + /// Custom product destinations where key of the dictionary is the name of the SPM product and the value contains the + /// supported destinations. + /// **Note**: This setting should only be used when using Tuist for SPM package projects, _not_ for your external + /// dependencies. + /// SPM implicitly always supports all platforms, but some commands like `tuist cache` depend on destinations being explicit. + /// If a product does not support all destinations, you can use `productDestinations` to make the supported destinations + /// explicit. + public var productDestinations: [String: Destinations] + + // 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] + + /// Creates `PackageSettings` instance for custom Swift Package Manager configuration. + /// - Parameters: + /// - productTypes: The custom `Product` types to be used for SPM targets. + /// - productDestinations: Custom destinations to be used for SPM products. + /// - baseSettings: Additional settings to be added to targets generated from SwiftPackageManager. + /// - targetSettings: Additional settings to be added to targets generated from SwiftPackageManager. + /// - projectOptions: Custom project configurations to be used for projects generated from SwiftPackageManager. + public init( + productTypes: [String: Product] = [:], + productDestinations: [String: Destinations] = [:], + baseSettings: Settings = .settings(), + targetSettings: [String: SettingsDictionary] = [:], + projectOptions: [String: Project.Options] = [:] + ) { + self.productTypes = productTypes + self.productDestinations = productDestinations + self.baseSettings = baseSettings + self.targetSettings = targetSettings + self.projectOptions = projectOptions + dumpIfNeeded(self) + } +} diff --git a/Sources/ProjectDescription/Path.swift b/Sources/ProjectDescription/Path.swift index f9e76787774..6a58ff471f1 100644 --- a/Sources/ProjectDescription/Path.swift +++ b/Sources/ProjectDescription/Path.swift @@ -4,8 +4,8 @@ import Foundation /// /// Paths can be relative and absolute. We discourage using absolute paths because they create a dependency with the environment /// where they are defined. -public struct Path: ExpressibleByStringInterpolation, Codable, Hashable { - public enum PathType: String, Codable { +public struct Path: ExpressibleByStringInterpolation, Codable, Hashable, Sendable { + public enum PathType: String, Codable, Sendable { case relativeToCurrentFile case relativeToManifest case relativeToRoot @@ -16,7 +16,7 @@ public struct Path: ExpressibleByStringInterpolation, Codable, Hashable { public var callerPath: String? /// Default PathType is `.relativeToManifest` - public init(_ path: String) { + public static func path(_ path: String) -> Self { self.init(path, type: .relativeToManifest) } diff --git a/Sources/ProjectDescription/Platform.swift b/Sources/ProjectDescription/Platform.swift index 77c35e85847..fc4f34cac7b 100644 --- a/Sources/ProjectDescription/Platform.swift +++ b/Sources/ProjectDescription/Platform.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - Platform /// A supported platform representation. -public enum Platform: String, Codable, Equatable, CaseIterable { +public enum Platform: String, Codable, Equatable, CaseIterable, Sendable { /// The iOS platform case iOS = "ios" /// The macOS platform @@ -17,7 +17,7 @@ public enum Platform: String, Codable, Equatable, CaseIterable { } /// A supported Swift Package Manager platform representation. -public enum PackagePlatform: String, Codable, Equatable, CaseIterable { +public enum PackagePlatform: String, Codable, Equatable, CaseIterable, Sendable { /// The iOS platform case iOS = "ios" /// The macOS platform diff --git a/Sources/ProjectDescription/PlatformCondition.swift b/Sources/ProjectDescription/PlatformCondition.swift index 1191edd8145..5f5daa4824b 100644 --- a/Sources/ProjectDescription/PlatformCondition.swift +++ b/Sources/ProjectDescription/PlatformCondition.swift @@ -1,7 +1,7 @@ import Foundation /// A condition applied to an "entity" allowing it to only be used in certain circumstances -public struct PlatformCondition: Codable, Hashable, Equatable { +public struct PlatformCondition: Codable, Hashable, Equatable, Sendable { public let platformFilters: Set /// For internal use only. use `.when` to ensure we can not have a `PlatformCondition` with an empty set of filters. private init(platformFilters: Set) { diff --git a/Sources/ProjectDescription/PlatformFilter.swift b/Sources/ProjectDescription/PlatformFilter.swift index 01eb8a3a9ba..f2685820c42 100644 --- a/Sources/ProjectDescription/PlatformFilter.swift +++ b/Sources/ProjectDescription/PlatformFilter.swift @@ -4,7 +4,7 @@ extension PlatformFilters { public static let all = Set(PlatformFilter.allCases) } -public enum PlatformFilter: Comparable, Hashable, Codable, CaseIterable { +public enum PlatformFilter: Comparable, Hashable, Codable, CaseIterable, Sendable { case ios case macos case tvos diff --git a/Sources/ProjectDescription/Plist.swift b/Sources/ProjectDescription/Plist.swift index 65dec47d3de..00ed8a4b84f 100644 --- a/Sources/ProjectDescription/Plist.swift +++ b/Sources/ProjectDescription/Plist.swift @@ -5,7 +5,7 @@ import Foundation public enum Plist { /// It represents the values of the .plist or .entitlements file dictionary. /// It ensures that the values used to define the content of the dynamically generated .plist or .entitlements files are valid - public indirect enum Value: Codable, Equatable { + public indirect enum Value: Codable, Equatable, Sendable { /// It represents a string value. case string(String) /// It represents an integer value. diff --git a/Sources/ProjectDescription/PluginLocation.swift b/Sources/ProjectDescription/PluginLocation.swift index a4f5d57fd97..ae726229177 100644 --- a/Sources/ProjectDescription/PluginLocation.swift +++ b/Sources/ProjectDescription/PluginLocation.swift @@ -1,7 +1,7 @@ import Foundation /// A location to a plugin, either local or remote. -public struct PluginLocation: Codable, Equatable { +public struct PluginLocation: Codable, Equatable, Sendable { /// The type of location `local` or `git`. public var type: LocationType @@ -43,7 +43,7 @@ public struct PluginLocation: Codable, Equatable { // MARK: - Codable extension PluginLocation { - public enum LocationType: Codable, Equatable { + public enum LocationType: Codable, Equatable, Sendable { case local(path: Path) case gitWithTag(url: String, tag: String, directory: String?, releaseUrl: String?) case gitWithSha(url: String, sha: String, directory: String?) diff --git a/Sources/ProjectDescription/PrivacyManifest.swift b/Sources/ProjectDescription/PrivacyManifest.swift new file mode 100644 index 00000000000..8f1f0cf349a --- /dev/null +++ b/Sources/ProjectDescription/PrivacyManifest.swift @@ -0,0 +1,48 @@ +import Foundation + +/// Describe the data your app or third-party SDK collects and the reasons required APIs it uses. +public struct PrivacyManifest: Codable, Equatable, Sendable { + /// A Boolean that indicates whether your app or third-party SDK uses data for tracking as defined under the App + /// Tracking Transparency framework. For more information, see [User Privacy and Data + /// Use](https://developer.apple.com/app-store/user-privacy-and-data-use/). + public var tracking: Bool + + /// An array of strings that lists the internet domains your app or third-party SDK connects to that + /// engage in tracking. If the user has not granted tracking permission through the App Tracking Transparency framework, + /// network requests to these domains fail and your app receives an error. If you set `tracking` to true then you need to + /// provide at least one internet domain in NSPrivacyTrackingDomains; otherwise, you can provide zero or more domains. + public var trackingDomains: [String] + + /// An array of dictionaries that describes the data types your app or third-party SDK collects. For + /// information on the keys and values to use in the dictionaries, see [Describing data use in privacy manifests](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests). + public var collectedDataTypes: [[String: Plist.Value]] + + /// An array of dictionaries that describe the API types your app or third-party SDK accesses that have + /// been designated as APIs that require reasons to access. For information on the keys and values to use in the dictionaries, + /// see [Describing use of required reason API](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api). + public var accessedApiTypes: [[String: Plist.Value]] + + /// Returns a PrivacyManifest. + /// - Parameter tracking: A Boolean that indicates whether your app or third-party SDK uses data for tracking. + /// - Parameter trackingDomains: An array of strings that lists the internet domains your app or third-party SDK connects to + /// that engage in tracking. + /// - Parameter collectedDataTypes: An array of dictionaries that describes the data types your app or third-party SDK + /// collects. + /// - Parameter accessedApiTypes: An array of dictionaries that describe the API types your app or third-party SDK accesses + /// that have + /// been designated as APIs that require reasons to access. + /// - Returns: PrivacyManifest. + public static func privacyManifest( + tracking: Bool, + trackingDomains: [String], + collectedDataTypes: [[String: Plist.Value]], + accessedApiTypes: [[String: Plist.Value]] + ) -> Self { + PrivacyManifest( + tracking: tracking, + trackingDomains: trackingDomains, + collectedDataTypes: collectedDataTypes, + accessedApiTypes: accessedApiTypes + ) + } +} diff --git a/Sources/ProjectDescription/Product.swift b/Sources/ProjectDescription/Product.swift index 3ac40028986..b473cb0f78f 100644 --- a/Sources/ProjectDescription/Product.swift +++ b/Sources/ProjectDescription/Product.swift @@ -1,7 +1,7 @@ import Foundation /// Possible products types. -public enum Product: String, Codable, Equatable { +public enum Product: String, Codable, Equatable, Sendable { /// An application. case app /// A static library. diff --git a/Sources/ProjectDescription/ProfileAction.swift b/Sources/ProjectDescription/ProfileAction.swift index 71bb10f93cc..cb9c27ab1d4 100644 --- a/Sources/ProjectDescription/ProfileAction.swift +++ b/Sources/ProjectDescription/ProfileAction.swift @@ -3,7 +3,7 @@ import Foundation /// An action that profiles the built products. /// /// It's initialized with the `.profileAction` static method -public struct ProfileAction: Equatable, Codable { +public struct ProfileAction: Equatable, Codable, Sendable { /// Indicates the build configuration the product should be profiled with. public var configuration: ConfigurationName diff --git a/Sources/ProjectDescription/Project.swift b/Sources/ProjectDescription/Project.swift index e58fa4a35ec..5489ba6ce4b 100644 --- a/Sources/ProjectDescription/Project.swift +++ b/Sources/ProjectDescription/Project.swift @@ -17,7 +17,7 @@ import Foundation /// targets: [ /// Target( /// name: "App", -/// platform: .iOS, +/// destinations: .iOS, /// product: .app, /// bundleId: "io.tuist.App", /// infoPlist: "Config/App-Info.plist", @@ -60,11 +60,13 @@ import Foundation /// ] /// ) /// ``` -public struct Project: Codable, Equatable { +public struct Project: Codable, Equatable, Sendable { /// The name of the project. Also, the file name of the generated Xcode project. public let name: String /// The name of the organization used by Xcode as copyright. public let organizationName: String? + /// The prefix for class files Xcode generates when you create a project or class file. + public let classPrefix: String? /// The project options. public let options: Options /// The Swift Packages used by the project. @@ -85,6 +87,7 @@ public struct Project: Codable, Equatable { public init( name: String, organizationName: String? = nil, + classPrefix: String? = nil, options: Options = .options(), packages: [Package] = [], settings: Settings? = nil, @@ -96,6 +99,7 @@ public struct Project: Codable, Equatable { ) { self.name = name self.organizationName = organizationName + self.classPrefix = classPrefix self.options = options self.packages = packages self.targets = targets @@ -106,4 +110,17 @@ public struct Project: Codable, Equatable { self.resourceSynthesizers = resourceSynthesizers dumpIfNeeded(self) } + + /// The project contains targets that depend on external dependencies + public var containsExternalDependencies: Bool { + targets.contains(where: { target in + target.dependencies.contains(where: { + if case .external = $0 { + return true + } else { + return false + } + }) + }) + } } diff --git a/Sources/ProjectDescription/ProjectOptions.swift b/Sources/ProjectDescription/ProjectOptions.swift index 8f32dc0a521..dd0660c1b8f 100644 --- a/Sources/ProjectDescription/ProjectOptions.swift +++ b/Sources/ProjectDescription/ProjectOptions.swift @@ -2,7 +2,7 @@ import Foundation extension Project { /// Options to configure a project. - public struct Options: Codable, Equatable { + public struct Options: Codable, Equatable, Sendable { /// Configures automatic target schemes generation. public var automaticSchemesOptions: AutomaticSchemesOptions @@ -55,9 +55,9 @@ extension Project { extension Project.Options { /// Automatic schemes options allow customizing the generation of the target schemes. - public enum AutomaticSchemesOptions: Codable, Equatable { + public enum AutomaticSchemesOptions: Codable, Equatable, Sendable { /// Allows you to define what targets will be enabled for code coverage data gathering. - public enum TargetSchemesGrouping: Codable, Equatable { + public enum TargetSchemesGrouping: Codable, Equatable, Sendable { /// Generate a single scheme for each project. case singleScheme @@ -89,7 +89,7 @@ extension Project.Options { } /// The text settings options - public struct TextSettings: Codable, Equatable { + public struct TextSettings: Codable, Equatable, Sendable { /// Whether tabs should be used instead of spaces public var usesTabs: Bool? diff --git a/Sources/ProjectDescription/ResourceFileElement.swift b/Sources/ProjectDescription/ResourceFileElement.swift index 428120ebd63..5f10bb2b3ef 100644 --- a/Sources/ProjectDescription/ResourceFileElement.swift +++ b/Sources/ProjectDescription/ResourceFileElement.swift @@ -7,7 +7,7 @@ import Foundation /// /// Note: For convenience, an element can be represented as a string literal /// `"some/pattern/**"` is the equivalent of `ResourceFileElement.glob(pattern: "some/pattern/**")` -public enum ResourceFileElement: Codable, Equatable { +public enum ResourceFileElement: Codable, Equatable, Sendable, Hashable { /// A glob pattern of files to include and ODR tags case glob(pattern: Path, excluding: [Path] = [], tags: [String] = [], inclusionCondition: PlatformCondition? = nil) @@ -31,6 +31,6 @@ public enum ResourceFileElement: Codable, Equatable { extension ResourceFileElement: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self = .glob(pattern: Path(value)) + self = .glob(pattern: .path(value)) } } diff --git a/Sources/ProjectDescription/ResourceFileElements.swift b/Sources/ProjectDescription/ResourceFileElements.swift index 89ad885d4fd..9a0e998c5d8 100644 --- a/Sources/ProjectDescription/ResourceFileElements.swift +++ b/Sources/ProjectDescription/ResourceFileElements.swift @@ -1,18 +1,21 @@ import Foundation /// A collection of resource file. -public struct ResourceFileElements: Codable, Equatable { +public struct ResourceFileElements: Codable, Equatable, Sendable { /// List of resource file elements public var resources: [ResourceFileElement] - public init(resources: [ResourceFileElement]) { - self.resources = resources + /// Define your apps privacy manifest + public var privacyManifest: PrivacyManifest? + + public static func resources(_ resources: [ResourceFileElement], privacyManifest: PrivacyManifest? = nil) -> Self { + self.init(resources: resources, privacyManifest: privacyManifest) } } extension ResourceFileElements: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self.init(resources: [.glob(pattern: Path(value))]) + self.init(resources: [.glob(pattern: .path(value))]) } } diff --git a/Sources/ProjectDescription/ResourceSynthesizer.swift b/Sources/ProjectDescription/ResourceSynthesizer.swift index e6d8ecfb8ae..2f4fb377ff0 100644 --- a/Sources/ProjectDescription/ResourceSynthesizer.swift +++ b/Sources/ProjectDescription/ResourceSynthesizer.swift @@ -7,7 +7,7 @@ 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 { // swiftlint:disable:this type_body_length +public struct ResourceSynthesizer: Codable, Equatable, Sendable { // swiftlint:disable:this type_body_length /// Templates can be of multiple types public var templateType: TemplateType public var parser: Parser @@ -15,7 +15,7 @@ public struct ResourceSynthesizer: Codable, Equatable { // swiftlint:disable:thi 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 { + public enum TemplateType: Codable, Equatable, Sendable { /// Plugin template file /// `name` is a name of a plugin /// `resourceName` is a name of the resource - that is used for finding a template as well as naming the resulting @@ -30,7 +30,7 @@ public struct ResourceSynthesizer: Codable, Equatable { // swiftlint:disable:thi /// Each parser will give you different metadata from a file /// You can read more about available parsers and how to use their metadata here: /// https://github.com/SwiftGen/SwiftGen#available-parsers - public enum Parser: String, Codable { + public enum Parser: String, Codable, Sendable { case strings case assets case plists @@ -41,7 +41,7 @@ public struct ResourceSynthesizer: Codable, Equatable { // swiftlint:disable:thi case yaml case files - public enum Option: Equatable, Codable { + public enum Option: Equatable, Codable, Sendable { /// It represents a string value. case string(String) /// It represents an integer value. diff --git a/Sources/ProjectDescription/RunAction.swift b/Sources/ProjectDescription/RunAction.swift index b30be5eea97..91f239e3df1 100644 --- a/Sources/ProjectDescription/RunAction.swift +++ b/Sources/ProjectDescription/RunAction.swift @@ -3,7 +3,7 @@ import Foundation /// An action that runs the built products. /// /// It's initialized with the .runAction static method. -public struct RunAction: Equatable, Codable { +public struct RunAction: Equatable, Codable, Sendable { /// Indicates the build configuration the product should run with. public var configuration: ConfigurationName @@ -29,7 +29,7 @@ public struct RunAction: Equatable, Codable { public var options: RunActionOptions /// List of diagnostics options to set to the action. - public var diagnosticsOptions: [SchemeDiagnosticsOption] + public var diagnosticsOptions: SchemeDiagnosticsOptions /// A target that will be used to expand the variables defined inside Environment Variables definition (e.g. $SOURCE_ROOT) public var expandVariableFromTarget: TargetReference? @@ -46,7 +46,7 @@ public struct RunAction: Equatable, Codable { executable: TargetReference? = nil, arguments: Arguments? = nil, options: RunActionOptions = .options(), - diagnosticsOptions: [SchemeDiagnosticsOption] = [.mainThreadChecker, .performanceAntipatternChecker], + diagnosticsOptions: SchemeDiagnosticsOptions = .options(), expandVariableFromTarget: TargetReference? = nil, launchStyle: LaunchStyle = .automatically ) { @@ -86,7 +86,7 @@ public struct RunAction: Equatable, Codable { executable: TargetReference? = nil, arguments: Arguments? = nil, options: RunActionOptions = .options(), - diagnosticsOptions: [SchemeDiagnosticsOption] = [.mainThreadChecker], + diagnosticsOptions: SchemeDiagnosticsOptions = .options(), expandVariableFromTarget: TargetReference? = nil, launchStyle: LaunchStyle = .automatically ) -> RunAction { diff --git a/Sources/ProjectDescription/RunActionOptions.swift b/Sources/ProjectDescription/RunActionOptions.swift index 75f3e86882e..7d488f57bdb 100644 --- a/Sources/ProjectDescription/RunActionOptions.swift +++ b/Sources/ProjectDescription/RunActionOptions.swift @@ -1,7 +1,7 @@ import Foundation /// Options for the `RunAction` action -public struct RunActionOptions: Equatable, Codable { +public struct RunActionOptions: Equatable, Codable, Sendable { /// Language to use when running the app. public var language: SchemeLanguage? @@ -14,7 +14,7 @@ public struct RunActionOptions: Equatable, Codable { public var storeKitConfigurationPath: Path? /// A simulated GPS location to use when running the app. - public var simulatedLocation: SimulatedLocation? + public var simulatedLocation: ProjectDescription.SimulatedLocation? /// Configure your project to work with the Metal frame debugger. public var enableGPUFrameCaptureMode: GPUFrameCaptureMode @@ -40,7 +40,7 @@ public struct RunActionOptions: Equatable, Codable { language: SchemeLanguage? = nil, region: String? = nil, storeKitConfigurationPath: Path? = nil, - simulatedLocation: SimulatedLocation? = nil, + simulatedLocation: ProjectDescription.SimulatedLocation? = nil, enableGPUFrameCaptureMode: GPUFrameCaptureMode = GPUFrameCaptureMode.default ) { self.language = language @@ -53,7 +53,9 @@ public struct RunActionOptions: Equatable, Codable { /// Creates an `RunActionOptions` instance /// /// - Parameters: - /// - language: language (e.g. "pl"). + /// - language: language (e.g. "en"). + /// + /// - region: region (e.g. "US"). /// /// - storeKitConfigurationPath: The path of the /// [StoreKit configuration @@ -70,12 +72,14 @@ public struct RunActionOptions: Equatable, Codable { public static func options( language: SchemeLanguage? = nil, + region: String? = nil, storeKitConfigurationPath: Path? = nil, - simulatedLocation: SimulatedLocation? = nil, + simulatedLocation: ProjectDescription.SimulatedLocation? = nil, enableGPUFrameCaptureMode: GPUFrameCaptureMode = GPUFrameCaptureMode.default ) -> Self { self.init( language: language, + region: region, storeKitConfigurationPath: storeKitConfigurationPath, simulatedLocation: simulatedLocation, enableGPUFrameCaptureMode: enableGPUFrameCaptureMode @@ -84,77 +88,7 @@ public struct RunActionOptions: Equatable, Codable { } 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 var identifier: String? - /// Path to a .gpx file that indicates the location - public var gpxFile: Path? - - private init( - identifier: String? = nil, - gpxFile: Path? = nil - ) { - self.identifier = identifier - self.gpxFile = gpxFile - } - - public static func custom(gpxFile: Path) -> SimulatedLocation { - .init(gpxFile: gpxFile) - } - - public static var london: SimulatedLocation { - .init(identifier: "London, England") - } - - public static var johannesburg: SimulatedLocation { - .init(identifier: "Johannesburg, South Africa") - } - - public static var moscow: SimulatedLocation { - .init(identifier: "Moscow, Russia") - } - - public static var mumbai: SimulatedLocation { - .init(identifier: "Mumbai, India") - } - - public static var tokyo: SimulatedLocation { - .init(identifier: "Tokyo, Japan") - } - - public static var sydney: SimulatedLocation { - .init(identifier: "Sydney, Australia") - } - - public static var hongKong: SimulatedLocation { - .init(identifier: "Hong Kong, China") - } - - public static var honolulu: SimulatedLocation { - .init(identifier: "Honolulu, HI, USA") - } - - public static var sanFrancisco: SimulatedLocation { - .init(identifier: "San Francisco, CA, USA") - } - - public static var mexicoCity: SimulatedLocation { - .init(identifier: "Mexico City, Mexico") - } - - public static var newYork: SimulatedLocation { - .init(identifier: "New York, NY, USA") - } - - public static var rioDeJaneiro: SimulatedLocation { - .init(identifier: "Rio De Janeiro, Brazil") - } - } -} - -extension RunActionOptions { - public enum GPUFrameCaptureMode: String, Codable, Equatable { + public enum GPUFrameCaptureMode: String, Codable, Equatable, Sendable { case autoEnabled case metal case openGL @@ -165,3 +99,8 @@ extension RunActionOptions { } } } + +extension RunActionOptions { + @available(*, deprecated, message: "Use ProjectDescription.SimulatedLocation directly instead.") + public typealias SimulatedLocation = ProjectDescription.SimulatedLocation +} diff --git a/Sources/ProjectDescription/Scheme.swift b/Sources/ProjectDescription/Scheme.swift index 7414779cb03..05a03e22bcb 100644 --- a/Sources/ProjectDescription/Scheme.swift +++ b/Sources/ProjectDescription/Scheme.swift @@ -3,7 +3,7 @@ import Foundation /// A custom scheme for a project. /// /// A scheme defines a collection of targets to Build, Run, Test, Profile, Analyze and Archive. -public struct Scheme: Equatable, Codable { +public struct Scheme: Equatable, Codable, Sendable { /// The name of the scheme. 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 @@ -35,7 +35,7 @@ public struct Scheme: Equatable, Codable { /// - archiveAction: Action that runs the project archive. /// - profileAction: Action that profiles the project. /// - analyzeAction: Action that analyze the project. - public init( + public static func scheme( name: String, shared: Bool = true, hidden: Bool = false, @@ -45,15 +45,17 @@ public struct Scheme: Equatable, Codable { archiveAction: ArchiveAction? = nil, profileAction: ProfileAction? = nil, analyzeAction: AnalyzeAction? = nil - ) { - self.name = name - self.shared = shared - self.hidden = hidden - self.buildAction = buildAction - self.testAction = testAction - self.runAction = runAction - self.archiveAction = archiveAction - self.profileAction = profileAction - self.analyzeAction = analyzeAction + ) -> Self { + self.init( + name: name, + shared: shared, + hidden: hidden, + buildAction: buildAction, + testAction: testAction, + runAction: runAction, + archiveAction: archiveAction, + profileAction: profileAction, + analyzeAction: analyzeAction + ) } } diff --git a/Sources/ProjectDescription/SchemeDiagnosticsOption.swift b/Sources/ProjectDescription/SchemeDiagnosticsOption.swift deleted file mode 100644 index a797f877919..00000000000 --- a/Sources/ProjectDescription/SchemeDiagnosticsOption.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -// TODO: refactor this to a struct in the next breaking release - -/// Options to configure scheme diagnostics for run and test actions. -public enum SchemeDiagnosticsOption: String, Equatable, Codable { - /// Enable the address sanitizer - case enableAddressSanitizer - - /// Enable the detect use of stack after return of address sanitizer - case enableDetectStackUseAfterReturn - - /// Enable the thread sanitizer - case enableThreadSanitizer - - /// Enable the main thread cheker - case mainThreadChecker - - /// Enable thread performance checker - case performanceAntipatternChecker -} diff --git a/Sources/ProjectDescription/SchemeDiagnosticsOptions.swift b/Sources/ProjectDescription/SchemeDiagnosticsOptions.swift new file mode 100644 index 00000000000..93691c95227 --- /dev/null +++ b/Sources/ProjectDescription/SchemeDiagnosticsOptions.swift @@ -0,0 +1,35 @@ +import Foundation + +/// Options to configure scheme diagnostics for run and test actions. +public struct SchemeDiagnosticsOptions: Equatable, Codable, Sendable { + /// Enable the address sanitizer + public var addressSanitizerEnabled: Bool + + /// Enable the detect use of stack after return of address sanitizer + public var detectStackUseAfterReturnEnabled: Bool + + /// Enable the thread sanitizer + public var threadSanitizerEnabled: Bool + + /// Enable the main thread cheker + public var mainThreadCheckerEnabled: Bool + + /// Enable thread performance checker + public var performanceAntipatternCheckerEnabled: Bool + + public static func options( + addressSanitizerEnabled: Bool = false, + detectStackUseAfterReturnEnabled: Bool = false, + threadSanitizerEnabled: Bool = false, + mainThreadCheckerEnabled: Bool = true, + performanceAntipatternCheckerEnabled: Bool = true + ) -> SchemeDiagnosticsOptions { + return SchemeDiagnosticsOptions( + addressSanitizerEnabled: addressSanitizerEnabled, + detectStackUseAfterReturnEnabled: detectStackUseAfterReturnEnabled, + threadSanitizerEnabled: threadSanitizerEnabled, + mainThreadCheckerEnabled: mainThreadCheckerEnabled, + performanceAntipatternCheckerEnabled: performanceAntipatternCheckerEnabled + ) + } +} diff --git a/Sources/ProjectDescription/SchemeLanguage.swift b/Sources/ProjectDescription/SchemeLanguage.swift index d5a5473c1b2..c55bcd7c908 100644 --- a/Sources/ProjectDescription/SchemeLanguage.swift +++ b/Sources/ProjectDescription/SchemeLanguage.swift @@ -1,7 +1,7 @@ import Foundation /// A language to use for run and test actions. -public struct SchemeLanguage: Codable, Equatable, ExpressibleByStringLiteral { +public struct SchemeLanguage: Codable, Equatable, ExpressibleByStringLiteral, Sendable { public let identifier: String /// Creates a new scheme language. diff --git a/Sources/ProjectDescription/ScreenCaptureFormat.swift b/Sources/ProjectDescription/ScreenCaptureFormat.swift index 127729f3bb8..bae08f82d30 100644 --- a/Sources/ProjectDescription/ScreenCaptureFormat.swift +++ b/Sources/ProjectDescription/ScreenCaptureFormat.swift @@ -7,7 +7,7 @@ import Foundation /// In Xcode 15 screen recordings are enabled by default (in favour of screenshots). /// This setting is ignored by Xcode 14.x and prior. /// -public enum ScreenCaptureFormat: String, Codable { +public enum ScreenCaptureFormat: String, Codable, Sendable { /// Screenshots case screenshots /// Automatic screen recordings diff --git a/Sources/ProjectDescription/Settings.swift b/Sources/ProjectDescription/Settings.swift index ff5ea547adb..4a755254f68 100644 --- a/Sources/ProjectDescription/Settings.swift +++ b/Sources/ProjectDescription/Settings.swift @@ -4,7 +4,7 @@ public typealias SettingsDictionary = [String: SettingValue] /// A value or a collection of values used for settings configuration. public enum SettingValue: ExpressibleByStringInterpolation, ExpressibleByArrayLiteral, ExpressibleByBooleanLiteral, Equatable, - Codable + Codable, Sendable { case string(String) case array([String]) @@ -32,8 +32,8 @@ public enum SettingValue: ExpressibleByStringInterpolation, ExpressibleByArrayLi /// A the build settings and the .xcconfig file of a project or target. It is initialized with either the `.debug` or `.release` /// static method. -public struct Configuration: Equatable, Codable { - public enum Variant: String, Codable { +public struct Configuration: Equatable, Codable, Sendable { + public enum Variant: String, Codable, Sendable { case debug case release } @@ -89,7 +89,7 @@ public struct Configuration: Equatable, Codable { /// Specifies the default set of settings applied to all the projects and targets. /// The default settings can be overridden via `Settings base: SettingsDictionary` /// and `Configuration settings: SettingsDictionary`. -public enum DefaultSettings: Codable, Equatable { +public enum DefaultSettings: Codable, Equatable, Sendable { /// Recommended settings including warning flags to help you catch some of the bugs at the early stage of development. If you /// need to override certain settings in a `Configuration` it's possible to add those keys to `excluding`. case recommended(excluding: Set = []) @@ -114,7 +114,7 @@ extension DefaultSettings { // MARK: - Settings /// A group of settings configuration. -public struct Settings: Equatable, Codable { +public struct Settings: Equatable, Codable, Sendable { /// A dictionary with build settings that are inherited from all the configurations. public var base: SettingsDictionary public var configurations: [Configuration] diff --git a/Sources/ProjectDescription/SettingsTransformers.swift b/Sources/ProjectDescription/SettingsTransformers.swift index 2def23ab9f9..9a0b9c066a4 100644 --- a/Sources/ProjectDescription/SettingsTransformers.swift +++ b/Sources/ProjectDescription/SettingsTransformers.swift @@ -55,6 +55,9 @@ extension SettingsDictionary { } /// Sets `"CODE_SIGN_STYLE"` to `"Automatic"` and `"DEVELOPMENT_TEAM"` to `devTeam` + /// - Parameters: + /// - devTeam: Your Apple Developer Team ID. See + /// [here](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/) how you can find it. public func automaticCodeSigning(devTeam: String) -> SettingsDictionary { merging([ "CODE_SIGN_STYLE": "Automatic", @@ -109,13 +112,25 @@ extension SettingsDictionary { // MARK: - Swift Compiler - Custom Flags /// Sets `"OTHER_SWIFT_FLAGS"` to `flags` + @available(*, deprecated, message: "Please use the version with array support") public func otherSwiftFlags(_ flags: String...) -> SettingsDictionary { - merging(["OTHER_SWIFT_FLAGS": SettingValue(flags.joined(separator: " "))]) + otherSwiftFlags(flags) + } + + /// Sets `"OTHER_SWIFT_FLAGS"` to `flags` + public func otherSwiftFlags(_ flags: [String]) -> SettingsDictionary { + merging(["OTHER_SWIFT_FLAGS": .array(flags)]) } /// Sets `"SWIFT_ACTIVE_COMPILATION_CONDITIONS"` to `conditions` + @available(*, deprecated, message: "Please use the version with array support") public func swiftActiveCompilationConditions(_ conditions: String...) -> SettingsDictionary { - merging(["SWIFT_ACTIVE_COMPILATION_CONDITIONS": SettingValue(conditions.joined(separator: " "))]) + swiftActiveCompilationConditions(conditions) + } + + /// Sets `"SWIFT_ACTIVE_COMPILATION_CONDITIONS"` to `conditions` + public func swiftActiveCompilationConditions(_ conditions: [String]) -> SettingsDictionary { + merging(["SWIFT_ACTIVE_COMPILATION_CONDITIONS": .array(conditions)]) } // MARK: - Swift Compiler - Code Generation @@ -171,11 +186,12 @@ extension SettingsDictionary { public func debugInformationFormat(_ format: DebugInformationFormat) -> SettingsDictionary { merging(["DEBUG_INFORMATION_FORMAT": SettingValue(format)]) } -} -extension SettingsDictionary { - @available(*, deprecated, renamed: "swiftObjcBridgingHeaderPath") - public func swiftObjcBridingHeaderPath(_ path: String) -> SettingsDictionary { - swiftObjcBridgingHeaderPath(path) + /// Sets `"_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES"` + /// NOTE: This is only available when using Xcode 16 or later. + /// This setting may change and is not guaranteed to work across all beta versions. + public func betaFeature_enableExplicitModules(_ enabled: Bool) -> SettingsDictionary { + // This is the value as of Xcode 16 beta 1 + merging(["_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES": SettingValue(enabled)]) } } diff --git a/Sources/ProjectDescription/SimulatedLocation.swift b/Sources/ProjectDescription/SimulatedLocation.swift new file mode 100644 index 00000000000..3b4d9a519e2 --- /dev/null +++ b/Sources/ProjectDescription/SimulatedLocation.swift @@ -0,0 +1,69 @@ +import Foundation + +/// Simulated location represents a GPS location that is used when running an app on the simulator. +public struct SimulatedLocation: Codable, Equatable, Sendable { + /// The identifier of the location (e.g. London, England) + public var identifier: String? + /// Path to a .gpx file that indicates the location + public var gpxFile: Path? + + private init( + identifier: String? = nil, + gpxFile: Path? = nil + ) { + self.identifier = identifier + self.gpxFile = gpxFile + } + + public static func custom(gpxFile: Path) -> SimulatedLocation { + .init(gpxFile: gpxFile) + } + + public static var london: SimulatedLocation { + .init(identifier: "London, England") + } + + public static var johannesburg: SimulatedLocation { + .init(identifier: "Johannesburg, South Africa") + } + + public static var moscow: SimulatedLocation { + .init(identifier: "Moscow, Russia") + } + + public static var mumbai: SimulatedLocation { + .init(identifier: "Mumbai, India") + } + + public static var tokyo: SimulatedLocation { + .init(identifier: "Tokyo, Japan") + } + + public static var sydney: SimulatedLocation { + .init(identifier: "Sydney, Australia") + } + + public static var hongKong: SimulatedLocation { + .init(identifier: "Hong Kong, China") + } + + public static var honolulu: SimulatedLocation { + .init(identifier: "Honolulu, HI, USA") + } + + public static var sanFrancisco: SimulatedLocation { + .init(identifier: "San Francisco, CA, USA") + } + + public static var mexicoCity: SimulatedLocation { + .init(identifier: "Mexico City, Mexico") + } + + public static var newYork: SimulatedLocation { + .init(identifier: "New York, NY, USA") + } + + public static var rioDeJaneiro: SimulatedLocation { + .init(identifier: "Rio de Janeiro, Brazil") + } +} diff --git a/Sources/ProjectDescription/SourceFilesList.swift b/Sources/ProjectDescription/SourceFilesList.swift index 2c9ab9f2300..cfef5914b8f 100644 --- a/Sources/ProjectDescription/SourceFilesList.swift +++ b/Sources/ProjectDescription/SourceFilesList.swift @@ -1,7 +1,7 @@ import Foundation /// A glob pattern configuration representing source files and its compiler flags, if any. -public struct SourceFileGlob: Codable, Equatable { +public struct SourceFileGlob: Codable, Equatable, Sendable { /// Glob pattern to the source files. public var glob: Path @@ -61,27 +61,27 @@ public struct SourceFileGlob: Codable, Equatable { extension SourceFileGlob: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self.init(glob: Path(value), excluding: [], compilerFlags: nil, codeGen: nil, compilationCondition: nil) + self.init(glob: .path(value), excluding: [], compilerFlags: nil, codeGen: nil, compilationCondition: nil) } } /// A collection of source file globs. -public struct SourceFilesList: Codable, Equatable { +public struct SourceFilesList: Codable, Equatable, Sendable { /// List glob patterns. public var globs: [SourceFileGlob] /// Creates the source files list with the glob patterns. /// /// - Parameter globs: Glob patterns. - public init(globs: [SourceFileGlob]) { - self.globs = globs + public static func sourceFilesList(globs: [SourceFileGlob]) -> Self { + self.init(globs: globs) } /// Creates the source files list with the glob patterns as strings. /// /// - Parameter globs: Glob patterns. - public init(globs: [String]) { - self.globs = globs.map(SourceFileGlob.init) + public static func sourceFilesList(globs: [String]) -> Self { + sourceFilesList(globs: globs.map(SourceFileGlob.init)) } /// Returns a sources list from a list of paths. @@ -94,7 +94,7 @@ public struct SourceFilesList: Codable, Equatable { /// Support file as single string extension SourceFilesList: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { - self.init(globs: [value]) + self = .sourceFilesList(globs: [value]) } } diff --git a/Sources/ProjectDescription/Target.swift b/Sources/ProjectDescription/Target.swift index a476b508f08..8cbb7840977 100644 --- a/Sources/ProjectDescription/Target.swift +++ b/Sources/ProjectDescription/Target.swift @@ -1,7 +1,7 @@ import Foundation /// A target of a project. -public struct Target: Codable, Equatable { +public struct Target: Codable, Equatable, Sendable { /// The name of the target. Also, the product name if not specified with ``productName``. public var name: String @@ -70,7 +70,10 @@ public struct Target: Codable, Equatable { /// Specifies whether if the target can be merged as part of another binary or not public var mergeable: Bool - public init( + /// The target's tags associated with on demand resources + public var onDemandResourcesTags: OnDemandResourcesTags? + + public static func target( name: String, destinations: Destinations, product: Product, @@ -92,260 +95,33 @@ public struct Target: Codable, Equatable { additionalFiles: [FileElement] = [], buildRules: [BuildRule] = [], mergedBinaryType: MergedBinaryType = .disabled, - mergeable: Bool = false - ) { - self.name = name - self.destinations = destinations - self.bundleId = bundleId - self.productName = productName - self.product = product - self.infoPlist = infoPlist - self.entitlements = entitlements - self.dependencies = dependencies - self.settings = settings - self.sources = sources - self.resources = resources - self.copyFiles = copyFiles - self.headers = headers - self.scripts = scripts - self.coreDataModels = coreDataModels - self.environmentVariables = environmentVariables - self.launchArguments = launchArguments - self.deploymentTargets = deploymentTargets - self.additionalFiles = additionalFiles - self.buildRules = buildRules - self.mergedBinaryType = mergedBinaryType - self.mergeable = mergeable - } - - @available( - *, - deprecated, - message: "Use `Destinations` and `DeploymentTargets` to configure deployment devices and minimum platform versions." - ) - public init( - name: String, - platform: Platform, - product: Product, - productName: String? = nil, - bundleId: String, - deploymentTarget: DeploymentTarget? = nil, - infoPlist: InfoPlist? = .default, - sources: SourceFilesList? = nil, - resources: ResourceFileElements? = nil, - copyFiles: [CopyFilesAction]? = nil, - headers: Headers? = nil, - entitlements: Entitlements? = nil, - scripts: [TargetScript] = [], - dependencies: [TargetDependency] = [], - settings: Settings? = nil, - coreDataModels: [CoreDataModel] = [], - environmentVariables: [String: EnvironmentVariable] = [:], - launchArguments: [LaunchArgument] = [], - additionalFiles: [FileElement] = [], - buildRules: [BuildRule] = [], - mergedBinaryType: MergedBinaryType = .disabled, - mergeable: Bool = false - ) { - self.name = name - destinations = Destinations.from(platform: platform, deploymentTarget: deploymentTarget) - self.bundleId = bundleId - self.productName = productName - self.product = product - self.infoPlist = infoPlist - self.entitlements = entitlements - self.dependencies = dependencies - self.settings = settings - self.sources = sources - self.resources = resources - self.copyFiles = copyFiles - self.headers = headers - self.scripts = scripts - self.coreDataModels = coreDataModels - self.environmentVariables = environmentVariables - self.launchArguments = launchArguments - deploymentTargets = DeploymentTargets.from(manifest: deploymentTarget) - self.additionalFiles = additionalFiles - self.buildRules = buildRules - self.mergedBinaryType = mergedBinaryType - self.mergeable = mergeable - } - - @available(*, deprecated, message: "please use environmentVariables instead") - public init( - name: String, - platform: Platform, - product: Product, - productName: String? = nil, - bundleId: String, - deploymentTarget: DeploymentTarget? = nil, - infoPlist: InfoPlist? = .default, - sources: SourceFilesList? = nil, - resources: ResourceFileElements? = nil, - copyFiles: [CopyFilesAction]? = nil, - headers: Headers? = nil, - entitlements: Entitlements? = nil, - scripts: [TargetScript] = [], - dependencies: [TargetDependency] = [], - settings: Settings? = nil, - coreDataModels: [CoreDataModel] = [], - environment: [String: String], - launchArguments: [LaunchArgument] = [], - additionalFiles: [FileElement] = [], - buildRules: [BuildRule] = [], - mergedBinaryType: MergedBinaryType = .disabled, - mergeable: Bool = false - ) { - self.name = name - destinations = Destinations.from(platform: platform, deploymentTarget: deploymentTarget) - self.bundleId = bundleId - self.productName = productName - self.product = product - self.infoPlist = infoPlist - self.entitlements = entitlements - self.dependencies = dependencies - self.settings = settings - self.sources = sources - self.resources = resources - self.copyFiles = copyFiles - self.headers = headers - self.scripts = scripts - self.coreDataModels = coreDataModels - environmentVariables = environment.mapValues { value in - EnvironmentVariable(value: value, isEnabled: true) - } - self.launchArguments = launchArguments - deploymentTargets = DeploymentTargets.from(manifest: deploymentTarget) - self.additionalFiles = additionalFiles - self.buildRules = buildRules - self.mergedBinaryType = mergedBinaryType - self.mergeable = mergeable - } -} - -extension Target { - @available(*, deprecated, renamed: "Destinations", message: "Targets are no longer constrained to a single platform") - var platform: Platform { - destinations.platforms.first ?? .iOS - } - - @available( - *, - deprecated, - renamed: "DeploymentTargets", - message: "Device support is now defined in `Destinations`. Minimum Deployment Version is defined in `DeploymentTargets`" - ) - var deploymentTarget: DeploymentTarget? { - switch platform { - case .iOS: - guard let version = deploymentTargets?.iOS else { return nil } - var devices = DeploymentDevice() - - if destinations.contains(.iPhone) { - devices.insert(.iphone) - } - - if destinations.contains(.iPad) { - devices.insert(.ipad) - } - - if destinations.contains(.macCatalyst) { - devices.insert(.mac) - } - - if destinations.contains(.appleVisionWithiPadDesign) { - devices.insert(.vision) - } - - return .iOS( - targetVersion: version, - devices: devices, - supportsMacDesignedForIOS: destinations.contains(.macWithiPadDesign) - ) - case .macOS: - guard let version = deploymentTargets?.macOS else { return nil } - return .macOS(targetVersion: version) - case .watchOS: - guard let version = deploymentTargets?.watchOS else { return nil } - return .watchOS(targetVersion: version) - case .tvOS: - guard let version = deploymentTargets?.tvOS else { return nil } - return .tvOS(targetVersion: version) - case .visionOS: - guard let version = deploymentTargets?.visionOS else { return nil } - return .visionOS(targetVersion: version) - } - } -} - -extension Destinations { - /// Maps a ProjectDescription.Package instance into a TuistGraph.Package model. - /// - Parameters: - /// - manifest: Manifest representation of Package. - /// - generatorPaths: Generator paths. - fileprivate static func from( - platform: Platform, - deploymentTarget: DeploymentTarget? - ) -> Destinations { - switch (platform, deploymentTarget) { - case (.macOS, _): - return [.mac] - case let (.iOS, .some(.iOS(_, devices, supportsMacDesignedForIOS: supportsMacDesignedForIOS))): - var destinations: [Destination] = [] - - if devices.contains(.iphone) { - destinations.append(.iPhone) - } - - if devices.contains(.ipad) { - destinations.append(.iPad) - } - - if devices.contains(.mac) { - destinations.append(.macCatalyst) - } - - if devices.contains(.vision) { - destinations.append(.appleVisionWithiPadDesign) - } - - if supportsMacDesignedForIOS { - destinations.append(.macWithiPadDesign) - } - - return Set(destinations) - case (.iOS, _): // an iOS platform, but `nil` deployment target. - return .iOS - case (.tvOS, _): - return .tvOS - case (.watchOS, _): - return .watchOS - case (.visionOS, _): - return .visionOS - } - } -} - -extension DeploymentTargets { - /// Maps a ProjectDescription.DeploymentTarget instance into a TuistGraph.DeploymentTarget instance. - /// - Parameters: - /// - manifest: Manifest representation of deployment target model. - static func from(manifest: DeploymentTarget?) -> DeploymentTargets { - if let manifest { - switch manifest { - case let .iOS(version, _, _): - return .iOS(version) - case let .macOS(version): - return .macOS(version) - case let .watchOS(version): - return .watchOS(version) - case let .tvOS(version): - return .tvOS(version) - case let .visionOS(version): - return .visionOS(version) - } - } else { - return DeploymentTargets() - } + mergeable: Bool = false, + onDemandResourcesTags: OnDemandResourcesTags? = nil + ) -> Self { + self.init( + name: name, + destinations: destinations, + product: product, + productName: productName, + bundleId: bundleId, + deploymentTargets: deploymentTargets, + infoPlist: infoPlist, + sources: sources, + resources: resources, + copyFiles: copyFiles, + headers: headers, + entitlements: entitlements, + scripts: scripts, + dependencies: dependencies, + settings: settings, + coreDataModels: coreDataModels, + environmentVariables: environmentVariables, + launchArguments: launchArguments, + additionalFiles: additionalFiles, + buildRules: buildRules, + mergedBinaryType: mergedBinaryType, + mergeable: mergeable, + onDemandResourcesTags: onDemandResourcesTags + ) } } diff --git a/Sources/ProjectDescription/TargetDependency.swift b/Sources/ProjectDescription/TargetDependency.swift index b2228b37685..ac726fa2789 100644 --- a/Sources/ProjectDescription/TargetDependency.swift +++ b/Sources/ProjectDescription/TargetDependency.swift @@ -2,7 +2,7 @@ import Foundation /// Dependency status used by `.framework` and `.xcframework` target /// dependencies -public enum FrameworkStatus: String, Codable, Hashable { +public enum FrameworkStatus: String, Codable, Hashable, Sendable { /// Required dependency case required @@ -11,7 +11,7 @@ public enum FrameworkStatus: String, Codable, Hashable { } /// Dependency status used by `.sdk` target dependencies -public enum SDKStatus: String, Codable, Hashable { +public enum SDKStatus: String, Codable, Hashable, Sendable { /// Required dependency case required @@ -20,17 +20,24 @@ public enum SDKStatus: String, Codable, Hashable { } /// Dependency type used by `.sdk` target dependencies -public enum SDKType: String, Codable, Hashable { +public enum SDKType: String, Codable, Hashable, Sendable { /// Library SDK dependency + /// Libraries are located in: + /// `{path-to-xcode}.app/Contents/Developer/Platforms/{platform}.platform/Developer/SDKs/{runtime}.sdk/usr/lib` case library + /// Swift library SDK dependency + /// Swift libraries are located in: + /// `{path-to-xcode}.app/Contents/Developer/Platforms/{platform}.platform/Developer/SDKs/{runtime}.sdk/usr/lib/swift` + case swiftLibrary + /// Framework SDK dependency case framework } /// A target dependency. -public enum TargetDependency: Codable, Hashable { - public enum PackageType: Codable, Hashable { +public enum TargetDependency: Codable, Hashable, Sendable { + public enum PackageType: Codable, Hashable, Sendable { /// A runtime package type represents a standard package whose sources are linked at runtime. /// For example importing the framework and consuming from dependent targets. case runtime @@ -43,24 +50,6 @@ public enum TargetDependency: Codable, Hashable { case macro } - /// A condition applied to a `TargetDependency` allowing it to only be used in certain circumstances - @available(*, deprecated, renamed: "PlatformCondition") - public struct Condition: Codable, Hashable, Equatable { - public let platformFilters: Set - /// For internal use only. use `.when` to ensure we can not have a `Condition` with an empty set of filters. - private init(platformFilters: Set) { - self.platformFilters = platformFilters - } - - /// Creates a condition using the specified set of filters. - /// - Parameter platformFilters: filters to define which platforms this condition supports - /// - Returns: a `Condition` with the given set of filters or `nil` if empty. - public static func when(_ platformFilters: Set) -> Condition? { - guard !platformFilters.isEmpty else { return nil } - return Condition(platformFilters: platformFilters) - } - } - /// Dependency on another target within the same project /// /// - Parameters: @@ -94,7 +83,8 @@ public enum TargetDependency: Codable, Hashable { case library(path: Path, publicHeaders: Path, swiftModuleMap: Path?, condition: PlatformCondition? = nil) /// Dependency on a swift package manager product using Xcode native integration. It's recommended to use `external` instead. - /// For more info, check the [external dependencies documentation](https://docs.tuist.io/guides/third-party-dependencies/). + /// For more info, check the [external dependencies documentation + /// ](https://docs.tuist.io/documentation/tuist/dependencies/#External-dependencies). /// /// - Parameters: /// - product: The name of the output product. ${PRODUCT_NAME} inside Xcode. @@ -103,14 +93,6 @@ public enum TargetDependency: Codable, Hashable { /// - condition: condition under which to use this dependency, `nil` if this should always be used case package(product: String, type: PackageType = .runtime, condition: PlatformCondition? = nil) - /// Dependency on a swift package manager plugin product using Xcode native integration. - /// - /// - Parameters: - /// - product: The name of the output product. ${PRODUCT_NAME} inside Xcode. - /// e.g. RxSwift - /// - condition: condition under which to use this dependency, `nil` if this should always be used - case packagePlugin(product: String, condition: PlatformCondition? = nil) - /// Dependency on system library or framework /// /// - Parameters: @@ -132,7 +114,7 @@ public enum TargetDependency: Codable, Hashable { /// Dependency on XCTest. case xctest - /// Dependency on an external dependency imported through `Dependencies.swift`. + /// Dependency on an external dependency imported through `Package.swift`. /// /// - Parameters: /// - name: Name of the external dependency @@ -171,8 +153,6 @@ public enum TargetDependency: Codable, Hashable { return "library" case .package: return "package" - case .packagePlugin: - return "packagePlugin" case .sdk: return "sdk" case .xcframework: diff --git a/Sources/ProjectDescription/TargetReference.swift b/Sources/ProjectDescription/TargetReference.swift index 8f483c0b448..0ade25bd940 100644 --- a/Sources/ProjectDescription/TargetReference.swift +++ b/Sources/ProjectDescription/TargetReference.swift @@ -3,13 +3,13 @@ import Foundation /// A target reference for a specified project. /// /// The project is specified through the path and should contain the target name. -public struct TargetReference: Hashable, Codable, ExpressibleByStringInterpolation { +public struct TargetReference: Hashable, Codable, ExpressibleByStringInterpolation, Sendable { /// Path to the target's project directory. public var projectPath: Path? /// Name of the target. public var targetName: String - public init(projectPath: Path?, target: String) { + init(projectPath: Path?, target: String) { self.projectPath = projectPath targetName = target } @@ -21,4 +21,8 @@ public struct TargetReference: Hashable, Codable, ExpressibleByStringInterpolati public static func project(path: Path, target: String) -> TargetReference { .init(projectPath: path, target: target) } + + public static func target(_ name: String) -> TargetReference { + .init(projectPath: nil, target: name) + } } diff --git a/Sources/ProjectDescription/TargetScript.swift b/Sources/ProjectDescription/TargetScript.swift index 71b9c86f3ec..a92e95c8441 100644 --- a/Sources/ProjectDescription/TargetScript.swift +++ b/Sources/ProjectDescription/TargetScript.swift @@ -4,12 +4,12 @@ import Foundation /// /// Target scripts, represented as target script build phases in the generated Xcode projects, are useful to define actions to be /// executed before of after the build process of a target. -public struct TargetScript: Codable, Equatable { // swiftlint:disable:this type_body_length +public struct TargetScript: Codable, Equatable, Sendable { // swiftlint:disable:this type_body_length /// Order when the script gets executed. /// /// - pre: Before the sources and resources build phase. /// - post: After the sources and resources build phase. - public enum Order: String, Codable, Equatable { + public enum Order: String, Codable, Equatable, Sendable { case pre case post } @@ -19,7 +19,7 @@ public struct TargetScript: Codable, Equatable { // swiftlint:disable:this type_ /// - tool: Executes the tool with the given arguments. Tuist will look up the tool on the environment's PATH. /// - scriptPath: Executes the file at the path with the given arguments. /// - text: Executes the embedded script. This should be a short command. - public enum Script: Equatable, Codable { + public enum Script: Equatable, Codable, Sendable { case tool(path: String, args: [String]) case scriptPath(path: Path, args: [String]) case embedded(String) diff --git a/Sources/ProjectDescription/Template/Template.swift b/Sources/ProjectDescription/Template/Template.swift index 3b5c0f0ad5d..fd45c1e7b31 100644 --- a/Sources/ProjectDescription/Template/Template.swift +++ b/Sources/ProjectDescription/Template/Template.swift @@ -1,7 +1,7 @@ import Foundation /// A scaffold template model. -public struct Template: Codable, Equatable { +public struct Template: Codable, Equatable, Sendable { /// Description of template public let description: String /// Attributes to be passed to template @@ -21,7 +21,7 @@ public struct Template: Codable, Equatable { } /// Enum containing information about how to generate item - public enum Contents: Codable, Equatable { + public enum Contents: Codable, Equatable, Sendable { /// String Contents is defined in `name_of_template.swift` and contains a simple `String` /// Can not contain any additional logic apart from plain `String` from `arguments` case string(String) @@ -34,22 +34,75 @@ public struct Template: Codable, Equatable { } /// File description for generating - public struct Item: Codable, Equatable { + public struct Item: Codable, Equatable, Sendable { public let path: String public let contents: Contents - public init(path: String, contents: Contents) { - self.path = path - self.contents = contents + public static func item(path: String, contents: Contents) -> Self { + self.init(path: path, contents: contents) } } /// Attribute to be passed to `tuist scaffold` for generating with `Template` - public enum Attribute: Codable, Equatable { + public enum Attribute: Codable, Equatable, Sendable { /// Required attribute with a given name case required(String) /// Optional attribute with a given name and a default value used when attribute not provided by user - case optional(String, default: String) + case optional(String, default: Value) + } +} + +extension Template.Attribute { + /// This represents the default value type of Attribute + public indirect enum Value: Codable, Equatable, Sendable { + /// It represents a string value. + case string(String) + /// It represents an integer value. + case integer(Int) + /// It represents a floating value. + case real(Double) + /// It represents a boolean value. + case boolean(Bool) + /// It represents a dictionary value. + case dictionary([String: Value]) + /// It represents an array value. + case array([Value]) + } +} + +extension Template.Attribute.Value: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension Template.Attribute.Value: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .integer(value) + } +} + +extension Template.Attribute.Value: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self = .real(value) + } +} + +extension Template.Attribute.Value: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self = .boolean(value) + } +} + +extension Template.Attribute.Value: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, Template.Attribute.Value)...) { + self = .dictionary(Dictionary(uniqueKeysWithValues: elements)) + } +} + +extension Template.Attribute.Value: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: Template.Attribute.Value...) { + self = .array(elements) } } diff --git a/Sources/ProjectDescription/TestAction.swift b/Sources/ProjectDescription/TestAction.swift index 5681ec5061d..57036fa7e56 100644 --- a/Sources/ProjectDescription/TestAction.swift +++ b/Sources/ProjectDescription/TestAction.swift @@ -4,7 +4,7 @@ import Foundation /// /// You can create a test action with either a set of test targets or test plans using the `.targets` or `.testPlans` static /// methods respectively. -public struct TestAction: Equatable, Codable { +public struct TestAction: Equatable, Codable, Sendable { /// List of test plans. The first in the list will be the default plan. public var testPlans: [Path]? @@ -33,7 +33,7 @@ public struct TestAction: Equatable, Codable { public var options: TestActionOptions /// List of diagnostics options to set to the action. - public var diagnosticsOptions: [SchemeDiagnosticsOption] + public var diagnosticsOptions: SchemeDiagnosticsOptions /// List of testIdentifiers to skip to the test public var skippedTests: [String]? @@ -48,7 +48,7 @@ public struct TestAction: Equatable, Codable { preActions: [ExecutionAction], postActions: [ExecutionAction], options: TestActionOptions, - diagnosticsOptions: [SchemeDiagnosticsOption], + diagnosticsOptions: SchemeDiagnosticsOptions, skippedTests: [String]? ) { self.testPlans = testPlans @@ -86,7 +86,7 @@ public struct TestAction: Equatable, Codable { preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [], options: TestActionOptions = .options(), - diagnosticsOptions: [SchemeDiagnosticsOption] = [.mainThreadChecker], + diagnosticsOptions: SchemeDiagnosticsOptions = .options(), skippedTests: [String] = [] ) -> Self { Self( @@ -129,7 +129,7 @@ public struct TestAction: Equatable, Codable { preActions: preActions, postActions: postActions, options: .options(), - diagnosticsOptions: [], + diagnosticsOptions: .options(), skippedTests: nil ) } diff --git a/Sources/ProjectDescription/TestActionOptions.swift b/Sources/ProjectDescription/TestActionOptions.swift index 60762d8ab7e..78eb159c553 100644 --- a/Sources/ProjectDescription/TestActionOptions.swift +++ b/Sources/ProjectDescription/TestActionOptions.swift @@ -1,7 +1,7 @@ import Foundation /// The type `TestActionOptions` represents a set of options for a test action. -public struct TestActionOptions: Equatable, Codable { +public struct TestActionOptions: Equatable, Codable, Sendable { /// Language used to run the tests. public var language: SchemeLanguage? diff --git a/Sources/ProjectDescription/TestableTarget.swift b/Sources/ProjectDescription/TestableTarget.swift index 7db0498aa71..004cf368976 100644 --- a/Sources/ProjectDescription/TestableTarget.swift +++ b/Sources/ProjectDescription/TestableTarget.swift @@ -1,24 +1,58 @@ import Foundation -public struct TestableTarget: Equatable, Codable, ExpressibleByStringInterpolation { +public struct TestableTarget: Equatable, Codable, ExpressibleByStringInterpolation, Sendable { public var target: TargetReference public var isSkipped: Bool public var isParallelizable: Bool public var isRandomExecutionOrdering: Bool + public var simulatedLocation: SimulatedLocation? - public init( + init( target: TargetReference, - skipped: Bool = false, - parallelizable: Bool = false, - randomExecutionOrdering: Bool = false + isSkipped: Bool, + isParallelizable: Bool, + isRandomExecutionOrdering: Bool, + simulatedLocation: SimulatedLocation? = nil ) { self.target = target - isSkipped = skipped - isParallelizable = parallelizable - isRandomExecutionOrdering = randomExecutionOrdering + self.isSkipped = isSkipped + self.isParallelizable = isParallelizable + self.isRandomExecutionOrdering = isRandomExecutionOrdering + self.simulatedLocation = simulatedLocation + } + + /// Returns a testable target. + /// + /// - Parameters: + /// - target: The name or reference of target to test. + /// - isSkipped: Whether to skip this test target. If true, the test target is disabled. + /// - isParallelizable: Whether to run in parallel. + /// - isRandomExecutionOrdering: Whether to test in random order. + /// - simulatedLocation: The simulated GPS location to use when testing this target. + /// Please note that the `.custom(gpxPath:)` case must refer to a valid GPX file in your project’s resources. + public static func testableTarget( + target: TargetReference, + isSkipped: Bool = false, + isParallelizable: Bool = false, + isRandomExecutionOrdering: Bool = false, + simulatedLocation: SimulatedLocation? = nil + ) -> Self { + self.init( + target: target, + isSkipped: isSkipped, + isParallelizable: isParallelizable, + isRandomExecutionOrdering: isRandomExecutionOrdering, + simulatedLocation: simulatedLocation + ) } public init(stringLiteral value: String) { - self.init(target: .init(projectPath: nil, target: value)) + self.init( + target: TargetReference(projectPath: nil, target: value), + isSkipped: false, + isParallelizable: false, + isRandomExecutionOrdering: false, + simulatedLocation: nil + ) } } diff --git a/Sources/ProjectDescription/TestingOptions.swift b/Sources/ProjectDescription/TestingOptions.swift index b644eae3327..9cbb0fcf032 100644 --- a/Sources/ProjectDescription/TestingOptions.swift +++ b/Sources/ProjectDescription/TestingOptions.swift @@ -1,5 +1,5 @@ /// Options to configure testing of autogenerated schemes. -public struct TestingOptions: OptionSet, Codable, Equatable { +public struct TestingOptions: OptionSet, Codable, Equatable, Sendable { public let rawValue: Int public init(rawValue: Int) { diff --git a/Sources/ProjectDescription/Version.swift b/Sources/ProjectDescription/Version.swift index 3c273194f33..e9b6a1ccb9a 100644 --- a/Sources/ProjectDescription/Version.swift +++ b/Sources/ProjectDescription/Version.swift @@ -1,7 +1,7 @@ /// A struct representing a semver version. /// This is taken from SPMUtility and copied here so we do not create a direct dependency for ProjectDescription. Used for /// specifying version number requirements inside of Project.swift -public struct Version: Hashable, Codable { +public struct Version: Hashable, Codable, Sendable { /// The major version. public var major: Int diff --git a/Sources/ProjectDescription/Workspace.swift b/Sources/ProjectDescription/Workspace.swift index bb2a6cb6de0..197e5cfcc0a 100644 --- a/Sources/ProjectDescription/Workspace.swift +++ b/Sources/ProjectDescription/Workspace.swift @@ -22,7 +22,7 @@ import Foundation /// ) /// ``` -public struct Workspace: Codable, Equatable { +public struct Workspace: Codable, Equatable, Sendable { /// The name of the workspace. Also, the file name of the generated Xcode workspace. public let name: String diff --git a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift index c54dab3d865..40d6dee2239 100644 --- a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift +++ b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift @@ -1,10 +1,10 @@ extension Workspace { /// Generation options allow customizing the generation of the Xcode workspace. - public struct GenerationOptions: Codable, Equatable { + public struct GenerationOptions: Codable, Equatable, Sendable { /// Contains options for autogenerated workspace schemes - public enum AutogeneratedWorkspaceSchemes: Codable, Equatable { + public enum AutogeneratedWorkspaceSchemes: Codable, Equatable, Sendable { /// Contains options for code coverage - public enum CodeCoverageMode: Codable, Equatable { + public enum CodeCoverageMode: Codable, Equatable, Sendable { /// Gather code coverage data for all targets in workspace. case all /// Enable code coverage for targets that have enabled code coverage in any of schemes in workspace. diff --git a/Sources/TuistAcceptanceTesting/ServerAcceptanceTestCase.swift b/Sources/TuistAcceptanceTesting/ServerAcceptanceTestCase.swift new file mode 100644 index 00000000000..70d0b26b764 --- /dev/null +++ b/Sources/TuistAcceptanceTesting/ServerAcceptanceTestCase.swift @@ -0,0 +1,44 @@ +import Foundation +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistKit + +open class ServerAcceptanceTestCase: TuistAcceptanceTestCase { + public var fullHandle: String = "" + public var organizationHandle: String = "" + public var projectHandle: String = "" + + override public func setUpFixture(_ fixture: TuistAcceptanceFixtures) async throws { + try await super.setUpFixture(fixture) + organizationHandle = String(UUID().uuidString.prefix(12).lowercased()) + projectHandle = String(UUID().uuidString.prefix(12).lowercased()) + fullHandle = "\(organizationHandle)/\(projectHandle)" + let email = try XCTUnwrap(ProcessInfo.processInfo.environment[EnvKey.authEmail.rawValue]) + let password = try XCTUnwrap(ProcessInfo.processInfo.environment[EnvKey.authPassword.rawValue]) + try await run(AuthCommand.self, "--email", email, "--password", password) + try await run(OrganizationCreateCommand.self, organizationHandle) + try await run(ProjectCreateCommand.self, fullHandle) + try FileHandler.shared.write( + """ + import ProjectDescription + + let config = Config( + fullHandle: "\(fullHandle)", + url: "\(ProcessInfo.processInfo.environment["TUIST_URL"] ?? "https://canary.tuist.io")" + ) + """, + path: fixturePath.appending(components: "Tuist", "Config.swift"), + atomically: true + ) + } + + override open func tearDown() async throws { + try await run(ProjectDeleteCommand.self, fullHandle) + try await run(OrganizationDeleteCommand.self, organizationHandle) + try await run(LogoutCommand.self) + TestingLogHandler.reset() + try await super.tearDown() + } +} diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift index e3c842f1047..a278abb8724 100644 --- a/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift @@ -2,15 +2,20 @@ import Foundation public enum TuistAcceptanceFixtures { case appWithBuildRules + case appWithCustomDefaultConfiguration case appWithFrameworkAndTests + case appWithGoogleMaps case appWithPlugins + case appWithPreviews case appWithSpmDependencies case appWithTestPlan + case appWithTests case commandLineToolBasic case commandLineToolWithDynamicFramework case commandLineToolWithDynamicLibrary case commandLineToolWithStaticLibrary case frameworkWithEnvironmentVariables + case frameworkWithMacroAndPluginPackages case frameworkWithNativeSwiftMacro case frameworkWithSwiftMacro case invalidManifest @@ -24,6 +29,8 @@ public enum TuistAcceptanceFixtures { case iosWppWithCustomResourceParserOptions case iosAppWithCustomScheme case iosAppWithExtensions + case iosAppWithExtensionAndTests + case iosAppWithDynamicFrameworksLinkingStaticFrameworks case iosAppWithFrameworkAndResources case iosAppWithFrameworkAndDisabledResources case iosAppWithFrameworkLinkingStaticFramework @@ -35,6 +42,10 @@ public enum TuistAcceptanceFixtures { case iosAppWithLocalBinarySwiftPackage case iosAppWithLocalSwiftPackage case iosAppWithMultiConfigs + case iosAppWithOnDemandResources + case iosAppWithPluginsAndTemplates + case iosAppWithPrivacyManifest + case iosAppWithSpmDependencies case iosAppWithRemoteBinarySwiftPackage case iosAppWithRemoteSwiftPackage case iosAppWithStaticFrameworks @@ -51,28 +62,41 @@ public enum TuistAcceptanceFixtures { case macosAppWithExtensions case manifestWithLogs case multiplatformAppWithExtension + case multiplatformAppWithMacrosAndEmbeddedWatchOSApp case multiplatformAppWithSdk + case multiplatformµFeatureUnitTestsWithExplicitDependencies case plugin + case projectWithClassPrefix case projectWithFileHeaderTemplate case projectWithInlineFileHeaderTemplate + case spmPackage case tuistPlugin case visionosApp case workspaceWithFileHeaderTemplate case workspaceWithInlineFileHeaderTemplate + case xcodeApp case custom(String) public var path: String { switch self { case .appWithBuildRules: return "app_with_build_rules" + case .appWithCustomDefaultConfiguration: + return "app_with_custom_default_configuration" case .appWithFrameworkAndTests: return "app_with_framework_and_tests" + case .appWithGoogleMaps: + return "app_with_google_maps" case .appWithPlugins: return "app_with_plugins" + case .appWithPreviews: + return "app_with_previews" case .appWithSpmDependencies: return "app_with_spm_dependencies" case .appWithTestPlan: return "app_with_test_plan" + case .appWithTests: + return "app_with_tests" case .commandLineToolBasic: return "command_line_tool_basic" case .commandLineToolWithDynamicFramework: @@ -83,6 +107,8 @@ public enum TuistAcceptanceFixtures { return "command_line_tool_with_static_library" case .frameworkWithEnvironmentVariables: return "framework_with_environment_variables" + case .frameworkWithMacroAndPluginPackages: + return "framework_with_macro_and_plugin_packages" case .frameworkWithNativeSwiftMacro: return "framework_with_native_swift_macro" case .frameworkWithSwiftMacro: @@ -103,12 +129,16 @@ public enum TuistAcceptanceFixtures { return "ios_app_with_custom_configuration" case .iosAppWithCustomDevelopmentRegion: return "ios_app_with_custom_development_region" + case .iosAppWithDynamicFrameworksLinkingStaticFrameworks: + return "ios_app_with_dynamic_frameworks_linking_static_frameworks" 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 .iosAppWithExtensionAndTests: + return "ios_app_with_extension_and_tests" case .iosAppWithFrameworkAndResources: return "ios_app_with_framework_and_resources" case .iosAppWithFrameworkAndDisabledResources: @@ -131,6 +161,14 @@ public enum TuistAcceptanceFixtures { return "ios_app_with_local_swift_package" case .iosAppWithMultiConfigs: return "ios_app_with_multi_configs" + case .iosAppWithOnDemandResources: + return "ios_app_with_on_demand_resources" + case .iosAppWithSpmDependencies: + return "ios_app_with_spm_dependencies" + case .iosAppWithPluginsAndTemplates: + return "ios_app_with_plugins_and_templates" + case .iosAppWithPrivacyManifest: + return "ios_app_with_privacy_manifest" case .iosAppWithRemoteBinarySwiftPackage: return "ios_app_with_remote_binary_swift_package" case .iosAppWithRemoteSwiftPackage: @@ -163,14 +201,22 @@ public enum TuistAcceptanceFixtures { return "manifest_with_logs" case .multiplatformAppWithExtension: return "multiplatform_app_with_extension" + case .multiplatformAppWithMacrosAndEmbeddedWatchOSApp: + return "multiplatform_app_with_macros_and_embedded_watchos_app" case .multiplatformAppWithSdk: return "multiplatform_app_with_sdk" + case .multiplatformµFeatureUnitTestsWithExplicitDependencies: + return "multiplatform_µFeature_unit_tests_with_explicit_dependencies" case .plugin: return "plugin" + case .projectWithClassPrefix: + return "project_with_class_prefix" case .projectWithFileHeaderTemplate: return "project_with_file_header_template" case .projectWithInlineFileHeaderTemplate: return "project_with_inline_file_header_template" + case .spmPackage: + return "spm_package" case .tuistPlugin: return "tuist_plugin" case .visionosApp: @@ -179,6 +225,8 @@ public enum TuistAcceptanceFixtures { return "workspace_with_file_header_template" case .workspaceWithInlineFileHeaderTemplate: return "workspace_with_inline_file_header_template" + case .xcodeApp: + return "xcode_app" case let .custom(path): return path } diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift index 21f96af0ed0..0ce81363f90 100644 --- a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport import XcodeProj import XCTest @@ -78,4 +78,54 @@ extension TuistAcceptanceTestCase { return } } + + /// Asserts that a simulated location is contained in a specific testable target. + /// - Parameters: + /// - xcodeprojPath: A specific `.xcodeproj` file path. + /// - scheme: A specific scheme name. + /// - testTarget: A specific test target name. + /// - simulatedLocation: A simulated location. This value can be passed a `location string` or a `GPX filename`. + /// For example, "Rio de Janeiro, Brazil" or "Grand Canyon.gpx". + public func XCTAssertContainsSimulatedLocation( + xcodeprojPath: AbsolutePath, + scheme: String, + testTarget: String, + simulatedLocation: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + let xcodeproj = try XcodeProj(pathString: xcodeprojPath.pathString) + + guard let scheme = xcodeproj.sharedData?.schemes + .filter({ $0.name == scheme }) + .first + else { + XCTFail( + "The '\(scheme)' scheme doesn't exist.", + file: file, + line: line + ) + return + } + + guard let testableTarget = scheme.testAction?.testables + .filter({ $0.buildableReference.blueprintName == testTarget }) + .first + else { + XCTFail( + "The '\(testTarget)' testable target doesn't exist.", + file: file, + line: line + ) + return + } + + XCTAssertEqual( + testableTarget.locationScenarioReference?.identifier.contains(simulatedLocation), + true, + "The '\(testableTarget)' testable target doesn't have simulated location set.", + file: file, + line: line + ) + } } diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift index 6c8295598e3..9c7d068aa1c 100644 --- a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift @@ -1,8 +1,9 @@ // swiftlint:disable force_try -import TSCBasic +import Path import TuistCore -import TuistGraph @_exported import TuistKit +import XcodeGraph +import XcodeProj import XCTest @testable import TuistSupport @@ -16,23 +17,24 @@ open class TuistAcceptanceTestCase: XCTestCase { public var xcodeprojPath: AbsolutePath! public var workspacePath: AbsolutePath! public var fixturePath: AbsolutePath! - public var derivedDataPath: AbsolutePath! + public var derivedDataPath: AbsolutePath { derivedDataDirectory.path } public var environment: MockEnvironment! public var sourceRootPath: AbsolutePath! + private var derivedDataDirectory: TemporaryDirectory! private var fixtureTemporaryDirectory: TemporaryDirectory! - override open func setUp() { - super.setUp() + override open func setUp() async throws { + try await super.setUp() DispatchQueue.once(token: "io.tuist.test.logging") { LoggingSystem.bootstrap(AcceptanceTestCaseLogHandler.init) } - derivedDataPath = try! TemporaryDirectory(removeTreeOnDeinit: true).path - fixtureTemporaryDirectory = try! TemporaryDirectory(removeTreeOnDeinit: true) + derivedDataDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) + fixtureTemporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - sourceRootPath = try! AbsolutePath( + sourceRootPath = try AbsolutePath( validating: ProcessInfo.processInfo.environment[ "TUIST_CONFIG_SRCROOT" ]! @@ -41,7 +43,7 @@ open class TuistAcceptanceTestCase: XCTestCase { do { // Environment environment = try MockEnvironment() - Environment.shared = environment + Environment._shared.mutate { $0 = environment } } catch { XCTFail("Failed to setup environment") } @@ -52,12 +54,12 @@ open class TuistAcceptanceTestCase: XCTestCase { workspacePath = nil fixturePath = nil fixtureTemporaryDirectory = nil - derivedDataPath = nil + derivedDataDirectory = nil try await super.tearDown() } - public func setUpFixture(_ fixture: TuistAcceptanceFixtures) throws { + public func setUpFixture(_ fixture: TuistAcceptanceFixtures) async throws { let fixturesPath = sourceRootPath .appending(component: "fixtures") @@ -70,14 +72,31 @@ open class TuistAcceptanceTestCase: XCTestCase { } public func run(_ command: (some AsyncParsableCommand).Type, _ arguments: [String] = []) async throws { - let arguments = arguments + [ + let arguments = [ "--path", fixturePath.pathString, - ] + ] + arguments var parsedCommand = try command.parse(arguments) try await parsedCommand.run() } + public func run(_ command: InitCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + + public func run(_ command: InitCommand.Type, _ arguments: [String] = []) async throws { + fixturePath = fixtureTemporaryDirectory.path.appending( + component: arguments[arguments.firstIndex(where: { $0 == "--name" })! + 1] + ) + + let arguments = [ + "--path", fixturePath.pathString, + ] + arguments + + let parsedCommand = try command.parse(arguments) + try await parsedCommand.run() + } + public func run(_ command: RunCommand.Type, _ arguments: String...) async throws { try await run(command, arguments) } @@ -96,10 +115,10 @@ open class TuistAcceptanceTestCase: XCTestCase { } public func run(_ command: EditCommand.Type, _ arguments: [String] = []) async throws { - let arguments = arguments + [ + let arguments = [ "--path", fixturePath.pathString, "--permanent", - ] + ] + arguments let parsedCommand = try command.parse(arguments) try await parsedCommand.run() @@ -120,22 +139,24 @@ open class TuistAcceptanceTestCase: XCTestCase { } public func run(_ command: TestCommand.Type, _ arguments: [String] = []) async throws { - let arguments = arguments + [ + let arguments = [ "--derived-data-path", derivedDataPath.pathString, "--path", fixturePath.pathString, - ] + ] + arguments let parsedCommand = try command.parse(arguments) try await parsedCommand.run() } public func run(_ command: BuildCommand.Type, _ arguments: [String] = []) async throws { - let arguments = arguments + [ + let terminatorIndex = arguments.firstIndex(of: "--") ?? arguments.endIndex + let regularArguments = arguments.prefix(upTo: terminatorIndex) + let arguments = regularArguments + [ "--derived-data-path", derivedDataPath.pathString, "--path", fixturePath.pathString, - ] + ] + arguments.suffix(from: terminatorIndex) - let parsedCommand = try command.parse(arguments) + let parsedCommand = try command.parse(Array(arguments)) try await parsedCommand.run() } @@ -143,11 +164,29 @@ open class TuistAcceptanceTestCase: XCTestCase { try await run(command, arguments) } + public func run(_ command: ShareCommand.Type, _ arguments: [String] = []) async throws { + let arguments = [ + "--derived-data-path", derivedDataPath.pathString, + "--path", fixturePath.pathString, + ] + arguments + + let parsedCommand = try command.parse(arguments) + try await parsedCommand.run() + } + + public func run(_ command: ShareCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + + public func run(_ command: GenerateCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + public func run(_ command: GenerateCommand.Type, _ arguments: [String] = []) async throws { - let arguments = arguments + [ + let arguments = [ "--no-open", "--path", fixturePath.pathString, - ] + ] + arguments let parsedCommand = try command.parse(arguments) try await parsedCommand.run() @@ -163,11 +202,6 @@ open class TuistAcceptanceTestCase: XCTestCase { } public func run(_ command: (some ParsableCommand).Type, _ arguments: [String] = []) throws { - if String(describing: command) == "InitCommand" { - fixturePath = fixtureTemporaryDirectory.path.appending( - component: arguments[arguments.firstIndex(where: { $0 == "--name" })! + 1] - ) - } var parsedCommand = try command.parseAsRoot( arguments + ["--path", fixturePath.pathString] @@ -185,6 +219,48 @@ open class TuistAcceptanceTestCase: XCTestCase { contents += "\n" try FileHandler.shared.write(contents, path: filePath, atomically: true) } + + public func XCTAssertXCFrameworkLinked( + _ 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) + + guard try target.frameworksBuildPhase()?.files? + .contains(where: { $0.file?.nameOrPath == "\(framework).xcframework" }) == true + else { + XCTFail( + "Target \(targetName) doesn't link the xcframework \(framework)", + file: file, + line: line + ) + return + } + } + + public func XCTAssertXCFrameworkNotLinked( + _ 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) + + if try target.frameworksBuildPhase()?.files? + .contains(where: { $0.file?.nameOrPath == "\(framework).xcframework" }) == true + { + XCTFail( + "Target \(targetName) links the xcframework \(framework)", + file: file, + line: line + ) + return + } + } } // swiftlint:enable force_try diff --git a/Sources/TuistAnalytics/Backends/TuistAnalyticsBackend.swift b/Sources/TuistAnalytics/Backends/TuistAnalyticsBackend.swift index 513a413be4f..e5fad51eda1 100644 --- a/Sources/TuistAnalytics/Backends/TuistAnalyticsBackend.swift +++ b/Sources/TuistAnalytics/Backends/TuistAnalyticsBackend.swift @@ -1,4 +1,5 @@ import Foundation +import TuistCore /// An analytics backend an entity (e.g. an HTTP server) /// that can process analytics events generated by the Tuist CLI. diff --git a/Sources/TuistAnalytics/Log/Logger.swift b/Sources/TuistAnalytics/Log/Logger.swift new file mode 100644 index 00000000000..a4d4e6e2f0a --- /dev/null +++ b/Sources/TuistAnalytics/Log/Logger.swift @@ -0,0 +1,3 @@ +import TuistSupport + +let logger = Logger(label: "io.tuist.analytics") diff --git a/Sources/TuistAnalytics/Models/CommandEvent.swift b/Sources/TuistAnalytics/Models/CommandEvent.swift deleted file mode 100644 index ded8d4c20a2..00000000000 --- a/Sources/TuistAnalytics/Models/CommandEvent.swift +++ /dev/null @@ -1,62 +0,0 @@ -import AnyCodable -import Foundation -import TuistCore - -/// A `CommandEvent` is the analytics event to track the execution of a Tuist command -public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { - public let name: String - public let subcommand: String? - public let params: [String: AnyCodable] - public let commandArguments: [String] - public let durationInMs: Int - public let clientId: String - public let tuistVersion: String - public let swiftVersion: String - public let macOSVersion: String - public let machineHardwareName: String - public let isCI: Bool - - public let id = UUID() - public let date = Date() - public let dispatcherId = TuistAnalyticsDispatcher.dispatcherId - - private enum CodingKeys: String, CodingKey { - case name - case subcommand - case params - case commandArguments - case durationInMs = "duration" - case clientId - case tuistVersion - case swiftVersion - case macOSVersion = "macos_version" - case machineHardwareName - case isCI - } - - public init( - name: String, - subcommand: String?, - params: [String: AnyCodable], - commandArguments: [String], - durationInMs: Int, - clientId: String, - tuistVersion: String, - swiftVersion: String, - macOSVersion: String, - machineHardwareName: String, - isCI: Bool - ) { - self.name = name - self.subcommand = subcommand - self.params = params - self.commandArguments = commandArguments - self.durationInMs = durationInMs - self.clientId = clientId - self.tuistVersion = tuistVersion - self.swiftVersion = swiftVersion - self.macOSVersion = macOSVersion - self.machineHardwareName = machineHardwareName - self.isCI = isCI - } -} diff --git a/Sources/TuistAnalytics/TuistAnalytics.swift b/Sources/TuistAnalytics/TuistAnalytics.swift index 51349f2ffbd..3f401aa670c 100644 --- a/Sources/TuistAnalytics/TuistAnalytics.swift +++ b/Sources/TuistAnalytics/TuistAnalytics.swift @@ -1,14 +1,14 @@ import Foundation -import TSCBasic +import Path import TuistAsyncQueue -import TuistGraph import TuistLoader +import XcodeGraph public enum TuistAnalytics { public static func bootstrap(dispatcher: TuistAnalyticsDispatcher) throws { AsyncQueue.sharedInstance.register(dispatcher: dispatcher) - Task.detached(priority: .background) { - AsyncQueue.sharedInstance.start() // Re-try to send all events that got persisted and haven't been sent yet + Task { + await AsyncQueue.sharedInstance.start() // Re-try to send all events that got persisted and haven't been sent yet } } } diff --git a/Sources/TuistAnalytics/Utilities/TuistAnalyticsDispatcher.swift b/Sources/TuistAnalytics/Utilities/TuistAnalyticsDispatcher.swift index aed240cdba6..5c4bb45abae 100644 --- a/Sources/TuistAnalytics/Utilities/TuistAnalyticsDispatcher.swift +++ b/Sources/TuistAnalytics/Utilities/TuistAnalyticsDispatcher.swift @@ -1,10 +1,10 @@ import Foundation import TuistAsyncQueue import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -/// `TuistAnalyticsTagger` is responsible to send analytics events that gets stored and reported to the cloud backend (if defined) +/// `TuistAnalyticsTagger` is responsible to send analytics events that gets stored and reported to the Tuist server (if defined) public struct TuistAnalyticsDispatcher: AsyncQueueDispatching { public static let dispatcherId = "TuistAnalytics" @@ -18,16 +18,16 @@ public struct TuistAnalyticsDispatcher: AsyncQueueDispatching { public var identifier = TuistAnalyticsDispatcher.dispatcherId - public func dispatch(event: AsyncQueueEvent, completion: @escaping () throws -> Void) throws { + public func dispatch(event: AsyncQueueEvent, completion: @escaping () async throws -> Void) throws { guard let commandEvent = event as? CommandEvent else { return } - Task.detached { + Task { _ = try? await backend?.send(commandEvent: commandEvent) - try completion() + try await completion() } } - public func dispatchPersisted(data: Data, completion: @escaping () throws -> Void) throws { + public func dispatchPersisted(data: Data, completion: @escaping () async throws -> Void) throws { let decoder = JSONDecoder() let commandEvent = try decoder.decode(CommandEvent.self, from: data) return try dispatch(event: commandEvent, completion: completion) diff --git a/Sources/TuistAsyncQueue/AsyncQueue.swift b/Sources/TuistAsyncQueue/AsyncQueue.swift index 8f6323fc393..88ffe2a8fa6 100644 --- a/Sources/TuistAsyncQueue/AsyncQueue.swift +++ b/Sources/TuistAsyncQueue/AsyncQueue.swift @@ -51,8 +51,8 @@ public class AsyncQueue: AsyncQueuing { // MARK: - AsyncQueuing - public func start() { - loadEvents() + public func start() async { + await loadEvents() queue.resume() waitIfCI() } @@ -83,7 +83,7 @@ public class AsyncQueue: AsyncQueuing { logger.debug("Dispatching event with ID '\(event.id.uuidString)' to '\(dispatcher.identifier)'") do { try dispatcher.dispatch(event: event) { - try self.persistor.delete(event: event) + try await self.persistor.delete(event: event) operation.finish(success: true) } } catch { @@ -96,9 +96,9 @@ public class AsyncQueue: AsyncQueuing { } } - private func dispatchPersisted(eventTuple: AsyncQueueEventTuple) throws { + private func dispatchPersisted(eventTuple: AsyncQueueEventTuple) async throws { guard let dispatcher = dispatchers.first(where: { $0.key == eventTuple.dispatcherId })?.value else { - try deletePersistedEvent(filename: eventTuple.filename) + try await deletePersistedEvent(filename: eventTuple.filename) logger.error("Couldn't find dispatcher for persisted event with id: \(eventTuple.dispatcherId)") return } @@ -115,7 +115,7 @@ public class AsyncQueue: AsyncQueuing { do { logger.debug("Dispatching persisted event with ID '\(event.id.uuidString)' to '\(dispatcher.identifier)'") try dispatcher.dispatchPersisted(data: event.data) { - try self.deletePersistedEvent(filename: event.filename) + try await self.deletePersistedEvent(filename: event.filename) } } catch { logger.debug("Failed to dispatch persisted event with ID '\(event.id.uuidString)' to '\(dispatcher.identifier)'") @@ -123,18 +123,18 @@ public class AsyncQueue: AsyncQueuing { } } - private func loadEvents() { + private func loadEvents() async { do { - let events = try persistor.readAll() + let events = try await persistor.readAll() for event in events { - try dispatchPersisted(eventTuple: event) + try await dispatchPersisted(eventTuple: event) } } catch { logger.debug("Error loading persisted events: \(error)") } } - private func deletePersistedEvent(filename: String) throws { - try persistor.delete(filename: filename) + private func deletePersistedEvent(filename: String) async throws { + try await persistor.delete(filename: filename) } } diff --git a/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift b/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift index 056fe00dd77..8585e4ba769 100644 --- a/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift +++ b/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift @@ -1,5 +1,6 @@ +import FileSystem import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport @@ -8,7 +9,7 @@ public typealias AsyncQueueEventTuple = (dispatcherId: String, id: UUID, date: D public protocol AsyncQueuePersisting { /// Reads all the persisted events and returns them. - func readAll() throws -> [AsyncQueueEventTuple] + func readAll() async throws -> [AsyncQueueEventTuple] /// Persiss a given event. /// - Parameter event: Event to be persisted. @@ -16,11 +17,11 @@ public protocol AsyncQueuePersisting { /// Deletes the given event from disk. /// - Parameter event: Event to be deleted. - func delete(event: T) throws + func delete(event: T) async throws /// Deletes the given file name from disk. /// - Parameter filename: Name of the file to be deleted. - func delete(filename: String) throws + func delete(filename: String) async throws } final class AsyncQueuePersistor: AsyncQueuePersisting { @@ -28,11 +29,13 @@ final class AsyncQueuePersistor: AsyncQueuePersisting { let directory: AbsolutePath let jsonEncoder = JSONEncoder() + let fileSystem: FileSystem // MARK: - Init - init(directory: AbsolutePath = Environment.shared.queueDirectory) { + init(directory: AbsolutePath = Environment.shared.queueDirectory, fileSystem: FileSystem = FileSystem()) { self.directory = directory + self.fileSystem = fileSystem } func write(event: some AsyncQueueEvent) throws { @@ -42,17 +45,17 @@ final class AsyncQueuePersistor: AsyncQueuePersisting { try data.write(to: path.url) } - func delete(event: some AsyncQueueEvent) throws { - try delete(filename: filename(event: event)) + func delete(event: some AsyncQueueEvent) async throws { + try await delete(filename: filename(event: event)) } - func delete(filename: String) throws { + func delete(filename: String) async throws { let path = directory.appending(component: filename) guard FileHandler.shared.exists(path) else { return } - try FileHandler.shared.delete(path) + try await fileSystem.remove(path) } - func readAll() throws -> [AsyncQueueEventTuple] { + func readAll() async throws -> [AsyncQueueEventTuple] { let paths = FileHandler.shared.glob(directory, glob: "*.json") var events: [AsyncQueueEventTuple] = [] for eventPath in paths { @@ -64,7 +67,7 @@ final class AsyncQueuePersistor: AsyncQueuePersisting { else { /// Changing the naming convention is a breaking change. When detected /// we delete the event. - try? FileHandler.shared.delete(eventPath) + try? await fileSystem.remove(eventPath) continue } do { @@ -78,7 +81,7 @@ final class AsyncQueuePersistor: AsyncQueuePersisting { ) events.append(event) } catch { - try? FileHandler.shared.delete(eventPath) + try? await fileSystem.remove(eventPath) } } return events diff --git a/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift b/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift index 42fa5f23860..a2cd56033d7 100644 --- a/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift +++ b/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift @@ -26,7 +26,7 @@ public class MockAsyncQueueDispatcher: AsyncQueueDispatching { public var invokedDispatchParametersEventsList = [AsyncQueueEvent]() public var stubbedDispatchError: Error? - public func dispatch(event: AsyncQueueEvent, completion: @escaping () throws -> Void) throws { + public func dispatch(event: AsyncQueueEvent, completion: @escaping () async throws -> Void) throws { invokedDispatch = true invokedDispatchCount += 1 invokedDispatchParameterEvent = event @@ -36,7 +36,12 @@ public class MockAsyncQueueDispatcher: AsyncQueueDispatching { throw error } invokedDispatchCallBack() - try completion() + let semaphore = DispatchSemaphore(value: 0) + Task { + try await completion() + semaphore.signal() + } + semaphore.wait() } public var invokedDispatchPersisted = false @@ -46,7 +51,7 @@ public class MockAsyncQueueDispatcher: AsyncQueueDispatching { public var invokedDispatchPersistedParametersDataList = [Data]() public var stubbedDispatchPersistedError: Error? - public func dispatchPersisted(data: Data, completion: @escaping () throws -> Void) throws { + public func dispatchPersisted(data: Data, completion: @escaping () async throws -> Void) throws { invokedDispatchPersisted = true invokedDispatchPersistedCount += 1 invokedDispatchPersistedDataParameter = data @@ -56,6 +61,11 @@ public class MockAsyncQueueDispatcher: AsyncQueueDispatching { throw error } invokedDispatchPersistedCallBack() - try completion() + let semaphore = DispatchSemaphore(value: 0) + Task { + try await completion() + semaphore.signal() + } + semaphore.wait() } } diff --git a/Sources/TuistAutomation/ProjectMappers/SkipUITestsProjectMapper.swift b/Sources/TuistAutomation/ProjectMappers/SkipUITestsProjectMapper.swift index 7250e6c56cb..a62b7acef40 100644 --- a/Sources/TuistAutomation/ProjectMappers/SkipUITestsProjectMapper.swift +++ b/Sources/TuistAutomation/ProjectMappers/SkipUITestsProjectMapper.swift @@ -1,14 +1,16 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph public final class SkipUITestsProjectMapper: ProjectMapping { public init() {} public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { + logger.debug("Transforming project \(project.name): Pruning UI tests targets") + var project = project - project.targets = project.targets.map { target in + project.targets = project.targets.mapValues { target in var copy = target if copy.product == .uiTests { copy.prune = true diff --git a/Sources/TuistAutomation/ProjectMappers/SourceRootPathProjectMapper.swift b/Sources/TuistAutomation/ProjectMappers/SourceRootPathProjectMapper.swift index e4a0ebec173..0dd41a88f4b 100644 --- a/Sources/TuistAutomation/ProjectMappers/SourceRootPathProjectMapper.swift +++ b/Sources/TuistAutomation/ProjectMappers/SourceRootPathProjectMapper.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph /// Automation commands create their own project in temporary directory /// This means `SRCROOT` has a different path from the directory where `.xcodeproj` resides @@ -12,6 +12,8 @@ public final class SourceRootPathProjectMapper: ProjectMapping { public init() {} public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { + logger.debug("Transforming project \(project.name): Setting $SRCROOT to \(project.name)") + var project = project var base = project.settings.base // Keep the value if defined by user diff --git a/Sources/TuistAutomation/Utilities/AppBundle.swift b/Sources/TuistAutomation/Utilities/AppBundle.swift new file mode 100644 index 00000000000..714683e54dc --- /dev/null +++ b/Sources/TuistAutomation/Utilities/AppBundle.swift @@ -0,0 +1,133 @@ +import Path +import TuistSupport +import XcodeGraph + +public struct AppBundle: Equatable { + /// Path to the app bundle + public let path: AbsolutePath + + /// The app's Info.plist + public let infoPlist: InfoPlist + + enum InfoPlistError: FatalError { + case unknownPlatform(platform: String, app: String) + + var description: String { + switch self { + case let .unknownPlatform(platform: platform, app: app): + return "The \(app)'s supported platform \(platform) is unknown." + } + } + + var type: ErrorType { + switch self { + case .unknownPlatform: + return .abort + } + } + } + + public struct InfoPlist: Codable, Equatable { + /// App version number (e.g. 10.3) + public let version: Version + + /// Name of the app + public let name: String + + /// Bundle ID + public let bundleId: String + + /// Minimum OS version + public let minimumOSVersion: Version + + /// Supported simulator platforms. + /// Device is currently not supported. + public let supportedPlatforms: [SupportedPlatform] + + init( + version: Version, + name: String, + bundleId: String, + minimumOSVersion: Version, + supportedPlatforms: [SupportedPlatform] + ) { + self.version = version + self.name = name + self.bundleId = bundleId + self.minimumOSVersion = minimumOSVersion + self.supportedPlatforms = supportedPlatforms + } + + public enum SupportedPlatform: Codable, Equatable { + case simulator(Platform) + case device(Platform) + } + + enum CodingKeys: String, CodingKey { + case version = "CFBundleShortVersionString" + case name = "CFBundleName" + case bundleId = "CFBundleIdentifier" + case minimumOSVersion = "MinimumOSVersion" + case supportedPlatforms = "CFBundleSupportedPlatforms" + } + + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder + .container(keyedBy: AppBundle.InfoPlist.CodingKeys.self) + version = Version( + stringLiteral: try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.version) + ) + let name = try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.name) + self.name = name + bundleId = try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.bundleId) + minimumOSVersion = Version( + stringLiteral: try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.minimumOSVersion) + ) + supportedPlatforms = try container.decode([String].self, forKey: AppBundle.InfoPlist.CodingKeys.supportedPlatforms) + .map { platformSDK in + if let platform = Platform(commandLineValue: platformSDK) { + return .device(platform) + } else if let platform = Platform.allCases + .first(where: { platformSDK.lowercased() == $0.xcodeSimulatorSDK }) + { + return .simulator(platform) + } else { + throw InfoPlistError.unknownPlatform(platform: platformSDK, app: name) + } + } + } + } +} + +#if DEBUG + extension AppBundle { + public static func test( + path: AbsolutePath = try! AbsolutePath(validating: "/App.app"), // swiftlint:disable:this force_try + infoPlist: InfoPlist = .test() + ) -> Self { + .init( + path: path, + infoPlist: infoPlist + ) + } + } + + extension AppBundle.InfoPlist { + public static func test( + version: Version = Version("1.0"), + name: String = "App", + bundleId: String = "io.tuist.App", + minimumOSVersion: Version = Version("17.4"), + supportedPlatforms: [SupportedPlatform] = [.simulator(.iOS)] + ) -> Self { + .init( + version: version, + name: name, + bundleId: bundleId, + minimumOSVersion: minimumOSVersion, + supportedPlatforms: supportedPlatforms + ) + } + } + +#endif diff --git a/Sources/TuistAutomation/Utilities/AppBundleLoader.swift b/Sources/TuistAutomation/Utilities/AppBundleLoader.swift new file mode 100644 index 00000000000..9281d1d9fd0 --- /dev/null +++ b/Sources/TuistAutomation/Utilities/AppBundleLoader.swift @@ -0,0 +1,70 @@ +import FileSystem +import Foundation +import Mockable +import Path +import TuistSupport + +enum AppBundleLoaderError: FatalError, Equatable { + case missingInfoPlist(AbsolutePath) + case failedDecodingInfoPlist(AbsolutePath, String) + + var description: String { + switch self { + case let .missingInfoPlist(path): + return "Expected Info.plist at \(path) was not found. Make sure it exists." + case let .failedDecodingInfoPlist(path, reason): + return "Failed decoding Info.plist at \(path) due to: \(reason)" + } + } + + var type: ErrorType { + switch self { + case .missingInfoPlist, .failedDecodingInfoPlist: + return .abort + } + } +} + +@Mockable +public protocol AppBundleLoading { + func load(_ appBundle: AbsolutePath) async throws -> AppBundle +} + +public struct AppBundleLoader: AppBundleLoading { + private let fileSystem: FileSysteming + + public init() { + self.init( + fileSystem: FileSystem() + ) + } + + init( + fileSystem: FileSysteming + ) { + self.fileSystem = fileSystem + } + + public func load(_ appBundle: AbsolutePath) async throws -> AppBundle { + let infoPlistPath = appBundle.appending(component: "Info.plist") + + if try await !fileSystem.exists(infoPlistPath) { + throw AppBundleLoaderError.missingInfoPlist(infoPlistPath) + } + + let data = try Data(contentsOf: infoPlistPath.url) + let decoder = PropertyListDecoder() + + let infoPlist: AppBundle.InfoPlist + do { + infoPlist = try decoder.decode(AppBundle.InfoPlist.self, from: data) + } catch { + throw AppBundleLoaderError.failedDecodingInfoPlist(infoPlistPath, error.localizedDescription) + } + + return AppBundle( + path: appBundle, + infoPlist: infoPlist + ) + } +} diff --git a/Sources/TuistAutomation/Utilities/AppRunner.swift b/Sources/TuistAutomation/Utilities/AppRunner.swift new file mode 100644 index 00000000000..64f4f959b00 --- /dev/null +++ b/Sources/TuistAutomation/Utilities/AppRunner.swift @@ -0,0 +1,142 @@ +import FileSystem +import Foundation +import Mockable +import Path +import struct TSCUtility.Version +import TuistCore +import TuistSupport +import XcodeGraph + +enum AppRunnerError: FatalError, Equatable { + case invalidSimulatorPlatform(String) + case selectedPlatformNotFound(String) + + var description: String { + switch self { + case let .invalidSimulatorPlatform(platform): + "The chosen simulator's platform \(platform) is invalid" + case let .selectedPlatformNotFound(platform): + "No app bundle for the selected platform \(platform) was found." + } + } + + var type: ErrorType { + switch self { + case .invalidSimulatorPlatform: + return .abort + case .selectedPlatformNotFound: + return .bug + } + } +} + +@Mockable +public protocol AppRunning { + func runApp( + _ appBundles: [AppBundle], + version: Version?, + device: String? + ) async throws +} + +public final class AppRunner: AppRunning { + private let simulatorController: SimulatorControlling + private let userInputReader: UserInputReading + + public convenience init() { + self.init( + simulatorController: SimulatorController(), + userInputReader: UserInputReader() + ) + } + + init( + simulatorController: SimulatorControlling, + userInputReader: UserInputReading + ) { + self.simulatorController = simulatorController + self.userInputReader = userInputReader + } + + public func runApp( + _ appBundles: [AppBundle], + version: Version?, + device: String? + ) async throws { + let simulatorPlatforms: [Platform] = appBundles + .map(\.infoPlist) + .flatMap(\.supportedPlatforms) + .compactMap { + switch $0 { + case .device: + return nil + case let .simulator(platform): + return platform + } + } + + let platformsWithVersions: [Platform: Version] = appBundles.reduce([:]) { acc, appBundle in + var acc = acc + for supportedPlatform in appBundle.infoPlist.supportedPlatforms { + switch supportedPlatform { + case .device: + continue + case let .simulator(platform): + if let minimumVersion = acc[platform] { + acc[platform] = min( + minimumVersion, + Version(appBundle.infoPlist.minimumOSVersion) + ) + } else { + acc[platform] = Version(appBundle.infoPlist.minimumOSVersion) + } + } + } + return acc + } + + let devices = try await simulatorPlatforms.concurrentMap { platform in + try await self.simulatorController.findAvailableDevices( + platform: platform, + version: version, + minVersion: platformsWithVersions[platform], + deviceName: device + ) + } + .flatMap { $0 } + + let simulator: SimulatorDeviceAndRuntime + let bootedDevices = devices.filter { !$0.device.isShutdown } + if bootedDevices.count == 1, let bootedDevice = bootedDevices.first { + simulator = bootedDevice + } else { + simulator = try userInputReader.readValue( + asking: "Select the simulator device where you want to run the app:", + values: devices, + valueDescription: { "\($0.device.name) (\($0.device.udid))" } + ) + } + + guard let platformName = simulator.runtime.name + .components(separatedBy: " ").first, + let simulatorPlatform = Platform(commandLineValue: platformName) + else { throw AppRunnerError.invalidSimulatorPlatform(simulator.runtime.name) } + + guard let appBundle = appBundles.first(where: { + $0.infoPlist.supportedPlatforms.contains(.simulator(simulatorPlatform)) + }) + else { throw AppRunnerError.selectedPlatformNotFound(simulatorPlatform.caseValue) } + + logger.notice("Installing and launching \(appBundle.infoPlist.name) on \(simulator.device.name)") + let device = try simulatorController.booted(device: simulator.device) + try simulatorController.installApp(at: appBundle.path, device: device) + try simulatorController.launchApp(bundleId: appBundle.infoPlist.bundleId, device: device, arguments: []) + logger.notice("\(appBundle.infoPlist.name) was successfully launched 📲", metadata: .success) + } +} + +extension Version { + init(_ version: XcodeGraph.Version) { + self.init(version.major, version.minor, version.patch) + } +} diff --git a/Sources/TuistAutomation/Utilities/BuildGraphInspector.swift b/Sources/TuistAutomation/Utilities/BuildGraphInspector.swift index 78bc9d3e847..802ec5b7df0 100644 --- a/Sources/TuistAutomation/Utilities/BuildGraphInspector.swift +++ b/Sources/TuistAutomation/Utilities/BuildGraphInspector.swift @@ -1,9 +1,11 @@ import Foundation -import TSCBasic +import Mockable +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph +@Mockable public protocol BuildGraphInspecting { /// Returns the build arguments to be used with the given target. /// - Parameter project: Project whose build arguments will be returned. @@ -72,9 +74,16 @@ public final class BuildGraphInspector: BuildGraphInspecting { ) -> [XcodeBuildArgument] { var arguments = [XcodeBuildArgument]() + let configurations: [BuildConfiguration: Configuration?] + if let targetConfigurations = target.settings?.configurations, !targetConfigurations.isEmpty { + configurations = targetConfigurations + } else { + configurations = project.settings.configurations + } + // Configuration if let configuration { - if (target.settings ?? project.settings)?.configurations.first(where: { $0.key.name == configuration }) != nil { + if configurations.contains(where: { $0.key.name == configuration }) { arguments.append(.configuration(configuration)) } else { logger diff --git a/Sources/TuistAutomation/Utilities/Formatter.swift b/Sources/TuistAutomation/Utilities/Formatter.swift index ca95761e328..cdb3fbf69f0 100644 --- a/Sources/TuistAutomation/Utilities/Formatter.swift +++ b/Sources/TuistAutomation/Utilities/Formatter.swift @@ -8,22 +8,23 @@ protocol Formatting { } final class Formatter: Formatting { - private let parser: Parser + private let formatter: XCBeautifier - init() { - parser = Parser( - colored: Environment.shared.shouldOutputBeColoured, - renderer: Self.renderer(), + init(environment: Environmenting = Environment.shared) { + formatter = XCBeautifier( + colored: environment.shouldOutputBeColoured, + renderer: Self.renderer(for: environment), + preserveUnbeautifiedLines: false, additionalLines: { nil } ) } func format(_ line: String) -> String? { - parser.parse(line: line) + formatter.format(line: line) } - private static func renderer() -> Renderer { - if Environment.shared.isGitHubActions { + private static func renderer(for environment: Environmenting) -> Renderer { + if environment.isGitHubActions { return .gitHubActions } else { return .terminal diff --git a/Sources/TuistAutomation/Utilities/TargetBuilder.swift b/Sources/TuistAutomation/Utilities/TargetBuilder.swift index f3bffb32b51..2cb396887d2 100644 --- a/Sources/TuistAutomation/Utilities/TargetBuilder.swift +++ b/Sources/TuistAutomation/Utilities/TargetBuilder.swift @@ -1,8 +1,9 @@ -import TSCBasic +import FileSystem +import Path import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public protocol TargetBuilding { /// Builds a provided target. @@ -18,9 +19,10 @@ 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. + /// - passthroughXcodeBuildArguments: The passthrough xcodebuild arguments to pass to xcodebuild func buildTarget( _ target: GraphTarget, - platform: TuistGraph.Platform, + platform: XcodeGraph.Platform, workspacePath: AbsolutePath, scheme: Scheme, clean: Bool, @@ -28,9 +30,10 @@ public protocol TargetBuilding { buildOutputPath: AbsolutePath?, derivedDataPath: AbsolutePath?, device: String?, - osVersion: Version?, + osVersion: XcodeGraph.Version?, rosetta: Bool, - graphTraverser: GraphTraversing + graphTraverser: GraphTraversing, + passthroughXcodeBuildArguments: [String] ) async throws } @@ -62,22 +65,24 @@ public final class TargetBuilder: TargetBuilding { private let xcodeBuildController: XcodeBuildControlling private let xcodeProjectBuildDirectoryLocator: XcodeProjectBuildDirectoryLocating private let simulatorController: SimulatorControlling - + private let fileSystem: FileSystem public init( buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), xcodeBuildController: XcodeBuildControlling = XcodeBuildController(), xcodeProjectBuildDirectoryLocator: XcodeProjectBuildDirectoryLocating = XcodeProjectBuildDirectoryLocator(), - simulatorController: SimulatorControlling = SimulatorController() + simulatorController: SimulatorControlling = SimulatorController(), + fileSystem: FileSystem = FileSystem() ) { self.buildGraphInspector = buildGraphInspector self.xcodeBuildController = xcodeBuildController self.xcodeProjectBuildDirectoryLocator = xcodeProjectBuildDirectoryLocator self.simulatorController = simulatorController + self.fileSystem = fileSystem } public func buildTarget( _ target: GraphTarget, - platform: TuistGraph.Platform, + platform: XcodeGraph.Platform, workspacePath: AbsolutePath, scheme: Scheme, clean: Bool, @@ -85,9 +90,10 @@ public final class TargetBuilder: TargetBuilding { buildOutputPath: AbsolutePath?, derivedDataPath: AbsolutePath?, device: String?, - osVersion: Version?, + osVersion: XcodeGraph.Version?, rosetta: Bool, - graphTraverser: GraphTraversing + graphTraverser: GraphTraversing, + passthroughXcodeBuildArguments: [String] ) async throws { logger.log(level: .notice, "Building scheme \(scheme.name)", metadata: .section) @@ -102,7 +108,7 @@ public final class TargetBuilder: TargetBuilding { for: target.target, on: platform, scheme: scheme, - version: osVersion, + version: osVersion.map { try .init(versionString: $0.description) }, deviceName: device, graphTraverser: graphTraverser, simulatorController: simulatorController @@ -116,14 +122,14 @@ public final class TargetBuilder: TargetBuilding { rosetta: rosetta, derivedDataPath: derivedDataPath, clean: clean, - arguments: buildArguments + arguments: buildArguments, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) - .printFormattedOutput() if let buildOutputPath { let configuration = configuration ?? target.project.settings.defaultDebugBuildConfiguration()? .name ?? BuildConfiguration.debug.name - try copyBuildProducts( + try await copyBuildProducts( to: buildOutputPath, projectPath: workspacePath, derivedDataPath: derivedDataPath, @@ -137,9 +143,9 @@ public final class TargetBuilder: TargetBuilding { to outputPath: AbsolutePath, projectPath: AbsolutePath, derivedDataPath: AbsolutePath?, - platform: TuistGraph.Platform, + platform: XcodeGraph.Platform, configuration: String - ) throws { + ) async throws { let xcodeSchemeBuildPath = try xcodeProjectBuildDirectoryLocator.locate( platform: platform, projectPath: projectPath, @@ -156,15 +162,13 @@ public final class TargetBuilder: TargetBuilding { } logger.log(level: .notice, "Copying build products to \(buildOutputPath.pathString)", metadata: .subsection) - try FileHandler.shared - .contentsOfDirectory(xcodeSchemeBuildPath) - .forEach { product in - let productOutputPath = buildOutputPath.appending(component: product.basename) - if FileHandler.shared.exists(productOutputPath) { - try FileHandler.shared.delete(productOutputPath) - } - - try FileHandler.shared.copy(from: product, to: productOutputPath) + for product in try FileHandler.shared.contentsOfDirectory(xcodeSchemeBuildPath) { + let productOutputPath = buildOutputPath.appending(component: product.basename) + if FileHandler.shared.exists(productOutputPath) { + try await fileSystem.remove(productOutputPath) } + + try FileHandler.shared.copy(from: product, to: productOutputPath) + } } } diff --git a/Sources/TuistAutomation/Utilities/TargetRunner.swift b/Sources/TuistAutomation/Utilities/TargetRunner.swift index f950db370df..f0c82ad2308 100644 --- a/Sources/TuistAutomation/Utilities/TargetRunner.swift +++ b/Sources/TuistAutomation/Utilities/TargetRunner.swift @@ -1,8 +1,8 @@ -import TSCBasic +import Path import struct TSCUtility.Version import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public protocol TargetRunning { /// Runs a provided target. @@ -18,7 +18,7 @@ public protocol TargetRunning { /// - arguments: Arguments to forward to the runnable target when running. func runTarget( _ target: GraphTarget, - platform: TuistGraph.Platform, + platform: XcodeGraph.Platform, workspacePath: AbsolutePath, schemeName: String, configuration: String?, @@ -76,7 +76,7 @@ public final class TargetRunner: TargetRunning { public func runTarget( _ target: GraphTarget, - platform: TuistGraph.Platform, + platform: XcodeGraph.Platform, workspacePath: AbsolutePath, schemeName: String, configuration: String?, @@ -153,8 +153,12 @@ public final class TargetRunner: TargetRunning { let settings = try await xcodeBuildController .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) + let simulator = try await simulatorController.askForAvailableDevice( + platform: platform, + version: version, + minVersion: minVersion, + deviceName: deviceName + ) logger.debug("Running app \(appPath.pathString) with arguments [\(arguments.joined(separator: ", "))]") logger.notice("Running app \(bundleId) on \(simulator.device.name)", metadata: .section) diff --git a/Sources/TuistAutomation/Utilities/XcodeBuildDestination+Find.swift b/Sources/TuistAutomation/Utilities/XcodeBuildDestination+Find.swift index fa5b4d87c70..77ee073cc5f 100644 --- a/Sources/TuistAutomation/Utilities/XcodeBuildDestination+Find.swift +++ b/Sources/TuistAutomation/Utilities/XcodeBuildDestination+Find.swift @@ -1,8 +1,8 @@ -import TSCBasic +import Path import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph extension XcodeBuildDestination { /// Finds the `XcodeBuildDestination` that matches the arguments provided @@ -16,16 +16,16 @@ extension XcodeBuildDestination { /// - Returns: The `XcodeBuildDestination` that is compatible with the given arguments. public static func find( for target: Target, - on platform: TuistGraph.Platform, + on platform: XcodeGraph.Platform, scheme: Scheme, - version: Version?, + version: TSCUtility.Version?, deviceName: String?, graphTraverser: GraphTraversing, simulatorController: SimulatorControlling ) async throws -> XcodeBuildDestination { switch platform { case .iOS, .tvOS, .watchOS, .visionOS: - let minVersion: Version? + let minVersion: TSCUtility.Version? if let deploymentTargetVersion = target.deploymentTargets[platform] { minVersion = deploymentTargetVersion.version() } else { diff --git a/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift b/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift index d6ae9679203..789a0384c6c 100644 --- a/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift +++ b/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift @@ -1,11 +1,9 @@ -import Combine import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport public final class XcodeBuildController: XcodeBuildControlling { - // MARK: - Attributes /// Matches lines of the forms: @@ -19,7 +17,9 @@ public final class XcodeBuildController: XcodeBuildControlling { private let formatter: Formatting private let environment: Environmenting - + private let simulatorController: SimulatorController + private let system: Systeming + public convenience init() { self.init(formatter: Formatter(), environment: Environment.shared) } @@ -30,6 +30,8 @@ public final class XcodeBuildController: XcodeBuildControlling { ) { self.formatter = formatter self.environment = environment + self.simulatorController = SimulatorController() + self.system = System.shared } public func build( @@ -39,8 +41,9 @@ public final class XcodeBuildController: XcodeBuildControlling { rosetta: Bool, derivedDataPath: AbsolutePath?, clean: Bool = false, - arguments: [XcodeBuildArgument] - ) throws -> AsyncThrowingStream, Error> { + arguments: [XcodeBuildArgument], + passthroughXcodeBuildArguments: [String] + ) async throws { var command = ["/usr/bin/xcrun", "xcodebuild"] // Action @@ -58,6 +61,9 @@ public final class XcodeBuildController: XcodeBuildControlling { // Arguments command.append(contentsOf: arguments.flatMap(\.arguments)) + // Passthrough arguments + command.append(contentsOf: passthroughXcodeBuildArguments) + // Destination switch destination { case let .device(udid): @@ -67,17 +73,17 @@ public final class XcodeBuildController: XcodeBuildControlling { } command.append(contentsOf: ["-destination", value.joined(separator: ",")]) case .mac: - command.append(contentsOf: ["-destination", SimulatorController().macOSDestination()]) + command.append(contentsOf: ["-destination", simulatorController.macOSDestination()]) case nil: break } - + // Derived data path - if let derivedDataPath = derivedDataPath { + if let derivedDataPath { command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } - return try run(command: command) + try await run(command: command) } public func test( @@ -92,8 +98,9 @@ public final class XcodeBuildController: XcodeBuildControlling { retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration? - ) throws -> AsyncThrowingStream, Error> { + testPlanConfiguration: TestPlanConfiguration?, + passthroughXcodeBuildArguments: [String] + ) async throws { var command = ["/usr/bin/xcrun", "xcodebuild"] // Action @@ -111,6 +118,9 @@ public final class XcodeBuildController: XcodeBuildControlling { // Arguments command.append(contentsOf: arguments.flatMap(\.arguments)) + // Passthrough arguments + command.append(contentsOf: passthroughXcodeBuildArguments) + // Retry On Failure if retryCount > 0 { command.append(contentsOf: XcodeBuildArgument.retryCount(retryCount).arguments) @@ -125,16 +135,16 @@ public final class XcodeBuildController: XcodeBuildControlling { } command.append(contentsOf: ["-destination", value.joined(separator: ",")]) case .mac: - command.append(contentsOf: ["-destination", SimulatorController().macOSDestination()]) + command.append(contentsOf: ["-destination", simulatorController.macOSDestination()]) } // Derived data path - if let derivedDataPath = derivedDataPath { + if let derivedDataPath { command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } // Result bundle path - if let resultBundlePath = resultBundlePath { + if let resultBundlePath { command.append(contentsOf: ["-resultBundlePath", resultBundlePath.pathString]) } @@ -157,7 +167,7 @@ public final class XcodeBuildController: XcodeBuildControlling { } } - return try run(command: command) + try await run(command: command) } public func archive( @@ -167,7 +177,7 @@ public final class XcodeBuildController: XcodeBuildControlling { archivePath: AbsolutePath, arguments: [XcodeBuildArgument], derivedDataPath: AbsolutePath? - ) throws -> AsyncThrowingStream, Error> { + ) async throws { var command = ["/usr/bin/xcrun", "xcodebuild"] // Action @@ -186,30 +196,29 @@ public final class XcodeBuildController: XcodeBuildControlling { command.append(contentsOf: ["-archivePath", archivePath.pathString]) // Derived data path - if let derivedDataPath = derivedDataPath { + if let derivedDataPath { command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } - + // Arguments command.append(contentsOf: arguments.flatMap(\.arguments)) - return try run(command: command) + try await run(command: command) } public func createXCFramework( - arguments: [XcodeBuildControllerCreateXCFrameworkArgument], + arguments: [String], output: AbsolutePath - ) throws -> AsyncThrowingStream, Error> { + ) async throws { var command = ["/usr/bin/xcrun", "xcodebuild", "-create-xcframework"] - command.append(contentsOf: arguments.flatMap(\.xcodebuildArguments)) + command.append(contentsOf: arguments) command.append(contentsOf: ["-output", output.pathString]) command.append("-allow-internal-distribution") - - return try run(command: command) + + try await run(command: command) } enum ShowBuildSettingsError: Error { - // swiftformat:disable trailingCommas case timeout } @@ -228,91 +237,126 @@ public final class XcodeBuildController: XcodeBuildControlling { command.append(contentsOf: ["-scheme", scheme]) // Derived data path - if let derivedDataPath = derivedDataPath { + if let derivedDataPath { command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } - + // Target command.append(contentsOf: target.xcodebuildArguments) - - let values = System.shared.publisher(command) - .mapToString() - .collectAndMergeOutput() - // xcodebuild has a bug where xcodebuild -showBuildSettings - // can sometimes hang indefinitely on projects that don't - // share any schemes, so automatically bail out if it looks - // like that's happening. - .timeout(.seconds(20), scheduler: DispatchQueue.main, customError: { ShowBuildSettingsError.timeout }) - .retry(5) - .values + + let buildSettings = try await loadBuildSettings(command) + var buildSettingsByTargetName = [String: XcodeBuildSettings]() - for try await string in values { - var currentSettings: [String: String] = [:] - var currentTarget: String? - - let flushTarget = { () in - if let currentTarget = currentTarget { - let buildSettings = XcodeBuildSettings( - currentSettings, - target: currentTarget, - configuration: configuration - ) - buildSettingsByTargetName[buildSettings.target] = buildSettings - } - - currentTarget = nil - currentSettings = [:] + var currentSettings: [String: String] = [:] + var currentTarget: String? + + func flushTarget() { + if let currentTarget { + let buildSettings = XcodeBuildSettings( + currentSettings, + target: currentTarget, + configuration: configuration + ) + buildSettingsByTargetName[buildSettings.target] = buildSettings } + + currentTarget = nil + currentSettings = [:] + } - string.enumerateLines { line, _ in - if let result = XcodeBuildController.targetSettingsRegex.firstMatch( - in: line, - range: NSRange(line.startIndex..., in: line) - ) { - let targetRange = Range(result.range(at: 1), in: line)! - - flushTarget() - currentTarget = String(line[targetRange]) - return - } - - let trimSet = CharacterSet.whitespacesAndNewlines - let components = line - .split(maxSplits: 1) { $0 == "=" } - .map { $0.trimmingCharacters(in: trimSet) } - - if components.count == 2 { - currentSettings[components[0]] = components[1] - } + buildSettings.enumerateLines { line, _ in + if let result = XcodeBuildController.targetSettingsRegex.firstMatch( + in: line, + range: NSRange(line.startIndex..., in: line) + ) { + let targetRange = Range(result.range(at: 1), in: line)! + + flushTarget() + currentTarget = String(line[targetRange]) + return + } + + let trimSet = CharacterSet.whitespacesAndNewlines + let components = line + .split(maxSplits: 1) { $0 == "=" } + .map { $0.trimmingCharacters(in: trimSet) } + + if components.count == 2 { + currentSettings[components[0]] = components[1] } - flushTarget() } + + flushTarget() + return buildSettingsByTargetName } - fileprivate func run(command: [String]) throws -> AsyncThrowingStream, Error> { + fileprivate func run(command: [String]) async throws { + func format(_ bytes: [UInt8]) -> String { + let string = String(decoding: bytes, as: Unicode.UTF8.self) + if self.environment.isVerbose == true { + return string + } else { + return self.format(string) + } + } - 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 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 self?.environment.isVerbose == true { - return SystemEvent.standardOutput(XcodeBuildOutput(raw: line)) - } else { - return SystemEvent.standardOutput(XcodeBuildOutput(raw: self?.formatter.format(line) ?? "")) - } + func log(_ bytes: [UInt8], isError: Bool = false) { + let lines = format(bytes).split(separator: "\n") + for line in lines where !line.isEmpty { + if isError { + logger.error("\(line)") + } else { + logger.notice("\(line)") } } - .eraseToAnyPublisher() - .stream + } + + logger.debug("Running xcodebuild command: \(command.joined(separator: " "))") + + try system.run(command, + verbose: false, + environment: system.env, + redirection: .stream(stdout: { bytes in + log(bytes) + }, stderr: { bytes in + log(bytes, isError: true) + })) + + } + + private func loadBuildSettings(_ command: [String]) async throws -> String { + // xcodebuild has a bug where xcodebuild -showBuildSettings + // can sometimes hang indefinitely on projects that don't + // share any schemes, so automatically bail out if it looks + // like that's happening. + return try await Task.retrying(maxRetryCount: 5) { + let systemTask = Task { + return try await self.system.runAndCollectOutput(command).standardOutput + } + + let timeoutTask = Task { + try await Task.sleep(nanoseconds: 20_000_000) + systemTask.cancel() + } + + let result = try await systemTask.value + timeoutTask.cancel() + return result + }.value + } +} + +// MARK: - Helpers + +fileprivate extension XcodeBuildController { + func format(_ multiLineText: String) -> String { + multiLineText.split(separator: "\n").map { + let line = String($0) + let formattedLine = formatter.format(line) + + return formattedLine ?? "" + } + .joined(separator: "\n") } } diff --git a/Sources/TuistAutomationTesting/Utilities/MockBuildGraphInspector.swift b/Sources/TuistAutomationTesting/Utilities/MockBuildGraphInspector.swift deleted file mode 100644 index 1d27448b70f..00000000000 --- a/Sources/TuistAutomationTesting/Utilities/MockBuildGraphInspector.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistCoreTesting -import TuistGraph - -@testable import TuistAutomation - -public final class MockBuildGraphInspector: BuildGraphInspecting { - public init() {} - public var workspacePathStub: ((AbsolutePath) -> AbsolutePath?)? - public func workspacePath(directory: AbsolutePath) -> AbsolutePath? { - workspacePathStub?(directory) ?? nil - } - - public var buildableTargetStub: ((Scheme, GraphTraversing) -> GraphTarget?)? - public func buildableTarget(scheme: Scheme, graphTraverser: GraphTraversing) -> GraphTarget? { - if let buildableTargetStub { - return buildableTargetStub(scheme, graphTraverser) - } else { - return GraphTarget.test() - } - } - - public var buildableSchemesStub: ((GraphTraversing) -> [Scheme])? - public func buildableSchemes(graphTraverser: GraphTraversing) -> [Scheme] { - if let buildableSchemesStub { - return buildableSchemesStub(graphTraverser) - } else { - return [] - } - } - - public var buildableEntrySchemesStub: ((GraphTraversing) -> [Scheme])? - public func buildableEntrySchemes(graphTraverser: GraphTraversing) -> [Scheme] { - buildableEntrySchemesStub?(graphTraverser) ?? [] - } - - public var buildArgumentsStub: ((Project, Target, String?, Bool) -> [XcodeBuildArgument])? - public func buildArguments( - project: Project, - target: Target, - configuration: String?, - skipSigning: Bool - ) -> [XcodeBuildArgument] { - if let buildArgumentsStub { - return buildArgumentsStub(project, target, configuration, skipSigning) - } else { - return [] - } - } - - public var testableTargetStub: ((Scheme, String?, [TestIdentifier], [TestIdentifier], GraphTraversing) -> GraphTarget?)? - public func testableTarget( - scheme: Scheme, - testPlan: String?, - testTargets: [TestIdentifier], - skipTestTargets: [TestIdentifier], - graphTraverser: GraphTraversing - ) -> GraphTarget? { - if let testableTargetStub { - return testableTargetStub(scheme, testPlan, testTargets, skipTestTargets, graphTraverser) - } else { - return GraphTarget.test() - } - } - - public var testableSchemesStub: ((GraphTraversing) -> [Scheme])? - public func testableSchemes(graphTraverser: GraphTraversing) -> [Scheme] { - if let testableSchemesStub { - return testableSchemesStub(graphTraverser) - } else { - return [] - } - } - - public var testSchemesStub: ((GraphTraversing) -> [Scheme])? - public func testSchemes(graphTraverser: GraphTraversing) -> [Scheme] { - testSchemesStub?(graphTraverser) ?? [] - } - - public var runnableTargetStub: ((Scheme, GraphTraversing) -> GraphTarget?)? - public func runnableTarget(scheme: Scheme, graphTraverser: GraphTraversing) -> GraphTarget? { - runnableTargetStub?(scheme, graphTraverser) - } - - public var runnableSchemesStub: ((GraphTraversing) -> [Scheme])? - public func runnableSchemes(graphTraverser: GraphTraversing) -> [Scheme] { - runnableSchemesStub?(graphTraverser) ?? [] - } - - public var workspaceSchemesStub: ((GraphTraversing) -> [Scheme])? - public func workspaceSchemes(graphTraverser: GraphTraversing) -> [Scheme] { - workspaceSchemesStub?(graphTraverser) ?? [] - } -} diff --git a/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift b/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift index c5ddb03599d..9d18d42e939 100644 --- a/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift +++ b/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift @@ -1,8 +1,8 @@ -import TSCBasic +import Path import TSCUtility import TuistAutomation import TuistCore -import TuistGraph +import XcodeGraph public final class MockTargetBuilder: TargetBuilding { public init() {} @@ -16,14 +16,15 @@ public final class MockTargetBuilder: TargetBuilding { AbsolutePath?, AbsolutePath?, String?, - Version?, + XcodeGraph.Version?, Bool, - GraphTraversing + GraphTraversing, + [String] ) throws -> Void)? public func buildTarget( _ target: GraphTarget, - platform _: TuistGraph.Platform, + platform _: XcodeGraph.Platform, workspacePath: AbsolutePath, scheme: Scheme, clean: Bool, @@ -31,9 +32,10 @@ public final class MockTargetBuilder: TargetBuilding { buildOutputPath: AbsolutePath?, derivedDataPath: AbsolutePath?, device: String?, - osVersion: Version?, + osVersion: XcodeGraph.Version?, rosetta: Bool, - graphTraverser: GraphTraversing + graphTraverser: GraphTraversing, + passthroughXcodeBuildArguments: [String] ) throws { try buildTargetStub?( target, @@ -46,7 +48,8 @@ public final class MockTargetBuilder: TargetBuilding { device, osVersion, rosetta, - graphTraverser + graphTraverser, + passthroughXcodeBuildArguments ) } } diff --git a/Sources/TuistAutomationTesting/Utilities/MockTargetRunner.swift b/Sources/TuistAutomationTesting/Utilities/MockTargetRunner.swift index 641e8546d16..f8c750db285 100644 --- a/Sources/TuistAutomationTesting/Utilities/MockTargetRunner.swift +++ b/Sources/TuistAutomationTesting/Utilities/MockTargetRunner.swift @@ -1,7 +1,7 @@ -import TSCBasic +import Path import struct TSCUtility.Version import TuistAutomation -import TuistGraph +import XcodeGraph public final class MockTargetRunner: TargetRunning { public init() {} @@ -12,7 +12,7 @@ public final class MockTargetRunner: TargetRunning { )? public func runTarget( _ target: GraphTarget, - platform _: TuistGraph.Platform, + platform _: XcodeGraph.Platform, workspacePath: AbsolutePath, schemeName: String, configuration: String?, diff --git a/Sources/TuistCache/CacheGraphContentHasher.swift b/Sources/TuistCache/CacheGraphContentHasher.swift new file mode 100644 index 00000000000..9a685ee3755 --- /dev/null +++ b/Sources/TuistCache/CacheGraphContentHasher.swift @@ -0,0 +1,116 @@ +import Foundation +import Mockable +import Path +import TuistCore +import TuistHasher +import TuistSupport +import XcodeGraph + +@Mockable +public protocol CacheGraphContentHashing { + /// Hashes graph + /// - Parameters: + /// - graph: Graph to hash + /// - configuration: Configuration to hash. + /// - config: The `Config.swift` model + /// - excludedTargets: Targets to be excluded from hashes calculation + func contentHashes( + for graph: Graph, + configuration: String?, + config: TuistCore.Config, + excludedTargets: Set + ) throws -> [GraphTarget: String] +} + +public final class CacheGraphContentHasher: CacheGraphContentHashing { + private let graphContentHasher: GraphContentHashing + private let contentHasher: ContentHashing + private let versionFetcher: CacheVersionFetching + private static let cachableProducts: Set = [.framework, .staticFramework, .bundle, .macro] + private let defaultConfigurationFetcher: DefaultConfigurationFetching + private let xcodeController: XcodeControlling + private let swiftVersionProvider: SwiftVersionProviding + + public convenience init( + contentHasher: ContentHashing = ContentHasher() + ) { + self.init( + graphContentHasher: GraphContentHasher(contentHasher: contentHasher), + contentHasher: contentHasher, + versionFetcher: CacheVersionFetcher(), + defaultConfigurationFetcher: DefaultConfigurationFetcher(), + xcodeController: XcodeController.shared, + swiftVersionProvider: SwiftVersionProvider.shared + ) + } + + init( + graphContentHasher: GraphContentHashing, + contentHasher: ContentHashing, + versionFetcher: CacheVersionFetching, + defaultConfigurationFetcher: DefaultConfigurationFetching, + xcodeController: XcodeControlling, + swiftVersionProvider: SwiftVersionProviding + ) { + self.graphContentHasher = graphContentHasher + self.contentHasher = contentHasher + self.versionFetcher = versionFetcher + self.defaultConfigurationFetcher = defaultConfigurationFetcher + self.xcodeController = xcodeController + self.swiftVersionProvider = swiftVersionProvider + } + + public func contentHashes( + for graph: Graph, + configuration: String?, + config: TuistCore.Config, + excludedTargets: Set + ) throws -> [GraphTarget: String] { + let graphTraverser = GraphTraverser(graph: graph) + let version = versionFetcher.version() + let configuration = try defaultConfigurationFetcher.fetch( + configuration: configuration, + config: config, + graph: graph + ) + + let hashes = try graphContentHasher.contentHashes( + for: graph, + include: { + self.isGraphTargetHashable( + $0, + graphTraverser: graphTraverser, + excludedTargets: excludedTargets + ) + }, + additionalStrings: [ + configuration, + try swiftVersionProvider.swiftlangVersion(), + version.rawValue, + xcodeController.selectedVersion().xcodeStringValue, + ] + ) + return hashes + } + + private func isGraphTargetHashable( + _ target: GraphTarget, + graphTraverser: GraphTraversing, + excludedTargets: Set + ) -> Bool { + let product = target.target.product + let name = target.target.name + + /** The second condition is to exclude the resources bundle associated to the given target name */ + let isExcluded = excludedTargets.contains(name) || excludedTargets + .contains(target.target.name.dropPrefix("\(target.project.name)_")) + let dependsOnXCTest = graphTraverser.dependsOnXCTest(path: target.path, name: name) + let isHashableProduct = CacheGraphContentHasher.cachableProducts.contains(product) + + return isHashableProduct && !isExcluded && !dependsOnXCTest + } + + private func isMacro(_ target: GraphTarget, graphTraverser: GraphTraversing) -> Bool { + !graphTraverser.directSwiftMacroExecutables(path: target.path, name: target.target.name).isEmpty + } +} diff --git a/Sources/TuistCache/CacheVersionFetcher.swift b/Sources/TuistCache/CacheVersionFetcher.swift new file mode 100644 index 00000000000..9fad9975b28 --- /dev/null +++ b/Sources/TuistCache/CacheVersionFetcher.swift @@ -0,0 +1,26 @@ +import Foundation +import XcodeGraph + +protocol CacheVersionFetching { + func version() -> CacheVersion +} + +enum CacheVersion: String, Equatable, Hashable { + /** + This is the first version that we introduced that used frameworks, bundles, and xcframeworks when developers opted into it. + However: + - It did not support multi-platform targetes + - The solution to support multiple architectures, xcframeworks, was not suitable for this problem, causing compilation issues. + */ + case version1 = "1.0.0" + /** + This version was introduced to support multi-platform caching and drop support for xcframeworks. + */ + case version2 = "2" +} + +struct CacheVersionFetcher: CacheVersionFetching { + func version() -> CacheVersion { + .version2 + } +} diff --git a/Sources/TuistCore/Analytics/CommandEvent.swift b/Sources/TuistCore/Analytics/CommandEvent.swift new file mode 100644 index 00000000000..907751240b7 --- /dev/null +++ b/Sources/TuistCore/Analytics/CommandEvent.swift @@ -0,0 +1,109 @@ +import AnyCodable +import Foundation +import Path + +/// A `CommandEvent` is the analytics event to track the execution of a Tuist command +public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { + public let runId: String + public let name: String + public let subcommand: String? + public let params: [String: AnyCodable] + public let commandArguments: [String] + public let durationInMs: Int + public let clientId: String + public let tuistVersion: String + public let swiftVersion: String + public let macOSVersion: String + public let machineHardwareName: String + public let isCI: Bool + public let status: Status + + public enum Status: Codable, Equatable { + case success, failure(String) + } + + public let id = UUID() + public let date = Date() + public let dispatcherId = "TuistAnalytics" + + private enum CodingKeys: String, CodingKey { + case runId + case name + case subcommand + case params + case commandArguments + case durationInMs = "duration" + case clientId + case tuistVersion + case swiftVersion + case macOSVersion = "macos_version" + case machineHardwareName + case isCI + case status + } + + public init( + runId: String, + name: String, + subcommand: String?, + params: [String: AnyCodable], + commandArguments: [String], + durationInMs: Int, + clientId: String, + tuistVersion: String, + swiftVersion: String, + macOSVersion: String, + machineHardwareName: String, + isCI: Bool, + status: Status + ) { + self.runId = runId + self.name = name + self.subcommand = subcommand + self.params = params + self.commandArguments = commandArguments + self.durationInMs = durationInMs + self.clientId = clientId + self.tuistVersion = tuistVersion + self.swiftVersion = swiftVersion + self.macOSVersion = macOSVersion + self.machineHardwareName = machineHardwareName + self.isCI = isCI + self.status = status + } +} + +#if MOCKING + extension CommandEvent { + public static func test( + runId: String = "", + name: String = "generate", + subcommand: String? = nil, + params: [String: AnyCodable] = [:], + commandArguments: [String] = [], + durationInMs: Int = 20, + clientId: String = "123", + tuistVersion: String = "1.2.3", + swiftVersion: String = "5.2", + macOSVersion: String = "10.15", + machineHardwareName: String = "arm64", + status: Status = .success + ) -> CommandEvent { + CommandEvent( + runId: runId, + name: name, + subcommand: subcommand, + params: params, + commandArguments: commandArguments, + durationInMs: durationInMs, + clientId: clientId, + tuistVersion: tuistVersion, + swiftVersion: swiftVersion, + macOSVersion: macOSVersion, + machineHardwareName: machineHardwareName, + isCI: false, + status: status + ) + } + } +#endif diff --git a/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift b/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift index 56386145239..c9427f97c2e 100644 --- a/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift +++ b/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift @@ -7,9 +7,9 @@ public protocol AsyncQueueDispatching { /// Dispatches a given event. /// - Parameter event: Event to be dispatched. - func dispatch(event: AsyncQueueEvent, completion: @escaping () throws -> Void) throws + func dispatch(event: AsyncQueueEvent, completion: @escaping () async throws -> Void) throws /// Dispatch a persisted event. /// - Parameter data: Serialized data of the event. - func dispatchPersisted(data: Data, completion: @escaping () throws -> Void) throws + func dispatchPersisted(data: Data, completion: @escaping () async throws -> Void) throws } diff --git a/Sources/TuistCore/Automation/Extensions/AsyncThrowingStream+XcodeBuildOutput.swift b/Sources/TuistCore/Automation/Extensions/AsyncThrowingStream+XcodeBuildOutput.swift deleted file mode 100644 index 33d8c20062a..00000000000 --- a/Sources/TuistCore/Automation/Extensions/AsyncThrowingStream+XcodeBuildOutput.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import TuistSupport - -extension AsyncThrowingStream where Element == SystemEvent { - public func printFormattedOutput() async throws { - for try await element in self { - switch element { - case let .standardError(error): - let lines = error.raw.split(separator: "\n") - for line in lines where !line.isEmpty { - logger.error("\(line)") - } - case let .standardOutput(output): - let lines = output.raw.split(separator: "\n") - for line in lines where !line.isEmpty { - logger.info("\(line)") - } - } - } - } -} diff --git a/Sources/TuistCore/Automation/XcodeBuildArgument.swift b/Sources/TuistCore/Automation/XcodeBuildArgument.swift index c360e4de78a..e4514b0daa1 100644 --- a/Sources/TuistCore/Automation/XcodeBuildArgument.swift +++ b/Sources/TuistCore/Automation/XcodeBuildArgument.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// It represents arguments that can be passed to the xcodebuild command. public enum XcodeBuildArgument: Equatable, CustomStringConvertible { diff --git a/Sources/TuistCore/Automation/XcodeBuildControlling.swift b/Sources/TuistCore/Automation/XcodeBuildControlling.swift index f30affcb1af..249b0b3bbca 100644 --- a/Sources/TuistCore/Automation/XcodeBuildControlling.swift +++ b/Sources/TuistCore/Automation/XcodeBuildControlling.swift @@ -1,5 +1,6 @@ import Foundation -import TSCBasic +import Mockable +import Path import TuistSupport public enum XcodeBuildDestination: Equatable { @@ -7,61 +8,7 @@ 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)] - } - } -} - +@Mockable public protocol XcodeBuildControlling { /// Returns an observable to build the given project using xcodebuild. /// - Parameters: @@ -71,6 +18,7 @@ public protocol XcodeBuildControlling { /// to determine the destination. /// - clean: True if xcodebuild should clean the project before building. /// - arguments: Extra xcodebuild arguments. + /// - passthroughXcodeBuildArguments: Passthrough xcodebuild arguments. func build( _ target: XcodeBuildTarget, scheme: String, @@ -78,8 +26,9 @@ public protocol XcodeBuildControlling { rosetta: Bool, derivedDataPath: AbsolutePath?, clean: Bool, - arguments: [XcodeBuildArgument] - ) throws -> AsyncThrowingStream, Error> + arguments: [XcodeBuildArgument], + passthroughXcodeBuildArguments: [String] + ) async throws /// Returns an observable to test the given project using xcodebuild. /// - Parameters: @@ -93,6 +42,7 @@ 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 + /// - passthroughXcodeBuildArguments: Passthrough xcodebuild arguments. func test( _ target: XcodeBuildTarget, scheme: String, @@ -105,8 +55,9 @@ public protocol XcodeBuildControlling { retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration? - ) throws -> AsyncThrowingStream, Error> + testPlanConfiguration: TestPlanConfiguration?, + passthroughXcodeBuildArguments: [String] + ) async throws /// Returns an observable that archives the given project using xcodebuild. /// - Parameters: @@ -123,17 +74,16 @@ public protocol XcodeBuildControlling { archivePath: AbsolutePath, arguments: [XcodeBuildArgument], derivedDataPath: AbsolutePath? - ) throws -> AsyncThrowingStream, Error> + ) async throws /// Creates an .xcframework combining the list of given frameworks. /// - Parameters: /// - arguments: A set of arguments to configure the XCFramework creation. /// - output: Path to the output .xcframework. func createXCFramework( - arguments: [XcodeBuildControllerCreateXCFrameworkArgument], + arguments: [String], output: AbsolutePath - ) - throws -> AsyncThrowingStream, Error> + ) async throws /// Gets the build settings of a scheme targets. /// - Parameters: diff --git a/Sources/TuistCore/Automation/XcodeBuildSettings.swift b/Sources/TuistCore/Automation/XcodeBuildSettings.swift index fecf8c62fba..ebfee4426e2 100644 --- a/Sources/TuistCore/Automation/XcodeBuildSettings.swift +++ b/Sources/TuistCore/Automation/XcodeBuildSettings.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path public struct XcodeBuildSettings { public typealias DictionaryType = [String: String] diff --git a/Sources/TuistCore/Automation/XcodeBuildTarget.swift b/Sources/TuistCore/Automation/XcodeBuildTarget.swift index fd0e2b39db9..5a02d7dede6 100644 --- a/Sources/TuistCore/Automation/XcodeBuildTarget.swift +++ b/Sources/TuistCore/Automation/XcodeBuildTarget.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path public enum XcodeBuildTarget: Equatable { /// The target is an Xcode project. diff --git a/Sources/TuistCore/Cache/CacheCategory.swift b/Sources/TuistCore/Cache/CacheCategory.swift index 96017a4f602..4835628af8b 100644 --- a/Sources/TuistCore/Cache/CacheCategory.swift +++ b/Sources/TuistCore/Cache/CacheCategory.swift @@ -3,12 +3,6 @@ public enum CacheCategory: String, CaseIterable, RawRepresentable { /// The plugins cache. case plugins - /// The build cache - case builds - - /// The tests cache - case tests - /// The projects generated for automation tasks cache case generatedAutomationProjects @@ -18,20 +12,41 @@ public enum CacheCategory: String, CaseIterable, RawRepresentable { /// The manifests cache case manifests + /// The edit projects cache + case editProjects + + /// The Tuist Runs cache + case runs + + /// The Tuist Binaries cache + case binaries + + /// The Tuist Selective Tests cache + case selectiveTests + public var directoryName: String { switch self { case .plugins: return "Plugins" - case .builds: - return "BuildCache" - case .tests: - return "incremental-tests" case .generatedAutomationProjects: return "Projects" case .projectDescriptionHelpers: return "ProjectDescriptionHelpers" case .manifests: return "Manifests" + case .editProjects: + return "EditProjects" + case .runs: + return "Runs" + case .binaries: + return "Binaries" + case .selectiveTests: + return "SelectiveTests" } } } + +public enum RemoteCacheCategory { + case binaries + case selectiveTests +} diff --git a/Sources/TuistCore/Cache/CacheDirectoriesProvider.swift b/Sources/TuistCore/Cache/CacheDirectoriesProvider.swift index 4e4da345f38..05b680d2dbb 100644 --- a/Sources/TuistCore/Cache/CacheDirectoriesProvider.swift +++ b/Sources/TuistCore/Cache/CacheDirectoriesProvider.swift @@ -1,32 +1,44 @@ import Foundation -import TSCBasic -import TuistGraph +import Mockable +import Path import TuistSupport +import XcodeGraph +@Mockable public protocol CacheDirectoriesProviding { - /// Returns the cache directory for a cache category - func cacheDirectory(for category: CacheCategory) -> AbsolutePath + /// Returns the cache directory for a Tuist cache category + func cacheDirectory(for category: CacheCategory) throws -> AbsolutePath + func cacheDirectory() -> AbsolutePath } public final class CacheDirectoriesProvider: CacheDirectoriesProviding { - public let cacheDirectory: AbsolutePath - // swiftlint:disable:next force_try - private static let defaultDirectory = try! AbsolutePath(validating: URL(fileURLWithPath: NSHomeDirectory()).path) - .appending(component: ".tuist") - private static var forcedCacheDirectory: AbsolutePath? { - ProcessInfo.processInfo.environment[Constants.EnvironmentVariables.forceConfigCacheDirectory] - .map { try! AbsolutePath(validating: $0) } // swiftlint:disable:this force_try + private let fileHandler: FileHandling + private let environment: Environmenting + + init( + fileHandler: FileHandling, + environment: Environmenting + ) { + self.fileHandler = fileHandler + self.environment = environment } - public init(config: Config?) { - if let cacheDirectory = config?.cache?.path { - self.cacheDirectory = cacheDirectory - } else { - cacheDirectory = CacheDirectoriesProvider.defaultDirectory.appending(component: "Cache") - } + public convenience init() { + self.init( + fileHandler: FileHandler.shared, + environment: Environment.shared + ) } - public func cacheDirectory(for category: CacheCategory) -> AbsolutePath { - (Self.forcedCacheDirectory ?? cacheDirectory).appending(component: category.directoryName) + public func cacheDirectory(for category: CacheCategory) throws -> AbsolutePath { + cacheDirectory().appending(components: ["tuist", category.directoryName]) + } + + public func cacheDirectory() -> Path.AbsolutePath { + if let cacheDirectory = environment.cacheDirectory { + return cacheDirectory + } else { + return FileHandler.shared.homeDirectory.appending(components: ".cache") + } } } diff --git a/Sources/TuistCore/Cache/CacheDirectoriesProviderFactory.swift b/Sources/TuistCore/Cache/CacheDirectoriesProviderFactory.swift index 80116518e16..fcc59c1a825 100644 --- a/Sources/TuistCore/Cache/CacheDirectoriesProviderFactory.swift +++ b/Sources/TuistCore/Cache/CacheDirectoriesProviderFactory.swift @@ -1,17 +1,19 @@ import Foundation -import TuistGraph +import Mockable import TuistSupport +import XcodeGraph +@Mockable public protocol CacheDirectoriesProviderFactoring { - func cacheDirectories(config: Config?) throws -> CacheDirectoriesProviding + func cacheDirectories() throws -> CacheDirectoriesProviding } public final class CacheDirectoriesProviderFactory: CacheDirectoriesProviderFactoring { public init() {} - public func cacheDirectories(config: Config?) throws -> CacheDirectoriesProviding { - let provider = CacheDirectoriesProvider(config: config) + public func cacheDirectories() throws -> CacheDirectoriesProviding { + let provider = CacheDirectoriesProvider() for category in CacheCategory.allCases { - let directory = provider.cacheDirectory(for: category) + let directory = try provider.cacheDirectory(for: category) if !FileHandler.shared.exists(directory) { try FileHandler.shared.createFolder(directory) } diff --git a/Sources/TuistCore/Cache/CacheOutputType.swift b/Sources/TuistCore/Cache/CacheOutputType.swift deleted file mode 100644 index 2cd72c0c526..00000000000 --- a/Sources/TuistCore/Cache/CacheOutputType.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -/// An enum that represents the type of output that the caching feature can work with. -public enum CacheOutputType: CustomStringConvertible, Equatable { - /// Resource bundle built for the simulator - case bundle - - /// Frameworks built for the simulator. - case framework - - /// XCFrameworks built for the simulator and/or device. - case xcframework(CacheXCFrameworkDestination) - - public var description: String { - switch self { - case .bundle: - return "bundle" - case .framework: - return "framework" - case let .xcframework(destination): - switch destination { - case [.device, .simulator]: - return "xcframework" - case .device: - return "device-xcframework" - case .simulator: - return "simulator-xcframework" - default: - fatalError("xcframework should contain at least one destination") - } - } - } -} diff --git a/Sources/TuistCore/Cache/CacheXCFrameworkDestination.swift b/Sources/TuistCore/Cache/CacheXCFrameworkDestination.swift deleted file mode 100644 index 33ef789a3ca..00000000000 --- a/Sources/TuistCore/Cache/CacheXCFrameworkDestination.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -/// An enum that represents the type of xcframeworks output -public struct CacheXCFrameworkDestination: OptionSet { - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let device = CacheXCFrameworkDestination(rawValue: 1 << 0) - public static let simulator = CacheXCFrameworkDestination(rawValue: 1 << 1) -} diff --git a/Sources/TuistCore/Cache/FileContentHashing.swift b/Sources/TuistCore/Cache/FileContentHashing.swift index 8795493f48a..ca68fab0468 100644 --- a/Sources/TuistCore/Cache/FileContentHashing.swift +++ b/Sources/TuistCore/Cache/FileContentHashing.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path public protocol FileContentHashing { func hash(path: AbsolutePath) throws -> String diff --git a/Sources/TuistCore/Cloud/CloudClienting.swift b/Sources/TuistCore/Cloud/CloudClienting.swift deleted file mode 100644 index cc81ce8257b..00000000000 --- a/Sources/TuistCore/Cloud/CloudClienting.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import TuistSupport - -public protocol CloudClienting { - func request(_ resource: HTTPResource) async throws -> (object: T, response: HTTPURLResponse) -} diff --git a/Sources/TuistCore/Cloud/CloudResponse.swift b/Sources/TuistCore/Cloud/CloudResponse.swift deleted file mode 100644 index 3f9d929eabf..00000000000 --- a/Sources/TuistCore/Cloud/CloudResponse.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public struct CloudResponse: Decodable { - public let status: String - public let data: T - - public init(status: String, data: T) { - self.status = status - self.data = data - } -} - -public struct CloudEmptyResponse: Decodable { - public init() {} -} diff --git a/Sources/TuistCore/Cloud/CloudResponseError.swift b/Sources/TuistCore/Cloud/CloudResponseError.swift deleted file mode 100644 index 2a3eda429ac..00000000000 --- a/Sources/TuistCore/Cloud/CloudResponseError.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import TuistSupport - -public struct CloudResponseError: Decodable, LocalizedError, Equatable { - public var status: String - public var errors: [Error]? - - public struct Error: Decodable, Equatable { - var code: String - var message: String - } - - public var errorDescription: String? { - errors?.map { $0.message.capitalizingFirstLetter() }.joined(separator: "\n") - } -} - -public struct CloudEmptyResponseError: Decodable, LocalizedError, Equatable { - public init() {} -} diff --git a/Sources/TuistCore/ContentHashing/ContentHasher.swift b/Sources/TuistCore/ContentHashing/ContentHasher.swift index aec292ccf45..004060d17e9 100644 --- a/Sources/TuistCore/ContentHashing/ContentHasher.swift +++ b/Sources/TuistCore/ContentHashing/ContentHasher.swift @@ -1,6 +1,6 @@ import CryptoKit import Foundation -import TSCBasic +import Path import TuistSupport /// `ContentHasher` diff --git a/Sources/TuistCore/ContentHashing/ContentHashing.swift b/Sources/TuistCore/ContentHashing/ContentHashing.swift index 8aae45e16f9..4a318319d5b 100644 --- a/Sources/TuistCore/ContentHashing/ContentHashing.swift +++ b/Sources/TuistCore/ContentHashing/ContentHashing.swift @@ -1,8 +1,12 @@ import Foundation +import Mockable +import Path +@Mockable public protocol ContentHashing: FileContentHashing { func hash(_ data: Data) throws -> String func hash(_ string: String) throws -> String func hash(_ strings: [String]) throws -> String func hash(_ dictionary: [String: String]) throws -> String + func hash(path filePath: AbsolutePath) throws -> String } diff --git a/Sources/TuistCore/ContentHashing/ContentHashingError.swift b/Sources/TuistCore/ContentHashing/ContentHashingError.swift index bc0779bbcf0..ee095188b9c 100644 --- a/Sources/TuistCore/ContentHashing/ContentHashingError.swift +++ b/Sources/TuistCore/ContentHashing/ContentHashingError.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport /// `ContentHashingError` diff --git a/Sources/TuistCore/ContentHashing/HashingFilesFilter.swift b/Sources/TuistCore/ContentHashing/HashingFilesFilter.swift index c1031e2e576..a71d9e858bb 100644 --- a/Sources/TuistCore/ContentHashing/HashingFilesFilter.swift +++ b/Sources/TuistCore/ContentHashing/HashingFilesFilter.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path class HashingFilesFilter { /// an array of filters, which should return if a path should be included in hashing calculations or not. diff --git a/Sources/TuistCore/Descriptors/DirectoryDescriptor.swift b/Sources/TuistCore/Descriptors/DirectoryDescriptor.swift index ce831ab73e2..794c7c6ea24 100644 --- a/Sources/TuistCore/Descriptors/DirectoryDescriptor.swift +++ b/Sources/TuistCore/Descriptors/DirectoryDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// Directory Descriptor /// diff --git a/Sources/TuistCore/Descriptors/FileDescriptor.swift b/Sources/TuistCore/Descriptors/FileDescriptor.swift index 5aab4563677..37cfac0fd1d 100644 --- a/Sources/TuistCore/Descriptors/FileDescriptor.swift +++ b/Sources/TuistCore/Descriptors/FileDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// File Descriptor /// diff --git a/Sources/TuistCore/Descriptors/SideEffectDescriptor.swift b/Sources/TuistCore/Descriptors/SideEffectDescriptor.swift index d7a42f30883..45e2e82f171 100644 --- a/Sources/TuistCore/Descriptors/SideEffectDescriptor.swift +++ b/Sources/TuistCore/Descriptors/SideEffectDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// Side Effect Descriptor /// diff --git a/Sources/TuistCore/Graph/CircularDependencyLinter.swift b/Sources/TuistCore/Graph/CircularDependencyLinter.swift index 31810672805..9f4bf94b366 100644 --- a/Sources/TuistCore/Graph/CircularDependencyLinter.swift +++ b/Sources/TuistCore/Graph/CircularDependencyLinter.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph // MARK: - CircularDependencyLinting @@ -40,7 +40,7 @@ public class CircularDependencyLinter: CircularDependencyLinting { } cache.add(project: project) - for target in project.targets { + for target in project.targets.values.sorted() { try lintTarget( path: path, name: target.name, diff --git a/Sources/TuistCore/Graph/ConditionCache.swift b/Sources/TuistCore/Graph/ConditionCache.swift index 5a7e0d8b81d..406a35fa76b 100644 --- a/Sources/TuistCore/Graph/ConditionCache.swift +++ b/Sources/TuistCore/Graph/ConditionCache.swift @@ -1,6 +1,6 @@ import Foundation -import TuistGraph import TuistSupport +import XcodeGraph /// Cache designed to store `PlatformCondition.CombinationResult` for `GraphTraverser` final class ConditionCache { diff --git a/Sources/TuistCore/Graph/GraphCircularDetector.swift b/Sources/TuistCore/Graph/GraphCircularDetector.swift index 96adb1037b9..1694589fe0d 100644 --- a/Sources/TuistCore/Graph/GraphCircularDetector.swift +++ b/Sources/TuistCore/Graph/GraphCircularDetector.swift @@ -1,8 +1,9 @@ import Foundation +import Path import TSCBasic struct GraphCircularDetectorNode: Hashable { - let path: AbsolutePath + let path: Path.AbsolutePath let name: String } diff --git a/Sources/TuistCore/Graph/GraphDependencyReference.swift b/Sources/TuistCore/Graph/GraphDependencyReference.swift index 60eb1a71d37..f6794d1afdd 100644 --- a/Sources/TuistCore/Graph/GraphDependencyReference.swift +++ b/Sources/TuistCore/Graph/GraphDependencyReference.swift @@ -1,11 +1,11 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph public enum GraphDependencyReference: Equatable, Comparable, Hashable { var condition: PlatformCondition? { switch self { - case let .framework(_, _, _, _, _, _, _, _, _, condition), + case let .framework(_, _, _, _, _, _, _, _, condition), let .library(_, _, _, _, condition), let .xcframework(_, _, _, _, _, condition), let .bundle(_, condition), @@ -36,7 +36,6 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { case framework( path: AbsolutePath, binaryPath: AbsolutePath, - isCarthage: Bool, dsymPath: AbsolutePath?, bcsymbolmapPaths: [AbsolutePath], linking: BinaryLinking, @@ -51,11 +50,10 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { init(_ dependency: GraphDependency, condition: PlatformCondition? = nil) { switch dependency { - case let .framework(path, binaryPath, dsymPath, bcsymbolmapPaths, linking, architectures, isCarthage, status): + case let .framework(path, binaryPath, dsymPath, bcsymbolmapPaths, linking, architectures, status): self = .framework( path: path, binaryPath: binaryPath, - isCarthage: isCarthage, dsymPath: dsymPath, bcsymbolmapPaths: bcsymbolmapPaths, linking: linking, @@ -94,7 +92,7 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { /// this attribute returns the path to them. public var precompiledPath: AbsolutePath? { switch self { - case let .framework(path, _, _, _, _, _, _, _, _, _): + case let .framework(path, _, _, _, _, _, _, _, _): return path case let .library(path, _, _, _, _): return path @@ -138,7 +136,6 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { case .framework( path: let path, binaryPath: _, - isCarthage: _, dsymPath: _, bcsymbolmapPaths: _, linking: _, diff --git a/Sources/TuistCore/Graph/GraphLoader.swift b/Sources/TuistCore/Graph/GraphLoader.swift index da0c2e97799..f32738141af 100644 --- a/Sources/TuistCore/Graph/GraphLoader.swift +++ b/Sources/TuistCore/Graph/GraphLoader.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph // MARK: - GraphLoading @@ -55,7 +55,6 @@ public final class GraphLoader: GraphLoading { workspace: updatedWorkspace, projects: cache.loadedProjects, packages: cache.packages, - targets: cache.loadedTargets, dependencies: cache.dependencies, dependencyConditions: cache.dependencyConditions ) @@ -76,7 +75,7 @@ public final class GraphLoader: GraphLoading { } cache.add(project: project) - for target in project.targets { + for target in project.targets.values { try loadTarget( path: path, name: target.name, @@ -178,6 +177,7 @@ public final class GraphLoader: GraphLoading { source: .system ) } + case let .package(product, type, _): switch type { case .macro: @@ -187,6 +187,7 @@ public final class GraphLoader: GraphLoading { case .plugin: return try loadPackage(fromPath: path, productName: product, type: .plugin) } + case .xctest: return try platforms.map { platform in try loadXCTestSDK(platform: platform) @@ -214,7 +215,6 @@ public final class GraphLoader: GraphLoading { bcsymbolmapPaths: metadata.bcsymbolmapPaths, linking: metadata.linking, architectures: metadata.architectures, - isCarthage: metadata.isCarthage, status: metadata.status ) cache.add(framework: framework, at: path) @@ -323,9 +323,7 @@ public final class GraphLoader: GraphLoading { init(projects: [Project]) { let allProjects = Dictionary(uniqueKeysWithValues: projects.map { ($0.path, $0) }) - let allTargets = allProjects.mapValues { - Dictionary(uniqueKeysWithValues: $0.targets.map { ($0.name, $0) }) - } + let allTargets = allProjects.mapValues { $0.targets } self.allProjects = allProjects self.allTargets = allTargets } diff --git a/Sources/TuistCore/Graph/GraphLoadingError.swift b/Sources/TuistCore/Graph/GraphLoadingError.swift index 272912d0d5b..016261b63fd 100644 --- a/Sources/TuistCore/Graph/GraphLoadingError.swift +++ b/Sources/TuistCore/Graph/GraphLoadingError.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport enum GraphLoadingError: FatalError, Equatable { diff --git a/Sources/TuistCore/Graph/GraphTraverser.swift b/Sources/TuistCore/Graph/GraphTraverser.swift index 91bb0717aab..26e4a25c16b 100644 --- a/Sources/TuistCore/Graph/GraphTraverser.swift +++ b/Sources/TuistCore/Graph/GraphTraverser.swift @@ -1,16 +1,25 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import func TSCBasic.topologicalSort +import func TSCBasic.transitiveClosure import TuistSupport +import XcodeGraph // swiftlint:disable type_body_length public class GraphTraverser: GraphTraversing { public var name: String { graph.name } public var hasPackages: Bool { !graph.packages.flatMap(\.value).isEmpty } - public var path: AbsolutePath { graph.path } + public var path: Path.AbsolutePath { graph.path } public var workspace: Workspace { graph.workspace } - public var projects: [AbsolutePath: Project] { graph.projects } - public var targets: [AbsolutePath: [String: Target]] { graph.targets } + public var projects: [Path.AbsolutePath: Project] { graph.projects } + + /// It returns the targets of the graph projects. + /// - Returns: A dictionary where the key is the path to the project, and the value are a dictionary where + /// the keys are the name of the targets, and the value the target representation. + public func targets() -> [Path.AbsolutePath: [String: Target]] { + return projects.mapValues { $0.targets } + } + public var dependencies: [GraphDependency: Set] { graph.dependencies } private let graph: Graph @@ -67,20 +76,20 @@ public class GraphTraverser: GraphTraversing { projects.values.flatMap(\.schemes) + graph.workspace.schemes } - public func precompiledFrameworksPaths() -> Set { + public func precompiledFrameworksPaths() -> Set { let dependencies = graph.dependencies.reduce(into: Set()) { acc, next in acc.formUnion([next.key]) acc.formUnion(next.value) } - return Set(dependencies.compactMap { dependency -> AbsolutePath? in - guard case let GraphDependency.framework(path, _, _, _, _, _, _, _) = dependency else { return nil } + return Set(dependencies.compactMap { dependency -> Path.AbsolutePath? in + guard case let GraphDependency.framework(path, _, _, _, _, _, _) = dependency else { return nil } return path }) } public func targets(product: Product) -> Set { var filteredTargets: Set = Set() - for (path, projectTargets) in targets { + for (path, projectTargets) in targets() { projectTargets.values.forEach { target in guard target.product == product else { return } guard let project = projects[path] else { return } @@ -90,34 +99,39 @@ public class GraphTraverser: GraphTraversing { return filteredTargets } - public func target(path: AbsolutePath, name: String) -> GraphTarget? { - guard let project = graph.projects[path], let target = graph.targets[path]?[name] else { return nil } + public func target(path: Path.AbsolutePath, name: String) -> GraphTarget? { + guard let project = graph.projects[path], + let target = project.targets[name] else { return nil } return GraphTarget(path: path, target: target, project: project) } - public func targets(at path: AbsolutePath) -> Set { + public func targets(at path: Path.AbsolutePath) -> Set { guard let project = graph.projects[path] else { return Set() } - guard let targets = graph.targets[path] else { return [] } - return Set(targets.values.map { GraphTarget(path: path, target: $0, project: project) }) + return Set(project.targets.values.map { GraphTarget(path: path, target: $0, project: project) }) } public func testPlan(name: String) -> TestPlan? { allTestPlans().first { $0.name == name } } - public func allTargetDependencies(path: AbsolutePath, name: String) -> Set { + public func allTargetDependencies(path: Path.AbsolutePath, name: String) -> Set { guard let target = target(path: path, name: name) else { return [] } - return transitiveClosure([target]) { target in + return allTargetDependencies(traversingFromTargets: [target]) + } + + public func allTargetDependencies(traversingFromTargets: [GraphTarget]) -> Set { + return transitiveClosure(traversingFromTargets) { target in Array( directTargetDependencies( path: target.path, name: target.target.name ) - ).map(\.graphTarget) + ) + .map(\.graphTarget) } } - public func directTargetDependencies(path: AbsolutePath, name: String) -> Set { + public func directTargetDependencies(path: Path.AbsolutePath, name: String) -> Set { let target = GraphDependency.target(name: name, path: path) guard let dependencies = graph.dependencies[target] else { return [] } @@ -128,7 +142,7 @@ public class GraphTraverser: GraphTraversing { return Set(convertToGraphTargetReferences(targetDependencies, for: target)) } - public func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set { + public func directLocalTargetDependencies(path: Path.AbsolutePath, name: String) -> Set { let target = GraphDependency.target(name: name, path: path) guard let dependencies = graph.dependencies[target], graph.projects[path] != nil @@ -142,11 +156,11 @@ public class GraphTraverser: GraphTraversing { } func convertToGraphTargetReferences( - _ dependencies: [(name: String, path: AbsolutePath)], + _ dependencies: [(name: String, path: Path.AbsolutePath)], for target: GraphDependency ) -> [GraphTargetReference] { dependencies.compactMap { dependencyName, dependencyPath -> GraphTargetReference? in - guard let projectDependencies = graph.targets[dependencyPath], + guard let projectDependencies = graph.projects[dependencyPath]?.targets, let dependencyTarget = projectDependencies[dependencyName], let dependencyProject = graph.projects[dependencyPath] else { @@ -158,8 +172,8 @@ public class GraphTraverser: GraphTraversing { } } - public func resourceBundleDependencies(path: AbsolutePath, name: String) -> Set { - guard let target = graph.targets[path]?[name] else { return [] } + public func resourceBundleDependencies(path: Path.AbsolutePath, name: String) -> Set { + guard let target = graph.projects[path]?.targets[name] else { return [] } guard target.supportsResources else { return [] } let canHostResources: (GraphDependency) -> Bool = { @@ -168,23 +182,39 @@ public class GraphTraverser: GraphTraversing { let bundles = filterDependencies( from: .target(name: name, path: path), - test: isDependencyResourceBundle, + test: { dependency in + isDependencyResourceBundle(dependency: dependency) && !isDependencyExternal(dependency) + }, skip: canHostResources ) + // External bundles are copied only to targets that can embed products to follow SPM logic. + // This prevents scenarios when a bundle is copied to a dynamic framework and the SPM targets then can't find it. + // See this issue for more detalis: https://github.com/tuist/tuist/pull/6565 + let externalBundles = filterDependencies( + from: .target(name: name, path: path), + test: { dependency in + isDependencyResourceBundle(dependency: dependency) && isDependencyExternal(dependency) && + canEmbedProducts(target: target) + }, + skip: canDependencyEmbedProducts + ) - return Set(bundles.compactMap { dependencyReference(to: $0, from: .target(name: name, path: path)) }) + return Set( + bundles.union(externalBundles) + .compactMap { dependencyReference(to: $0, from: .target(name: name, path: path)) } + ) } public func target(from dependency: GraphDependency) -> GraphTarget? { guard case let GraphDependency.target(name, path) = dependency else { return nil } - guard let target = graph.targets[path]?[name] else { return nil } + guard let target = graph.projects[path]?.targets[name] else { return nil } guard let project = graph.projects[path] else { return nil } return GraphTarget(path: path, target: target, project: project) } - public func appExtensionDependencies(path: AbsolutePath, name: String) -> Set { + public func appExtensionDependencies(path: Path.AbsolutePath, name: String) -> Set { let validProducts: [Product] = [ .appExtension, .stickerPackExtension, .watch2Extension, .tvTopShelfExtension, .messagesExtension, ] @@ -194,7 +224,7 @@ public class GraphTraverser: GraphTraversing { ) } - public func extensionKitExtensionDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { + public func extensionKitExtensionDependencies(path: Path.AbsolutePath, name: String) -> Set { let validProducts: [Product] = [ .extensionKitExtension, ] @@ -204,12 +234,12 @@ public class GraphTraverser: GraphTraversing { ) } - public func appClipDependencies(path: AbsolutePath, name: String) -> GraphTargetReference? { + public func appClipDependencies(path: Path.AbsolutePath, name: String) -> GraphTargetReference? { directLocalTargetDependencies(path: path, name: name) .first { $0.target.product == .appClip } } - public func buildsForMacCatalyst(path: AbsolutePath, name: String) -> Bool { + public func buildsForMacCatalyst(path: Path.AbsolutePath, name: String) -> Bool { guard target(path: path, name: name)?.target.supportsCatalyst ?? false else { return false } @@ -225,12 +255,12 @@ public class GraphTraverser: GraphTraversing { } // Filter based on edges - public func directStaticDependencies(path: AbsolutePath, name: String) -> Set { + public func directStaticDependencies(path: Path.AbsolutePath, name: String) -> Set { Set( graph.dependencies[.target(name: name, path: path)]? .compactMap { (dependency: GraphDependency) -> GraphDependencyReference? in guard case let GraphDependency.target(dependencyName, dependencyPath) = dependency, - let target = graph.targets[dependencyPath]?[dependencyName], + let target = graph.projects[dependencyPath]?.targets[dependencyName], target.product.isStatic else { return nil @@ -245,7 +275,7 @@ public class GraphTraverser: GraphTraversing { ) } - public func embeddableFrameworks(path: AbsolutePath, name: String) -> Set { + public func embeddableFrameworks(path: Path.AbsolutePath, name: String) -> Set { guard let target = target(path: path, name: name), canEmbedProducts(target: target.target) else { return Set() } var references: Set = Set([]) @@ -294,18 +324,18 @@ public class GraphTraverser: GraphTraversing { return references } - public func searchablePathDependencies(path: AbsolutePath, name: String) throws -> Set { + public func searchablePathDependencies(path: Path.AbsolutePath, name: String) throws -> Set { try linkableDependencies(path: path, name: name, shouldExcludeHostAppDependencies: false) .union(staticPrecompiledFrameworksDependencies(path: path, name: name)) } - public func linkableDependencies(path: AbsolutePath, name: String) throws -> Set { + public func linkableDependencies(path: Path.AbsolutePath, name: String) throws -> Set { try linkableDependencies(path: path, name: name, shouldExcludeHostAppDependencies: true) } // swiftlint:disable:next function_body_length public func linkableDependencies( - path: AbsolutePath, + path: Path.AbsolutePath, name: String, shouldExcludeHostAppDependencies: Bool ) throws -> Set { @@ -358,14 +388,37 @@ public class GraphTraverser: GraphTraversing { .filter(\.isPrecompiled) let precompiledDependencies = precompiled - .flatMap { filterDependencies(from: $0) - } + .flatMap { filterDependencies(from: $0) } - let precompiledLibrariesAndFrameworks = Set(precompiled + precompiledDependencies) + let precompiledDynamicLibrariesAndFrameworks = Set(precompiled + precompiledDependencies) .filter(\.isPrecompiledDynamicAndLinkable) + + let staticXCFrameworksLinkedByDynamicXCFrameworkDependencies = filterDependencies( + from: Set(precompiledDynamicLibrariesAndFrameworks).filter { $0.xcframeworkDependency != nil }, + test: { $0.xcframeworkDependency?.linking == .static }, + skip: { $0.xcframeworkDependency == nil } + ) + + let libraryDependenciesLinkedByStaticXCFrameworks = try staticXCFrameworksLinkedByDynamicXCFrameworkDependencies.flatMap { + guard let dependencies = dependencies[$0] else { return [GraphDependency]() } + return try dependencies.filter { + switch $0 { + case .sdk: + return true + default: + return false + } + } + } + + let precompiledLibrariesAndFrameworks = + ( + precompiledDynamicLibrariesAndFrameworks + staticXCFrameworksLinkedByDynamicXCFrameworkDependencies + + libraryDependenciesLinkedByStaticXCFrameworks + ) .compactMap { dependencyReference(to: $0, from: targetGraphDependency) } - references.formUnion(precompiledLibrariesAndFrameworks) + references.formUnion(Set(precompiledLibrariesAndFrameworks)) // Static libraries and frameworks / Static libraries' dynamic libraries if target.target.canLinkStaticProducts() { @@ -422,7 +475,7 @@ public class GraphTraverser: GraphTraversing { return references } - public func copyProductDependencies(path: AbsolutePath, name: String) -> Set { + public func copyProductDependencies(path: Path.AbsolutePath, name: String) -> Set { guard let target = target(path: path, name: name) else { return Set() } var dependencies = Set() @@ -437,7 +490,7 @@ public class GraphTraverser: GraphTraversing { return Set(dependencies) } - public func directSwiftMacroExecutables(path: AbsolutePath, name: String) -> Set { + public func directSwiftMacroExecutables(path: Path.AbsolutePath, name: String) -> Set { let dependencies = directTargetDependencies(path: path, name: name) .filter { $0.target.product == .macro } .map { GraphDependencyReference.product( @@ -449,39 +502,44 @@ public class GraphTraverser: GraphTraversing { return Set(dependencies) } - public func directSwiftMacroTargets(path: AbsolutePath, name: String) -> Set { + public func directSwiftMacroTargets(path: Path.AbsolutePath, name: String) -> Set { let dependencies = directTargetDependencies(path: path, name: name) .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) + public func allSwiftMacroTargets(path: Path.AbsolutePath, name: String) -> Set { + var 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 } + + if let target = target(path: path, name: name), !directSwiftMacroExecutables(path: path, name: name).isEmpty { + dependencies.insert(target) + } + return Set(dependencies) } - public func librariesPublicHeadersFolders(path: AbsolutePath, name: String) -> Set { + public func librariesPublicHeadersFolders(path: Path.AbsolutePath, name: String) -> Set { let dependencies = graph.dependencies[.target(name: name, path: path), default: []] - let libraryPublicHeaders = dependencies.compactMap { dependency -> AbsolutePath? in + let libraryPublicHeaders = dependencies.compactMap { dependency -> Path.AbsolutePath? in guard case let GraphDependency.library(_, publicHeaders, _, _, _) = dependency else { return nil } return publicHeaders } return Set(libraryPublicHeaders) } - public func librariesSearchPaths(path: AbsolutePath, name: String) throws -> Set { + public func librariesSearchPaths(path: Path.AbsolutePath, name: String) throws -> Set { let directDependencies = graph.dependencies[.target(name: name, path: path), default: []] - let directDependenciesLibraryPaths = directDependencies.compactMap { dependency -> AbsolutePath? in + let directDependenciesLibraryPaths = directDependencies.compactMap { dependency -> Path.AbsolutePath? in guard case let GraphDependency.library(path, _, _, _, _) = dependency else { return nil } return path } // In addition to any directly linked libraries, search paths for any transitivley linked libraries // are also needed. - let linkedLibraryPaths: [AbsolutePath] = try linkableDependencies( + let linkedLibraryPaths: [Path.AbsolutePath] = try linkableDependencies( path: path, name: name, shouldExcludeHostAppDependencies: false @@ -497,16 +555,16 @@ public class GraphTraverser: GraphTraversing { return Set((directDependenciesLibraryPaths + linkedLibraryPaths).compactMap { $0.removingLastComponent() }) } - public func librariesSwiftIncludePaths(path: AbsolutePath, name: String) -> Set { + public func librariesSwiftIncludePaths(path: Path.AbsolutePath, name: String) -> Set { let dependencies = graph.dependencies[.target(name: name, path: path), default: []] - let librarySwiftModuleMapPaths = dependencies.compactMap { dependency -> AbsolutePath? in + let librarySwiftModuleMapPaths = dependencies.compactMap { dependency -> Path.AbsolutePath? in guard case let GraphDependency.library(_, _, _, _, swiftModuleMapPath) = dependency else { return nil } return swiftModuleMapPath } return Set(librarySwiftModuleMapPaths.compactMap { $0.removingLastComponent() }) } - public func runPathSearchPaths(path: AbsolutePath, name: String) -> Set { + public func runPathSearchPaths(path: Path.AbsolutePath, name: String) -> Set { guard let target = target(path: path, name: name), canEmbedProducts(target: target.target), target.target.product == .unitTests, @@ -515,7 +573,7 @@ public class GraphTraverser: GraphTraversing { return Set() } - var references: Set = Set([]) + var references: Set = Set([]) let from = GraphDependency.target(name: name, path: path) let precompiledFrameworksPaths = filterDependencies( @@ -524,10 +582,10 @@ public class GraphTraverser: GraphTraversing { skip: canDependencyEmbedProducts ) .lazy - .compactMap { (dependency: GraphDependency) -> AbsolutePath? in + .compactMap { (dependency: GraphDependency) -> Path.AbsolutePath? in switch dependency { case let .xcframework(xcframework): return xcframework.path - case let .framework(path, _, _, _, _, _, _, _): return path + case let .framework(path, _, _, _, _, _, _): return path case .macro: return nil case .library: return nil case .bundle: return nil @@ -542,9 +600,9 @@ public class GraphTraverser: GraphTraversing { return references } - public func hostTargetFor(path: AbsolutePath, name: String) -> GraphTarget? { - guard let targets = graph.targets[path] else { return nil } + public func hostTargetFor(path: Path.AbsolutePath, name: String) -> GraphTarget? { guard let project = graph.projects[path] else { return nil } + let targets = project.targets return targets.values.compactMap { target -> GraphTarget? in let dependencies = self.graph.dependencies[.target(name: target.name, path: path), default: Set()] @@ -558,7 +616,7 @@ public class GraphTraverser: GraphTraversing { }.first } - public func allProjectDependencies(path: AbsolutePath) throws -> Set { + public func allProjectDependencies(path: Path.AbsolutePath) throws -> Set { let targets = targets(at: path) if targets.isEmpty { return Set() } var references: Set = Set() @@ -572,7 +630,63 @@ public class GraphTraverser: GraphTraversing { return references } - public func dependsOnXCTest(path: AbsolutePath, name: String) -> Bool { + public func needsEnableTestingSearchPaths(path: Path.AbsolutePath, name: String) -> Bool { + var cache: [GraphTarget: Bool] = [:] + + func _needsEnableTestingSearchPaths( + path: Path.AbsolutePath, + name: String + ) -> Bool { + // Target could not be created, something must be wrong + guard let target = target(path: path, name: name) else { + return false + } + + // If a cache value is already present use it + if let cacheValue = cache[target] { + return cacheValue + } + + // Find all target dependencies + let allTargetDependencies = allTargetDependencies( + path: path, + name: name + ) + + // Check whether the current target depends on XCTest + let currentTargetDependsOnXCTest = dependsOnXCTest(path: path, name: name) + + // If there are no further dependencies cache the value for the current target and return the value of it + guard !allTargetDependencies.isEmpty else { + cache[target] = currentTargetDependsOnXCTest + return currentTargetDependsOnXCTest + } + + // If there are dependencies found, we need to traverse deeper down the graph + var enable: Bool? // placeholder when we find a dependency that needs to enable testing paths + for dependency in allTargetDependencies { + let needs = _needsEnableTestingSearchPaths(path: dependency.path, name: dependency.target.name) + + if needs { + cache[dependency] = true + enable = true + break + } else { + cache[dependency] = false + } + } + + // Either found a value or we use the one from the current target + let result = enable ?? currentTargetDependsOnXCTest + + cache[target] = result + return result + } + + return _needsEnableTestingSearchPaths(path: path, name: name) + } + + public func dependsOnXCTest(path: Path.AbsolutePath, name: String) -> Bool { guard let target = target(path: path, name: name) else { return false } @@ -587,7 +701,7 @@ public class GraphTraverser: GraphTraversing { } return directDependencies.contains(where: { dependency in switch dependency { - case .sdk(name: "XCTest", path: _, status: _, source: _): + case .sdk(name: "XCTest.framework", path: _, status: _, source: _): return true default: return false @@ -606,16 +720,14 @@ public class GraphTraverser: GraphTraversing { allInternalTargets().filter { directTargetExternalDependencies(path: $0.path, name: $0.target.name).count != 0 } } - public func directTargetExternalDependencies(path: AbsolutePath, name: String) -> Set { + public func directTargetExternalDependencies(path: 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) } + Set(graph.projects.flatMap { path, project -> [GraphTarget] in + guard project.isExternal else { return [] } + return project.targets.values.map { GraphTarget(path: path, target: $0, project: project) } }) } @@ -628,8 +740,11 @@ public class GraphTraverser: GraphTraversing { 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]! + guard let project = graph.projects[path], + let target = project.targets[name] + else { + return nil + } return GraphTarget(path: path, target: target, project: project) } else { return nil @@ -639,8 +754,9 @@ public class GraphTraverser: GraphTraversing { return allExternalTargets.subtracting(allTargetExternalDependendedUponTargets) } - public func allSwiftPluginExecutables(path: TSCBasic.AbsolutePath, name: String) -> Set { - func precompiledMacroDependencies(_ graphDependency: GraphDependency) -> Set { + // swiftlint:disable:next function_body_length + public func allSwiftPluginExecutables(path: Path.AbsolutePath, name: String) -> Set { + func precompiledMacroDependencies(_ graphDependency: GraphDependency) -> Set { Set( dependencies[graphDependency, default: Set()] .lazy @@ -827,8 +943,8 @@ public class GraphTraverser: GraphTraversing { for dependencyTargetReference in dependencies { var platformsToInsert: Set? - let dependencyTarget = dependencyTargetReference.graphTarget + let inheritedPlatforms = dependencyTarget.target.product == .macro ? Set([.macOS]) : parentPlatforms if let dependencyCondition = dependencyTargetReference.condition, let platformIntersection = PlatformCondition.when(target.target.dependencyPlatformFilters)? .intersection(dependencyCondition) @@ -838,13 +954,16 @@ public class GraphTraverser: GraphTraversing { break case let .condition(condition): if let condition { - let dependencyPlatforms: [Platform] = condition.platformFilters.map(\.platform).filter { $0 != nil } - .map { $0! } - platformsToInsert = Set(dependencyPlatforms) + let dependencyPlatforms = Set( + condition.platformFilters.map(\.platform) + .filter { $0 != nil } + .map { $0! } + ) + .intersection(inheritedPlatforms) + platformsToInsert = dependencyPlatforms } } } else { - let inheritedPlatforms = dependencyTarget.target.product == .macro ? Set([.macOS]) : parentPlatforms platformsToInsert = inheritedPlatforms.intersection(dependencyTarget.target.supportedPlatforms) } @@ -884,6 +1003,13 @@ public class GraphTraverser: GraphTraversing { ) } + func isDependencyExternal(_ dependency: GraphDependency) -> Bool { + guard let targetDependency = dependency.targetDependency, + let project = graph.projects[targetDependency.path] + else { return false } + return project.isExternal + } + func isDependencyPrecompiledMacro(_ dependency: GraphDependency) -> Bool { switch dependency { case .macro: @@ -950,7 +1076,7 @@ public class GraphTraverser: GraphTraversing { return false case let .xcframework(xcframework): return xcframework.linking == .static - case let .framework(_, _, _, _, linking, _, _, _), + case let .framework(_, _, _, _, linking, _, _), let .library(_, _, linking, _, _): return linking == .static case .bundle: return false @@ -989,6 +1115,21 @@ public class GraphTraverser: GraphTraversing { } } + func isDependencyPrecompiledDynamicAndLinkable(dependency: GraphDependency) -> Bool { + switch dependency { + 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 + case .macro: 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 } @@ -996,12 +1137,19 @@ public class GraphTraverser: GraphTraversing { } func canDependencyLinkStaticProducts(dependency: GraphDependency) -> Bool { - guard case let GraphDependency.target(name, path) = dependency, - let target = target(path: path, name: name) else { return false } - return target.target.canLinkStaticProducts() + switch dependency { + case let .target(name, path): + guard let target = target(path: path, name: name) else { return false } + return target.target.canLinkStaticProducts() + case let .xcframework(xcframework): return xcframework.linking == .dynamic + case let .framework(_, _, _, _, linking, _, _): return linking == .dynamic + case let .library(_, _, linking, _, _): return linking == .dynamic + default: + return false + } } - func unitTestHost(path: AbsolutePath, name: String) -> GraphTarget? { + func unitTestHost(path: Path.AbsolutePath, name: String) -> GraphTarget? { directLocalTargetDependencies(path: path, name: name) .first(where: { $0.target.product.canHostTests() })?.graphTarget } @@ -1009,9 +1157,11 @@ public class GraphTraverser: GraphTraversing { func canEmbedProducts(target: Target) -> Bool { let validProducts: [Product] = [ .app, + .watch2App, .appClip, .unitTests, .uiTests, + .appExtension, .watch2Extension, .systemExtension, .xpc, @@ -1031,11 +1181,10 @@ 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): + case let .framework(path, binaryPath, dsymPath, bcsymbolmapPaths, linking, architectures, status): return .framework( path: path, binaryPath: binaryPath, - isCarthage: isCarthage, dsymPath: dsymPath, bcsymbolmapPaths: bcsymbolmapPaths, linking: linking, @@ -1096,22 +1245,20 @@ public class GraphTraverser: GraphTraversing { private func allTargets(excludingExternalTargets: Bool) -> Set { Set(projects.flatMap { projectPath, project -> [GraphTarget] in if excludingExternalTargets, project.isExternal { return [] } - - let targets = graph.targets[projectPath, default: [:]] - return targets.values.map { target in + return project.targets.values.map { target in GraphTarget(path: projectPath, target: target, project: project) } }) } private func staticPrecompiledFrameworksDependencies( - path: AbsolutePath, + path: Path.AbsolutePath, name: String ) -> [GraphDependencyReference] { let precompiledStatic = graph.dependencies[.target(name: name, path: path), default: []] .filter { dependency in switch dependency { - case let .framework(_, _, _, _, linking: linking, _, _, _): + case let .framework(_, _, _, _, linking: linking, _, _): return linking == .static case .xcframework, .library, .bundle, .packageProduct, .target, .sdk, .macro: return false @@ -1126,7 +1273,7 @@ public class GraphTraverser: GraphTraversing { } private func staticPrecompiledXCFrameworksDependencies( - path: AbsolutePath, + path: Path.AbsolutePath, name: String ) -> [GraphDependencyReference] { let dependencies = filterDependencies( @@ -1147,3 +1294,14 @@ public class GraphTraverser: GraphTraversing { } // swiftlint:enable type_body_length + +extension GraphDependency { + fileprivate var xcframeworkDependency: GraphDependency.XCFramework? { + switch self { + case let .xcframework(xcframework): + return xcframework + default: + return nil + } + } +} diff --git a/Sources/TuistCore/Graph/Mappers/GraphMapper.swift b/Sources/TuistCore/Graph/Mappers/GraphMapper.swift index 91a287b1d62..2beeebd837c 100644 --- a/Sources/TuistCore/Graph/Mappers/GraphMapper.swift +++ b/Sources/TuistCore/Graph/Mappers/GraphMapper.swift @@ -1,26 +1,26 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph /// A protocol that defines an interface to map dependency graphs. public protocol GraphMapping { /// Given a value graph, it maps it into another value graph. /// - Parameter graph: Graph to be mapped. - func map(graph: Graph) async throws -> (Graph, [SideEffectDescriptor]) + func map(graph: Graph, environment: MapperEnvironment) async throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) } /// A mapper that is initialized with a mapping function. public final class AnyGraphMapper: GraphMapping { /// A function to map the graph. - let mapper: (Graph) throws -> (Graph, [SideEffectDescriptor]) + let mapper: (Graph) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) /// Default initializer /// - Parameter mapper: Function to map the graph. - public init(mapper: @escaping (Graph) throws -> (Graph, [SideEffectDescriptor])) { + public init(mapper: @escaping (Graph) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment)) { self.mapper = mapper } - public func map(graph: Graph) throws -> (Graph, [SideEffectDescriptor]) { + public func map(graph: Graph, environment _: MapperEnvironment) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { try mapper(graph) } } @@ -35,14 +35,19 @@ public final class SequentialGraphMapper: GraphMapping { self.mappers = mappers } - public func map(graph: Graph) async throws -> (Graph, [SideEffectDescriptor]) { + public func map( + graph: Graph, + environment: MapperEnvironment + ) async throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { var graph = graph var sideEffects = [SideEffectDescriptor]() + var environment = environment for mapper in mappers { - let (mappedGraph, newSideEffects) = try await mapper.map(graph: graph) + let (mappedGraph, newSideEffects, newEnvironment) = try await mapper.map(graph: graph, environment: environment) sideEffects.append(contentsOf: newSideEffects) graph = mappedGraph + environment = newEnvironment } - return (graph, sideEffects) + return (graph, sideEffects, environment) } } diff --git a/Sources/TuistCore/Graph/Mappers/MapperEnvironment.swift b/Sources/TuistCore/Graph/Mappers/MapperEnvironment.swift new file mode 100644 index 00000000000..2768c8f5adf --- /dev/null +++ b/Sources/TuistCore/Graph/Mappers/MapperEnvironment.swift @@ -0,0 +1,22 @@ +import Foundation + +public protocol MapperEnvironmentKey: Hashable { + associatedtype Value + static var defaultValue: Self.Value { get } +} + +/// An environment dictionary that holds extra context in mappers +public struct MapperEnvironment { + private var environment: [ObjectIdentifier: Any] = [:] + + public init() {} + + public subscript(key: Key.Type) -> Key.Value { + get { + environment[ObjectIdentifier(key)] as? Key.Value ?? Key.defaultValue + } + set { + environment[ObjectIdentifier(key)] = newValue + } + } +} diff --git a/Sources/TuistCore/Graph/Mappers/MapperEnvironmentKeys.swift b/Sources/TuistCore/Graph/Mappers/MapperEnvironmentKeys.swift new file mode 100644 index 00000000000..d9d9326edbe --- /dev/null +++ b/Sources/TuistCore/Graph/Mappers/MapperEnvironmentKeys.swift @@ -0,0 +1,25 @@ +import Foundation +import XcodeGraph + +// Common MapperEnvironment keys that need to be shared with the closed source repository. + +private struct TestsCacheHashesKey: MapperEnvironmentKey { + static var defaultValue: [Target: String] = [:] +} + +private struct InitialGraphKey: MapperEnvironmentKey { + static var defaultValue: Graph? +} + +extension MapperEnvironment { + /// Hashes of targets that are missing in the remote storage + public var testsCacheUntestedHashes: [Target: String] { + get { self[TestsCacheHashesKey.self] } + set { self[TestsCacheHashesKey.self] = newValue } + } + + public var initialGraph: Graph? { + get { self[InitialGraphKey.self] } + set { self[InitialGraphKey.self] = newValue } + } +} diff --git a/Sources/TuistCore/Graph/Mappers/ProjectMapper.swift b/Sources/TuistCore/Graph/Mappers/ProjectMapper.swift index 6625c9097fa..dcb624d957b 100644 --- a/Sources/TuistCore/Graph/Mappers/ProjectMapper.swift +++ b/Sources/TuistCore/Graph/Mappers/ProjectMapper.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph public protocol ProjectMapping { func map(project: Project) throws -> (Project, [SideEffectDescriptor]) @@ -31,12 +31,15 @@ public class TargetProjectMapper: ProjectMapping { } public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { - var results = (targets: [Target](), sideEffects: [SideEffectDescriptor]()) - results = try project.targets.reduce(into: results) { results, target in + var results = (targets: [String: Target](), sideEffects: [SideEffectDescriptor]()) + results = try project.targets.values.reduce(into: results) { results, target in let (updatedTarget, sideEffects) = try mapper.map(target: target) - results.targets.append(updatedTarget) + results.targets[updatedTarget.name] = updatedTarget results.sideEffects.append(contentsOf: sideEffects) } - return (project.with(targets: results.targets), results.sideEffects) + var project = project + project.targets = results.targets + + return (project, results.sideEffects) } } diff --git a/Sources/TuistCore/Graph/Mappers/TargetMapper.swift b/Sources/TuistCore/Graph/Mappers/TargetMapper.swift index 5a11d497fb7..6cd70e82c17 100644 --- a/Sources/TuistCore/Graph/Mappers/TargetMapper.swift +++ b/Sources/TuistCore/Graph/Mappers/TargetMapper.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph public protocol TargetMapping { func map(target: Target) throws -> (Target, [SideEffectDescriptor]) diff --git a/Sources/TuistCore/Graph/Mappers/WorkspaceMapper.swift b/Sources/TuistCore/Graph/Mappers/WorkspaceMapper.swift index b4d1bee0cb6..75c6f54a1cf 100644 --- a/Sources/TuistCore/Graph/Mappers/WorkspaceMapper.swift +++ b/Sources/TuistCore/Graph/Mappers/WorkspaceMapper.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph public struct WorkspaceWithProjects: Equatable { public var workspace: Workspace diff --git a/Sources/TuistCore/Graph/ModelExtensions/BuildConfiguration+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/BuildConfiguration+Core.swift index d759ab3fdeb..842d9e55792 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/BuildConfiguration+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/BuildConfiguration+Core.swift @@ -1,6 +1,6 @@ import Foundation -import TuistGraph import TuistSupport +import XcodeGraph extension BuildConfiguration: XcodeRepresentable { public var xcodeValue: String { name } diff --git a/Sources/TuistCore/Graph/ModelExtensions/Headers+core.swift b/Sources/TuistCore/Graph/ModelExtensions/Headers+core.swift index efee4d1ce6e..c40307eab84 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Headers+core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Headers+core.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph import XcodeProj extension Headers { diff --git a/Sources/TuistCore/Graph/ModelExtensions/Product+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Product+Core.swift index 40773e88548..e36d9623bfd 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Product+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Product+Core.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph import XcodeProj extension Product { diff --git a/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift index ac6226ff987..16d02acd4fb 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift @@ -1,5 +1,7 @@ import Foundation -import TuistGraph +import Path +import TuistSupport +import XcodeGraph extension Project { /// It returns the project targets sorted based on the target type and the dependencies between them. @@ -8,7 +10,7 @@ extension Project { /// - Parameter graph: Dependencies graph. /// - Returns: Sorted targets. public func sortedTargetsForProjectScheme(graph: Graph) -> [Target] { - targets.sorted { first, second -> Bool in + targets.values.sorted { first, second -> Bool in // First criteria: Test bundles at the end if first.product.testsBundle, !second.product.testsBundle { return false @@ -38,4 +40,26 @@ extension Project { } } } + + public func derivedDirectoryPath(for target: Target) -> AbsolutePath { + if isExternal, + path.pathString + .contains("\(Constants.SwiftPackageManager.packageBuildDirectoryName)/checkouts") + { + return path + // Leads to SPM's .build directory + .parentDirectory.parentDirectory + .appending( + components: [ + Constants.DerivedDirectory.dependenciesDerivedDirectory, + target.name, + ] + ) + } else { + return path + .appending( + component: Constants.DerivedDirectory.name + ) + } + } } diff --git a/Sources/TuistCore/Graph/ModelExtensions/Requirement+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Requirement+Core.swift index d0ef20b884e..7d924a921a5 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Requirement+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Requirement+Core.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph import XcodeProj extension Requirement { diff --git a/Sources/TuistCore/Graph/ModelExtensions/Scheme+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Scheme+Core.swift index ba58c98cd7b..7977b11c07b 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Scheme+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Scheme+Core.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph extension Scheme { public func targetDependencies() -> [TargetReference] { diff --git a/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift index 4f36d8d1027..a37ed67b64e 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph public enum TargetError: FatalError, Equatable { case invalidSourcesGlob(targetName: String, invalidGlobs: [InvalidGlob]) @@ -18,8 +18,31 @@ public enum TargetError: FatalError, Equatable { } extension Target { - /// Returns the product name including the extension. + /// Returns the product name including the extension + /// if the PRODUCT_NAME build setting of the target is set and contains a static value that's consistent + /// throughout all the configurations, it uses that value, otherwise it defaults to the target's default. public var productNameWithExtension: String { + var settingsProductNames: Set = Set() + + if let value = settings?.base["PRODUCT_NAME"], case let SettingValue.string(baseProductName) = value { + settingsProductNames.insert(baseProductName) + } + settings?.configurations.values.forEach { configuration in + if let value = configuration?.settings["PRODUCT_NAME"], + case let SettingValue.string(configurationProductName) = value + { + settingsProductNames.insert(configurationProductName) + } + } + + let productName: String + + if settingsProductNames.count == 1, !settingsProductNames.first!.contains("$") { + productName = settingsProductNames.first! + } else { + productName = self.productName + } + switch product { case .staticLibrary, .dynamicLibrary: return "lib\(productName).\(product.xcodeValue.fileExtension!)" @@ -52,8 +75,8 @@ extension Target { /// This method unfolds the source file globs subtracting the paths that are excluded and ignoring /// the files that don't have a supported source extension. /// - Parameter sources: List of source file glob to be unfolded. - public static func sources(targetName: String, sources: [SourceFileGlob]) throws -> [TuistGraph.SourceFile] { - var sourceFiles: [AbsolutePath: TuistGraph.SourceFile] = [:] + public static func sources(targetName: String, sources: [SourceFileGlob]) throws -> [XcodeGraph.SourceFile] { + var sourceFiles: [AbsolutePath: XcodeGraph.SourceFile] = [:] var invalidGlobs: [InvalidGlob] = [] for source in sources { diff --git a/Sources/TuistCore/Graph/ModelExtensions/TargetScript+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/TargetScript+Core.swift index d5e5d7522a4..d620a27652f 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/TargetScript+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/TargetScript+Core.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph extension TargetScript { /// Returns the shell script that should be used in the target build phase. diff --git a/Sources/TuistCore/GraphTraverser/GraphTraverser+Extra.swift b/Sources/TuistCore/GraphTraverser/GraphTraverser+Extra.swift index 9c4d07e4231..3fc907f30d8 100644 --- a/Sources/TuistCore/GraphTraverser/GraphTraverser+Extra.swift +++ b/Sources/TuistCore/GraphTraverser/GraphTraverser+Extra.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph extension GraphTraversing { /// Returns the included based on the parameters. diff --git a/Sources/TuistCore/GraphTraverser/GraphTraversing.swift b/Sources/TuistCore/GraphTraverser/GraphTraversing.swift index c11ae1f0279..2bdd02c3503 100644 --- a/Sources/TuistCore/GraphTraverser/GraphTraversing.swift +++ b/Sources/TuistCore/GraphTraverser/GraphTraversing.swift @@ -1,7 +1,9 @@ import Foundation -import TSCBasic -import TuistGraph +import Mockable +import Path +import XcodeGraph +@Mockable public protocol GraphTraversing { /// Graph name var name: String { get } @@ -22,7 +24,7 @@ public protocol GraphTraversing { var projects: [AbsolutePath: Project] { get } /// Returns all the targets of the graph. - var targets: [AbsolutePath: [String: Target]] { get } + func targets() -> [AbsolutePath: [String: Target]] /// Dependencies. var dependencies: [GraphDependency: Set] { get } @@ -74,6 +76,9 @@ public protocol GraphTraversing { /// - Returns: All direct and transitive target dependencies func allTargetDependencies(path: AbsolutePath, name: String) -> Set + /// - Returns: All direct and transitive target dependencies, traversing from the passed targets + func allTargetDependencies(traversingFromTargets: [GraphTarget]) -> 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 @@ -188,6 +193,14 @@ public protocol GraphTraversing { /// - Parameter path: Path to the directory where the project is defined. func allProjectDependencies(path: AbsolutePath) throws -> Set + /// Determines whether ENABLE_TESTING_SEARCH_PATHS needs to be enabled + /// + /// - Parameters: + /// - path: Path to the project tha defines the target. + /// - name: Target name. + /// - Returns: True if the given target needs to have ENABLE_TESTING_SEARCH_PATHS enabled, false otherwise + func needsEnableTestingSearchPaths(path: AbsolutePath, name: String) -> Bool + /// Returns true if the given target depends on XCTest. /// - Parameters: /// - path: Path to the project tha defines the target. diff --git a/Sources/TuistCore/MetadataProviders/FrameworkMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/FrameworkMetadataProvider.swift index b71033c0a69..baf32027349 100644 --- a/Sources/TuistCore/MetadataProviders/FrameworkMetadataProvider.swift +++ b/Sources/TuistCore/MetadataProviders/FrameworkMetadataProvider.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph // MARK: - Provider Errors @@ -64,7 +64,6 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame let bcsymbolmapPaths = try bcsymbolmapPaths(frameworkPath: path) let linking = try linking(binaryPath: binaryPath) let architectures = try architectures(binaryPath: binaryPath) - let isCarthage = path.pathString.contains("Carthage/Build") return FrameworkMetadata( path: path, binaryPath: binaryPath, @@ -72,7 +71,6 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame bcsymbolmapPaths: bcsymbolmapPaths, linking: linking, architectures: architectures, - isCarthage: isCarthage, status: status ) } diff --git a/Sources/TuistCore/MetadataProviders/LibraryMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/LibraryMetadataProvider.swift index b5135812496..99515fb9d83 100644 --- a/Sources/TuistCore/MetadataProviders/LibraryMetadataProvider.swift +++ b/Sources/TuistCore/MetadataProviders/LibraryMetadataProvider.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph // MARK: - Provider Errors diff --git a/Sources/TuistCore/MetadataProviders/PrecompiledMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/PrecompiledMetadataProvider.swift index 4e9cda5a0b2..a48ac31b4dd 100644 --- a/Sources/TuistCore/MetadataProviders/PrecompiledMetadataProvider.swift +++ b/Sources/TuistCore/MetadataProviders/PrecompiledMetadataProvider.swift @@ -1,8 +1,8 @@ import Foundation import MachO -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph enum PrecompiledMetadataProviderError: FatalError, Equatable { case architecturesNotFound(AbsolutePath) @@ -217,7 +217,7 @@ public class PrecompiledMetadataProvider: PrecompiledMetadataProviding { guard String(data: magic, encoding: .ascii) == archiveFormatMagic else { return } - binary.seek(to: archiveHeaderSizeOffset) + binary.seek(to: currentOffset + archiveHeaderSizeOffset) guard let sizeString = binary.readString(ofLength: 10) else { return } let size = strtoul(sizeString, nil, 10) diff --git a/Sources/TuistCore/MetadataProviders/SystemFrameworkMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/SystemFrameworkMetadataProvider.swift new file mode 100644 index 00000000000..55ad5413259 --- /dev/null +++ b/Sources/TuistCore/MetadataProviders/SystemFrameworkMetadataProvider.swift @@ -0,0 +1,103 @@ +import Foundation +import Path +import TuistSupport +import XcodeGraph + +// MARK: - Provider Errors + +public enum SystemFrameworkMetadataProviderError: FatalError, Equatable { + case unsupportedSDK(name: String) + + public var description: String { + switch self { + case let .unsupportedSDK(sdk): + let supportedTypes = SDKType.supportedTypesDescription + return "The SDK type of \(sdk) is not currently supported - only \(supportedTypes) are supported." + } + } + + public var type: ErrorType { + switch self { + case .unsupportedSDK: + return .abort + } + } +} + +// MARK: - Provider + +public protocol SystemFrameworkMetadataProviding { + func loadMetadata(sdkName: String, status: SDKStatus, platform: Platform, source: SDKSource) throws -> SystemFrameworkMetadata +} + +extension SystemFrameworkMetadataProviding { + func loadXCTestMetadata(platform: Platform) throws -> SystemFrameworkMetadata { + try loadMetadata(sdkName: "XCTest.framework", status: .required, platform: platform, source: .developer) + } +} + +// MARK: - Default Implementation + +public final class SystemFrameworkMetadataProvider: SystemFrameworkMetadataProviding { + public init() {} + + public func loadMetadata( + sdkName: String, + status: SDKStatus, + platform: Platform, + source: SDKSource + ) throws -> SystemFrameworkMetadata { + let sdkNamePath = try AbsolutePath(validating: "/\(sdkName)") + guard let sdkExtension = sdkNamePath.extension + else { throw SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName) } + + let sdkType: SDKType + switch sdkExtension { + case "framework": + sdkType = .framework + case "tbd": + if sdkName.starts(with: "libswift") { + sdkType = .swiftLibrary + } else { + sdkType = .library + } + default: + throw SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName) + } + + let path = try sdkPath(name: sdkName, platform: platform, type: sdkType, source: source) + return SystemFrameworkMetadata( + name: sdkName, + path: path, + status: status, + source: source + ) + } + + private func sdkPath(name: String, platform: Platform, type: SDKType, source: SDKSource) throws -> AbsolutePath { + switch source { + case .developer: + let xcodeDeveloperSdkRootPath = platform.xcodeDeveloperSdkRootPath + let sdkRootPath = try AbsolutePath(validating: "/\(xcodeDeveloperSdkRootPath)") + return sdkRootPath + .appending(try RelativePath(validating: "Frameworks")) + .appending(component: name) + + case .system: + let sdkRootPath = try AbsolutePath(validating: "/\(platform.xcodeSdkRootPath)") + switch type { + case .framework: + return sdkRootPath + .appending(try RelativePath(validating: "System/Library/Frameworks")) + .appending(component: name) + case .library: + return sdkRootPath + .appending(try RelativePath(validating: "usr/lib")) + .appending(component: name) + case .swiftLibrary: + return sdkRootPath + .appending(components: "usr", "lib", "swift", name) + } + } + } +} diff --git a/Sources/TuistCore/MetadataProviders/SystemFrameworksMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/SystemFrameworksMetadataProvider.swift deleted file mode 100644 index 0c1de8883fe..00000000000 --- a/Sources/TuistCore/MetadataProviders/SystemFrameworksMetadataProvider.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph -import TuistSupport - -// MARK: - Provider Errors - -public enum SystemFrameworkMetadataProviderError: FatalError, Equatable { - case unsupportedSDK(name: String) - - public var description: String { - switch self { - case let .unsupportedSDK(sdk): - let supportedTypes = SDKType.supportedTypesDescription - return "The SDK type of \(sdk) is not currently supported - only \(supportedTypes) are supported." - } - } - - public var type: ErrorType { - switch self { - case .unsupportedSDK: - return .abort - } - } -} - -// MARK: - Provider - -public protocol SystemFrameworkMetadataProviding { - func loadMetadata(sdkName: String, status: SDKStatus, platform: Platform, source: SDKSource) throws -> SystemFrameworkMetadata -} - -extension SystemFrameworkMetadataProviding { - func loadXCTestMetadata(platform: Platform) throws -> SystemFrameworkMetadata { - try loadMetadata(sdkName: "XCTest.framework", status: .required, platform: platform, source: .developer) - } -} - -// MARK: - Default Implementation - -public final class SystemFrameworkMetadataProvider: SystemFrameworkMetadataProviding { - public init() {} - - public func loadMetadata( - sdkName: String, - status: SDKStatus, - platform: Platform, - source: SDKSource - ) throws -> SystemFrameworkMetadata { - let sdkNamePath = try AbsolutePath(validating: "/\(sdkName)") - guard let sdkExtension = sdkNamePath.extension, - let sdkType = SDKType(rawValue: sdkExtension) - else { - throw SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName) - } - let path = try sdkPath(name: sdkName, platform: platform, type: sdkType, source: source) - return SystemFrameworkMetadata( - name: sdkName, - path: path, - status: status, - source: source - ) - } - - private func sdkPath(name: String, platform: Platform, type: SDKType, source: SDKSource) throws -> AbsolutePath { - switch source { - case .developer: - let xcodeDeveloperSdkRootPath = platform.xcodeDeveloperSdkRootPath - let sdkRootPath = try AbsolutePath(validating: "/\(xcodeDeveloperSdkRootPath)") - return sdkRootPath - .appending(try RelativePath(validating: "Frameworks")) - .appending(component: name) - - case .system: - let sdkRootPath = try AbsolutePath(validating: "/\(platform.xcodeSdkRootPath)") - switch type { - case .framework: - return sdkRootPath - .appending(try RelativePath(validating: "System/Library/Frameworks")) - .appending(component: name) - case .library: - return sdkRootPath - .appending(try RelativePath(validating: "usr/lib")) - .appending(component: name) - } - } - } -} diff --git a/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift index 7543583c620..13674377299 100644 --- a/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift +++ b/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph // MARK: - Provider Errors @@ -141,7 +141,7 @@ public final class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCF binaryPath = try AbsolutePath(validating: library.identifier, relativeTo: xcframeworkPath) .appending(try RelativePath(validating: library.path.pathString)) .appending(component: library.path.basenameWithoutExt) - case "a": + case "a", "dylib": binaryPath = try AbsolutePath(validating: library.identifier, relativeTo: xcframeworkPath) .appending(try RelativePath(validating: library.path.pathString)) default: diff --git a/Sources/TuistGraph/Models/CompatibleXcodeVersions.swift b/Sources/TuistCore/Models/CompatibleXcodeVersions.swift similarity index 98% rename from Sources/TuistGraph/Models/CompatibleXcodeVersions.swift rename to Sources/TuistCore/Models/CompatibleXcodeVersions.swift index 4a370868052..df13fde0740 100644 --- a/Sources/TuistGraph/Models/CompatibleXcodeVersions.swift +++ b/Sources/TuistCore/Models/CompatibleXcodeVersions.swift @@ -1,5 +1,5 @@ import Foundation -import struct ProjectDescription.Version +import XcodeGraph /// Enum that represents all the Xcode versions that a project or set of projects is compatible with. public enum CompatibleXcodeVersions: Equatable, Hashable, ExpressibleByArrayLiteral, ExpressibleByStringInterpolation, diff --git a/Sources/TuistCore/Models/Config.swift b/Sources/TuistCore/Models/Config.swift new file mode 100644 index 00000000000..70f6c78c757 --- /dev/null +++ b/Sources/TuistCore/Models/Config.swift @@ -0,0 +1,129 @@ +import Foundation +import Path +import TuistSupport +import XcodeGraph + +/// This model allows to configure Tuist. +public struct Config: Equatable, Hashable { + /// List of `Plugin`s used to extend Tuist. + public let plugins: [PluginLocation] + + /// Generation options. + public let generationOptions: GenerationOptions + + /// List of Xcode versions the project or set of projects is compatible with. + public let compatibleXcodeVersions: CompatibleXcodeVersions + + /// The full project handle such as tuist-org/tuist. + public let fullHandle: String? + + /// The base URL that points to the Tuist server. + public let url: URL + + /// The version of Swift that will be used by Tuist. + /// If `nil` is passed then Tuist will use the environment’s version. + public let swiftVersion: Version? + + /// The path of the config file. + public let path: AbsolutePath? + + /// Returns the default Tuist configuration. + public static var `default`: Config { + Config( + compatibleXcodeVersions: .all, + fullHandle: nil, + url: Constants.URLs.production, + swiftVersion: nil, + plugins: [], + generationOptions: .init( + resolveDependenciesWithSystemScm: false, + disablePackageVersionLocking: false, + staticSideEffectsWarningTargets: .all + ), + path: nil + ) + } + + /// Initializes the tuist cofiguration. + /// + /// - Parameters: + /// - compatibleXcodeVersions: List of Xcode versions the project or set of projects is compatible with. + /// - cloud: Cloud configuration. + /// - swiftVersion: The version of Swift that will be used by Tuist. + /// - plugins: List of locations to a `Plugin` manifest. + /// - generationOptions: Generation options. + /// - path: The path of the config file. + public init( + compatibleXcodeVersions: CompatibleXcodeVersions, + fullHandle: String?, + url: URL, + swiftVersion: Version?, + plugins: [PluginLocation], + generationOptions: GenerationOptions, + path: AbsolutePath? + ) { + self.compatibleXcodeVersions = compatibleXcodeVersions + self.fullHandle = fullHandle + self.url = url + self.swiftVersion = swiftVersion + self.plugins = plugins + self.generationOptions = generationOptions + self.path = path + } + + // MARK: - Hashable + + public func hash(into hasher: inout Hasher) { + hasher.combine(generationOptions) + hasher.combine(fullHandle) + hasher.combine(url) + hasher.combine(swiftVersion) + hasher.combine(compatibleXcodeVersions) + } +} + +#if DEBUG + extension Config { + public static func test( + compatibleXcodeVersions: CompatibleXcodeVersions = .all, + fullHandle: String? = nil, + url: URL = Constants.URLs.production, + swiftVersion: Version? = nil, + plugins: [PluginLocation] = [], + generationOptions: GenerationOptions = Config.default.generationOptions, + path: AbsolutePath? = nil + ) -> Config { + .init( + compatibleXcodeVersions: compatibleXcodeVersions, + fullHandle: fullHandle, + url: url, + swiftVersion: swiftVersion, + plugins: plugins, + generationOptions: generationOptions, + path: path + ) + } + } + + extension Config.GenerationOptions { + public static func test( + resolveDependenciesWithSystemScm: Bool = false, + disablePackageVersionLocking: Bool = false, + clonedSourcePackagesDirPath: AbsolutePath? = nil, + staticSideEffectsWarningTargets: TuistCore.Config.GenerationOptions.StaticSideEffectsWarningTargets = .all, + enforceExplicitDependencies: Bool = false, + defaultConfiguration: String? = nil, + optionalAuthentication: Bool = false + ) -> Self { + .init( + resolveDependenciesWithSystemScm: resolveDependenciesWithSystemScm, + disablePackageVersionLocking: disablePackageVersionLocking, + clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, + staticSideEffectsWarningTargets: staticSideEffectsWarningTargets, + enforceExplicitDependencies: enforceExplicitDependencies, + defaultConfiguration: defaultConfiguration, + optionalAuthentication: optionalAuthentication + ) + } + } +#endif diff --git a/Sources/TuistGraph/Models/ConfigGenerationOptions.swift b/Sources/TuistCore/Models/ConfigGenerationOptions.swift similarity index 76% rename from Sources/TuistGraph/Models/ConfigGenerationOptions.swift rename to Sources/TuistCore/Models/ConfigGenerationOptions.swift index c2150f6f589..fe20e653f31 100644 --- a/Sources/TuistGraph/Models/ConfigGenerationOptions.swift +++ b/Sources/TuistCore/Models/ConfigGenerationOptions.swift @@ -1,5 +1,4 @@ -import TSCBasic -import TSCUtility +import Path extension Config { public struct GenerationOptions: Codable, Hashable { @@ -14,19 +13,25 @@ extension Config { public let clonedSourcePackagesDirPath: AbsolutePath? public let staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets public let enforceExplicitDependencies: Bool + public let defaultConfiguration: String? + public var optionalAuthentication: Bool public init( resolveDependenciesWithSystemScm: Bool, disablePackageVersionLocking: Bool, clonedSourcePackagesDirPath: AbsolutePath? = nil, staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets = .all, - enforceExplicitDependencies: Bool = false + enforceExplicitDependencies: Bool = false, + defaultConfiguration: String? = nil, + optionalAuthentication: Bool = false ) { self.resolveDependenciesWithSystemScm = resolveDependenciesWithSystemScm self.disablePackageVersionLocking = disablePackageVersionLocking self.clonedSourcePackagesDirPath = clonedSourcePackagesDirPath self.staticSideEffectsWarningTargets = staticSideEffectsWarningTargets self.enforceExplicitDependencies = enforceExplicitDependencies + self.defaultConfiguration = defaultConfiguration + self.optionalAuthentication = optionalAuthentication } } } diff --git a/Sources/TuistCore/Models/PackageSettings.swift b/Sources/TuistCore/Models/PackageSettings.swift new file mode 100644 index 00000000000..449a1ae74c8 --- /dev/null +++ b/Sources/TuistCore/Models/PackageSettings.swift @@ -0,0 +1,67 @@ +import Foundation +import XcodeGraph + +/// Contains the description of custom SPM settings +public struct PackageSettings: Equatable, Codable { + /// The custom `Product` types to be used for SPM targets. + public let productTypes: [String: Product] + + /// Custom destinations to be used for SPM products. + public let productDestinations: [String: Destinations] + + // 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: XcodeGraph.Project.Options] + + /// Swift tools version of the parsed `Package.swift` + public let swiftToolsVersion: Version + + /// 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], + productDestinations: [String: Destinations], + baseSettings: Settings, + targetSettings: [String: SettingsDictionary], + projectOptions: [String: XcodeGraph.Project.Options] = [:], + swiftToolsVersion: Version + ) { + self.productTypes = productTypes + self.productDestinations = productDestinations + self.baseSettings = baseSettings + self.targetSettings = targetSettings + self.projectOptions = projectOptions + self.swiftToolsVersion = swiftToolsVersion + } +} + +#if DEBUG + extension PackageSettings { + public static func test( + productTypes: [String: Product] = [:], + productDestinations: [String: Destinations] = [:], + baseSettings: Settings = Settings.default, + targetSettings: [String: SettingsDictionary] = [:], + projectOptions: [String: XcodeGraph.Project.Options] = [:], + swiftToolsVersion: Version = Version("5.4.9") + ) -> PackageSettings { + PackageSettings( + productTypes: productTypes, + productDestinations: productDestinations, + baseSettings: baseSettings, + targetSettings: targetSettings, + projectOptions: projectOptions, + swiftToolsVersion: swiftToolsVersion + ) + } + } +#endif diff --git a/Sources/TuistCore/Models/Plugin.swift b/Sources/TuistCore/Models/Plugin.swift new file mode 100644 index 00000000000..e43cb18ea02 --- /dev/null +++ b/Sources/TuistCore/Models/Plugin.swift @@ -0,0 +1,22 @@ +import Foundation +import Path + +/// A `Plugin` used to extend Tuist. +public struct Plugin: Equatable, Hashable { + /// The name of the plugin. + public let name: String + + /// Creates a `Plugin` + /// + /// - Parameters: + /// - name: The name of the plugin. + public init(name: String) { + self.name = name + } +} + +extension Plugin: CustomStringConvertible { + public var description: String { + "Plugin: \(name)" + } +} diff --git a/Sources/TuistGraph/Models/PluginLocation.swift b/Sources/TuistCore/Models/PluginLocation.swift similarity index 100% rename from Sources/TuistGraph/Models/PluginLocation.swift rename to Sources/TuistCore/Models/PluginLocation.swift diff --git a/Sources/TuistCore/Models/PluginResourceSynthesizer.swift b/Sources/TuistCore/Models/PluginResourceSynthesizer.swift new file mode 100644 index 00000000000..47048e0016c --- /dev/null +++ b/Sources/TuistCore/Models/PluginResourceSynthesizer.swift @@ -0,0 +1,32 @@ +import Foundation +import Path + +/// Resource synthesizer plugin model +public struct PluginResourceSynthesizer: Equatable { + /// Name of the plugin + public let name: String + /// Path to `ResourceSynthesizers` directory where all resource templates are located + public let path: AbsolutePath + + public init( + name: String, + path: AbsolutePath + ) { + self.name = name + self.path = path + } +} + +#if DEBUG + extension PluginResourceSynthesizer { + public static func test( + name: String = "Plugin", + path: AbsolutePath = try! AbsolutePath(validating: "/test") // swiftlint:disable:this force_try + ) -> Self { + .init( + name: name, + path: path + ) + } + } +#endif diff --git a/Sources/TuistCore/Models/Plugins.swift b/Sources/TuistCore/Models/Plugins.swift new file mode 100644 index 00000000000..81e1e75bede --- /dev/null +++ b/Sources/TuistCore/Models/Plugins.swift @@ -0,0 +1,53 @@ +import Foundation +import Path + +/// A model which contains all loaded plugin representations. +public struct Plugins: Equatable { + /// List of the loaded custom helper plugins. + public let projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin] + + /// List of paths to template definitions. + public let templateDirectories: [AbsolutePath] + + /// List of paths pointing to resource templates + public let resourceSynthesizers: [PluginResourceSynthesizer] + + /// Creates a `Plugins`. + /// + /// - Parameters: + /// - projectDescriptionHelpers: List of the loaded helper plugins. + /// - templatePaths: List of paths to the `Templates/` directory for the loaded plugins. + /// - resourceSynthesizers: List of the loaded resource synthesizer plugins + public init( + projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin], + templatePaths: [AbsolutePath], + resourceSynthesizers: [PluginResourceSynthesizer] + ) { + self.projectDescriptionHelpers = projectDescriptionHelpers + templateDirectories = templatePaths + self.resourceSynthesizers = resourceSynthesizers + } + + /// An empty `Plugins`. + public static let none: Plugins = .init( + projectDescriptionHelpers: [], + templatePaths: [], + resourceSynthesizers: [] + ) +} + +#if DEBUG + extension Plugins { + public static func test( + projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin] = [], + templatePaths: [AbsolutePath] = [], + resourceSynthesizers: [PluginResourceSynthesizer] = [] + ) -> Plugins { + Plugins( + projectDescriptionHelpers: projectDescriptionHelpers, + templatePaths: templatePaths, + resourceSynthesizers: resourceSynthesizers + ) + } + } +#endif diff --git a/Sources/TuistGraph/Models/ProjectDescriptionHelpersPlugin.swift b/Sources/TuistCore/Models/ProjectDescriptionHelpersPlugin.swift similarity index 98% rename from Sources/TuistGraph/Models/ProjectDescriptionHelpersPlugin.swift rename to Sources/TuistCore/Models/ProjectDescriptionHelpersPlugin.swift index f81cf5ecfb4..2301a321164 100644 --- a/Sources/TuistGraph/Models/ProjectDescriptionHelpersPlugin.swift +++ b/Sources/TuistCore/Models/ProjectDescriptionHelpersPlugin.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// A model representing a custom `ProjectDescription` helper. public struct ProjectDescriptionHelpersPlugin: Equatable { diff --git a/Sources/TuistCore/Models/Template.swift b/Sources/TuistCore/Models/Template.swift new file mode 100644 index 00000000000..98b6a6423fa --- /dev/null +++ b/Sources/TuistCore/Models/Template.swift @@ -0,0 +1,179 @@ +import Path + +public struct Template: Equatable { + public let description: String + public let attributes: [Attribute] + public let items: [Item] + + public init( + description: String, + attributes: [Attribute] = [], + items: [Item] = [] + ) { + self.description = description + self.attributes = attributes + self.items = items + } + + public enum Attribute: Equatable { + case required(String) + case optional(String, default: Value) + + public var isOptional: Bool { + switch self { + case .required: + return false + case .optional: + return true + } + } + + public var name: String { + switch self { + case let .required(name): + return name + case let .optional(name, default: _): + return name + } + } + } + + public enum Contents: Equatable { + case string(String) + case file(AbsolutePath) + case directory(AbsolutePath) + } + + public struct Item: Equatable { + public let path: RelativePath + public let contents: Contents + + public init( + path: RelativePath, + contents: Contents + ) { + self.path = path + self.contents = contents + } + } +} + +extension Template.Attribute { + /// This represents the default value type of Attribute + public indirect enum Value: Equatable { + /// It represents a string value. + case string(String) + /// It represents an integer value. + case integer(Int) + /// It represents a floating value. + case real(Double) + /// It represents a boolean value. + case boolean(Bool) + /// It represents a dictionary value. + case dictionary([String: Value]) + /// It represents an array value. + case array([Value]) + } +} + +extension Template.Attribute.Value: RawRepresentable { + public typealias RawValue = Any + + public init?(rawValue: RawValue) { + switch rawValue { + case is String: + if let string = rawValue as? String { + self = .string(string) + } else { + return nil + } + case is Int: + if let integer = rawValue as? Int { + self = .integer(integer) + } else { + return nil + } + case is Double: + if let real = rawValue as? Double { + self = .real(real) + } else { + return nil + } + case is Bool: + if let boolean = rawValue as? Bool { + self = .boolean(boolean) + } else { + return nil + } + case is [String: Any]: + if let dictionary = rawValue as? [String: Any] { + var newDictionary: [String: Self] = [:] + for (key, value) in dictionary { + newDictionary[key] = .init(rawValue: value) + } + self = .dictionary(newDictionary) + } else { + return nil + } + case is [Any]: + if let array = rawValue as? [Any] { + let newArray: [Self] = array.map { .init(rawValue: $0) }.compactMap { $0 } + self = .array(newArray) + } else { + return nil + } + default: + return nil + } + } + + public var rawValue: RawValue { + switch self { + case let .string(string): + return string + case let .integer(integer): + return integer + case let .real(real): + return real + case let .boolean(boolean): + return boolean + case let .dictionary(dictionary): + var newDictionary: [String: Any] = [:] + for (key, value) in dictionary { + newDictionary[key] = value.rawValue + } + return newDictionary + case let .array(array): + let newArray: [Any] = array.map(\.rawValue) + return newArray + } + } +} + +#if DEBUG + extension Template { + public static func test( + description: String = "Template", + attributes: [Attribute] = [], + items: [Template.Item] = [] + ) -> Template { + Template( + description: description, + attributes: attributes, + items: items + ) + } + } + + extension Template.Item { + public static func test( + path: RelativePath, + contents: Template.Contents = .string("test content") + ) -> Template.Item { + Template.Item( + path: path, + contents: contents + ) + } + } +#endif diff --git a/Sources/TuistCore/NodeLoaders/FrameworkLoader.swift b/Sources/TuistCore/NodeLoaders/FrameworkLoader.swift index be5f9261958..c80356504f7 100644 --- a/Sources/TuistCore/NodeLoaders/FrameworkLoader.swift +++ b/Sources/TuistCore/NodeLoaders/FrameworkLoader.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph enum FrameworkLoaderError: FatalError, Equatable { case frameworkNotFound(AbsolutePath) @@ -57,7 +57,6 @@ public final class FrameworkLoader: FrameworkLoading { bcsymbolmapPaths: metadata.bcsymbolmapPaths, linking: metadata.linking, architectures: metadata.architectures, - isCarthage: metadata.isCarthage, status: metadata.status ) } diff --git a/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift b/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift index 8b293b4772b..1585a9ea374 100644 --- a/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift +++ b/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph enum XCFrameworkLoaderError: FatalError, Equatable { case xcframeworkNotFound(AbsolutePath) diff --git a/Sources/TuistCore/Simulator/SimulatorController.swift b/Sources/TuistCore/Simulator/SimulatorController.swift index 4fdc9989af1..5cb26c72faa 100644 --- a/Sources/TuistCore/Simulator/SimulatorController.swift +++ b/Sources/TuistCore/Simulator/SimulatorController.swift @@ -1,9 +1,11 @@ import Foundation -import TSCBasic +import Mockable +import Path import struct TSCUtility.Version -import TuistGraph import TuistSupport +import XcodeGraph +@Mockable public protocol SimulatorControlling { /// Finds first available device defined by given parameters /// - Parameters: @@ -18,6 +20,20 @@ public protocol SimulatorControlling { deviceName: String? ) async throws -> SimulatorDeviceAndRuntime + /// Ask the user to select one of the available devices defined by given parameters + /// if there is more than one, otherwise return the only one available + /// - Parameters: + /// - platform: Given platform + /// - version: Specific version, ignored if nil + /// - minVersion: Minimum version of the OS + /// - deviceName: Specific device name (eg. iPhone X) + func askForAvailableDevice( + platform: Platform, + version: Version?, + minVersion: Version?, + deviceName: String? + ) async throws -> SimulatorDeviceAndRuntime + func findAvailableDevices( platform: Platform, version: Version?, @@ -51,6 +67,21 @@ public protocol SimulatorControlling { /// Returns the simulator destination for the macOS platform func macOSDestination() -> String + + /// Returns the list of simulator devices that are available in the system. + func devices() async throws -> [SimulatorDevice] + + /// Returns the list of simulator runtimes that are available in the system. + func devicesAndRuntimes() async throws -> [SimulatorDeviceAndRuntime] + + /// Boots a simulator, if necessary + /// - Returns: A simulator with the updated `state` + func booted(device: SimulatorDevice) throws -> SimulatorDevice + + /// Boots a simulator, if necessary + /// - Parameters: + /// - forced: If `true`, booting of the simulator is forced + func booted(device: SimulatorDevice, forced: Bool) throws -> SimulatorDevice } public enum SimulatorControllerError: Equatable, FatalError { @@ -81,12 +112,24 @@ public enum SimulatorControllerError: Equatable, FatalError { public final class SimulatorController: SimulatorControlling { private let jsonDecoder = JSONDecoder() + private let userInputReader: UserInputReading - public init() {} + private let system: Systeming + private let devEnvironment: DeveloperEnvironmenting + + public init( + userInputReader: UserInputReading = UserInputReader(), + system: Systeming = System.shared, + devEnvironment: DeveloperEnvironmenting = DeveloperEnvironment.shared + ) { + self.userInputReader = userInputReader + self.system = system + self.devEnvironment = devEnvironment + } /// Returns the list of simulator devices that are available in the system. - func devices() async throws -> [SimulatorDevice] { - let output = try await System.shared.runAndCollectOutput(["/usr/bin/xcrun", "simctl", "list", "devices", "--json"]) + public func devices() async throws -> [SimulatorDevice] { + let output = try await system.runAndCollectOutput(["/usr/bin/xcrun", "simctl", "list", "devices", "--json"]) let data = output.standardOutput.data(using: .utf8)! let json = try JSONSerialization.jsonObject(with: data, options: []) guard let dictionary = json as? [String: Any], @@ -108,7 +151,7 @@ public final class SimulatorController: SimulatorControlling { /// Returns the list of simulator runtimes that are available in the system. func runtimes() async throws -> [SimulatorRuntime] { - let output = try await System.shared.runAndCollectOutput(["/usr/bin/xcrun", "simctl", "list", "runtimes", "--json"]) + let output = try await system.runAndCollectOutput(["/usr/bin/xcrun", "simctl", "list", "runtimes", "--json"]) let data = output.standardOutput.data(using: .utf8)! let json = try JSONSerialization.jsonObject(with: data, options: []) guard let dictionary = json as? [String: Any], @@ -126,7 +169,7 @@ public final class SimulatorController: SimulatorControlling { /// - platform: Optionally filter by platform /// - deviceName: Optionally filter by device name /// - Returns: the list of simulator devices and runtimes. - func devicesAndRuntimes() async throws -> [SimulatorDeviceAndRuntime] { + public func devicesAndRuntimes() async throws -> [SimulatorDeviceAndRuntime] { async let runtimesTask = runtimes() async let devicesTask = devices() let (runtimes, devices) = try await (runtimesTask, devicesTask) @@ -143,7 +186,6 @@ public final class SimulatorController: SimulatorControlling { deviceName: String? ) async throws -> [SimulatorDeviceAndRuntime] { let devicesAndRuntimes = try await devicesAndRuntimes() - let maxRuntimeVersion = devicesAndRuntimes.map(\.runtime.version).max() let availableDevices = devicesAndRuntimes .sorted(by: { $0.runtime.version >= $1.runtime.version }) .filter { simulatorDeviceAndRuntime in @@ -160,13 +202,18 @@ public final class SimulatorController: SimulatorControlling { guard simulatorDeviceAndRuntime.device.name == deviceName else { return false } } - if version == nil, let maxRuntimeVersion { - guard simulatorDeviceAndRuntime.runtime.version == maxRuntimeVersion else { return false } - } + return true + } + + let maxRuntimeVersion = availableDevices.map(\.runtime.version).max() + return availableDevices.filter { simulatorDeviceAndRuntime in + if version == nil, let maxRuntimeVersion { + return simulatorDeviceAndRuntime.runtime.version == maxRuntimeVersion + } else { return true } - return availableDevices + } } public func findAvailableDevice( @@ -182,22 +229,62 @@ public final class SimulatorController: SimulatorControlling { deviceName: deviceName ) guard let device = availableDevices.first(where: { !$0.device.isShutdown }) ?? availableDevices.first - else { throw SimulatorControllerError.deviceNotFound(platform, version, deviceName, try await devicesAndRuntimes()) + else { + throw SimulatorControllerError.deviceNotFound(platform, version, deviceName, try await devicesAndRuntimes()) } return device } + public func askForAvailableDevice( + platform: Platform, + version: Version?, + minVersion: Version?, + deviceName: String? + ) async throws -> SimulatorDeviceAndRuntime { + let availableDevices = try await findAvailableDevices( + platform: platform, + version: version, + minVersion: minVersion, + deviceName: deviceName + ) + if availableDevices.isEmpty { + throw SimulatorControllerError.deviceNotFound( + platform, + version, + deviceName, + try await devicesAndRuntimes() + ) + } + let availableBootedDevices = availableDevices.filter { !$0.device.isShutdown } + if availableBootedDevices.count == 1, let onlyOption = availableBootedDevices.first { + return onlyOption + } + return try userInputReader.readValue( + asking: "Select the simulator device where you want to run the app:", + values: availableDevices, + valueDescription: { "\($0.device.name) (\($0.device.udid)" } + ) + } + public func installApp(at path: AbsolutePath, device: SimulatorDevice) throws { logger.debug("Installing app at \(path) on simulator device with id \(device.udid)") - let device = try device.booted() - try System.shared.run(["/usr/bin/xcrun", "simctl", "install", device.udid, path.pathString]) + let device = try device.booted(using: system) + try system.run(["/usr/bin/xcrun", "simctl", "install", device.udid, path.pathString]) } public func launchApp(bundleId: String, device: SimulatorDevice, arguments: [String]) throws { logger.debug("Launching app with bundle id \(bundleId) on simulator device with id \(device.udid)") - let device = try device.booted() - try System.shared.run(["/usr/bin/open", "-a", "Simulator"]) - try System.shared.run(["/usr/bin/xcrun", "simctl", "launch", device.udid, bundleId] + arguments) + let device = try device.booted(using: system) + try system.run(["/usr/bin/open", "-a", "Simulator"]) + try system.run(["/usr/bin/xcrun", "simctl", "launch", device.udid, bundleId] + arguments) + } + + public func booted(device: SimulatorDevice) throws -> SimulatorDevice { + try device.booted(using: system) + } + + public func booted(device: SimulatorDevice, forced: Bool) throws -> SimulatorDevice { + try device.booted(using: system, forced: forced) } /// https://www.mokacoding.com/blog/xcodebuild-destination-options/ @@ -228,7 +315,7 @@ public final class SimulatorController: SimulatorControlling { public func macOSDestination() -> String { let arch: String - switch DeveloperEnvironment.shared.architecture { + switch devEnvironment.architecture { case .arm64: arch = "arm64" case .x8664: @@ -241,9 +328,19 @@ public final class SimulatorController: SimulatorControlling { extension SimulatorDevice { /// Attempts to boot the simulator. /// - returns: The `SimulatorDevice` with updated `isShutdown` field. - fileprivate func booted() throws -> Self { - guard isShutdown else { return self } - try System.shared.run(["/usr/bin/xcrun", "simctl", "boot", udid]) + fileprivate func booted(using system: Systeming, forced: Bool = false) throws -> Self { + guard isShutdown || forced else { return self } + do { + try system.run(["/usr/bin/xcrun", "simctl", "boot", udid]) + } catch { + if forced, let error = error as? FatalError, + error.description.contains("Unable to boot device in current state: Booted") + { + // noop + } else { + throw error + } + } return SimulatorDevice( dataPath: dataPath, logPath: logPath, diff --git a/Sources/TuistCore/Simulator/SimulatorDevice.swift b/Sources/TuistCore/Simulator/SimulatorDevice.swift index eb76245a558..a435d77ccf1 100644 --- a/Sources/TuistCore/Simulator/SimulatorDevice.swift +++ b/Sources/TuistCore/Simulator/SimulatorDevice.swift @@ -1,8 +1,9 @@ import Foundation -import TSCBasic +import Path +import TuistSupport /// It represents a simulator device. Devices are obtained using Xcode's CLI simctl -public struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible { +public struct SimulatorDevice: Codable, Hashable, CustomStringConvertible, Equatable { /// Device data path. public let dataPath: AbsolutePath @@ -61,3 +62,31 @@ public struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible { self.runtimeIdentifier = runtimeIdentifier } } + +#if DEBUG + extension SimulatorDevice { + public static func test( + dataPath: AbsolutePath = "/Library/Developer/CoreSimulator/Devices/3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC/data", + logPath: AbsolutePath = "/Library/Logs/CoreSimulator/3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC", + udid: String = "3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC", + isAvailable: Bool = true, + deviceTypeIdentifier: String = "com.apple.CoreSimulator.SimDeviceType.iPad-Air--3rd-generation-", + state: String = "Shutdown", + name: String = "iPad Air (3rd generation)", + availabilityError: String? = nil, + runtimeIdentifier: String = "com.apple.CoreSimulator.SimRuntime.iOS-13-5" + ) -> Self { + Self( + dataPath: dataPath, + logPath: logPath, + udid: udid, + isAvailable: isAvailable, + deviceTypeIdentifier: deviceTypeIdentifier, + state: state, + name: name, + availabilityError: availabilityError, + runtimeIdentifier: runtimeIdentifier + ) + } + } +#endif diff --git a/Sources/TuistCore/Simulator/SimulatorDeviceAndRuntime.swift b/Sources/TuistCore/Simulator/SimulatorDeviceAndRuntime.swift index 5e11475aec4..f582f4c102e 100644 --- a/Sources/TuistCore/Simulator/SimulatorDeviceAndRuntime.swift +++ b/Sources/TuistCore/Simulator/SimulatorDeviceAndRuntime.swift @@ -1,9 +1,27 @@ import Foundation -public struct SimulatorDeviceAndRuntime: Hashable { +public struct SimulatorDeviceAndRuntime: Codable, Identifiable, Hashable, Equatable, Sendable { + public var id: String { + device.udid + } + /// Device public let device: SimulatorDevice /// Device's runtime. public let runtime: SimulatorRuntime } + +#if DEBUG + extension SimulatorDeviceAndRuntime { + public static func test( + device: SimulatorDevice = .test(), + runtime: SimulatorRuntime = .test() + ) -> Self { + Self( + device: device, + runtime: runtime + ) + } + } +#endif diff --git a/Sources/TuistCore/Simulator/SimulatorRuntime.swift b/Sources/TuistCore/Simulator/SimulatorRuntime.swift index c4f94ef603d..91bbae29f99 100644 --- a/Sources/TuistCore/Simulator/SimulatorRuntime.swift +++ b/Sources/TuistCore/Simulator/SimulatorRuntime.swift @@ -1,9 +1,11 @@ import Foundation -import TSCBasic +import Path +import TuistSupport +import XcodeGraph /// It represents a runtime that is available in the system. The list of available runtimes is obtained /// using Xcode's simctl cli tool. -public struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible { +public struct SimulatorRuntime: Equatable, Codable, Hashable, CustomStringConvertible { /// Runtime bundle path (e.g. /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime) public let bundlePath: AbsolutePath @@ -25,6 +27,14 @@ public struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible { // Name of the runtime (e.g. iOS 13.5) public let name: String + public var platform: Platform? { + // We pluck out the platform name from the name of the runtime (e.g. iOS 13.5) + guard let platformName = name.components(separatedBy: " ").first + else { return nil } + + return Platform(commandLineValue: platformName) + } + public init( bundlePath: AbsolutePath, buildVersion: String, @@ -57,3 +67,29 @@ public struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible { name } } + +#if DEBUG + extension SimulatorRuntime { + public static func test( + bundlePath: AbsolutePath = + "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime", + buildVersion: String = "17F61", + runtimeRoot: AbsolutePath = + "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot", + identifier: String = "com.apple.CoreSimulator.SimRuntime.iOS-13-5", + version: SimulatorRuntimeVersion = "13.5", + isAvailable: Bool = true, + name: String = "iOS 13.5" + ) -> Self { + Self( + bundlePath: bundlePath, + buildVersion: buildVersion, + runtimeRoot: runtimeRoot, + identifier: identifier, + version: version, + isAvailable: isAvailable, + name: name + ) + } + } +#endif diff --git a/Sources/TuistCore/Simulator/SimulatorRuntimeVersion.swift b/Sources/TuistCore/Simulator/SimulatorRuntimeVersion.swift index a531a36c7ed..0c5884850e1 100644 --- a/Sources/TuistCore/Simulator/SimulatorRuntimeVersion.swift +++ b/Sources/TuistCore/Simulator/SimulatorRuntimeVersion.swift @@ -1,6 +1,6 @@ import Foundation -public struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleByStringLiteral, Comparable, Decodable { +public struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleByStringLiteral, Comparable, Codable { // MARK: - Attributes public let major: Int @@ -20,6 +20,11 @@ public struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, Expres self.init(stringLiteral: try container.decode(String.self)) } + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } + // MARK: - Internal func flattened() -> SimulatorRuntimeVersion { diff --git a/Sources/TuistCore/Utils/DefaultConfigurationFetcher.swift b/Sources/TuistCore/Utils/DefaultConfigurationFetcher.swift new file mode 100644 index 00000000000..5e3f820f315 --- /dev/null +++ b/Sources/TuistCore/Utils/DefaultConfigurationFetcher.swift @@ -0,0 +1,81 @@ +import Foundation +import Mockable +import TuistSupport +import XcodeGraph + +@Mockable +public protocol DefaultConfigurationFetching { + func fetch( + configuration: String?, + config: TuistCore.Config, + graph: XcodeGraph.Graph + ) throws -> String +} + +enum DefaultConfigurationFetcherError: FatalError, Equatable { + case debugBuildConfigurationNotFound + case configurationNotFound(String, available: [String]) + case defaultConfigurationNotFound(String, available: [String]) + + var type: ErrorType { + switch self { + case .debugBuildConfigurationNotFound, .configurationNotFound, .defaultConfigurationNotFound: + return .abort + } + } + + var description: String { + switch self { + case .debugBuildConfigurationNotFound: + return "We couldn't find a build configuration of variant 'debug' for caching. Make sure one exists in the project." + case let .configurationNotFound(configuration, available): + return "We couldn't find the configuration \(configuration) in the project. The configurations available are: \(available.joined(separator: ", "))" + case let .defaultConfigurationNotFound(configuration, available): + return "We couldn't find the default configuration \(configuration) specified in your Config.swift in the project. The configurations available are: \(available.joined(separator: ", "))" + } + } +} + +public struct DefaultConfigurationFetcher: DefaultConfigurationFetching { + public init() {} + + public func fetch( + configuration: String?, + config: TuistCore.Config, + graph: XcodeGraph.Graph + ) throws -> String { + let allProjectConfigurations = Set(graph.projects.values.map(\.settings).flatMap(\.configurations.keys)).sorted() + + if let configuration { + if allProjectConfigurations.first(where: { $0.name == configuration }) != nil { + return configuration + } else { + throw DefaultConfigurationFetcherError.configurationNotFound( + configuration, + available: allProjectConfigurations.map(\.name) + ) + } + } + + if let defaultConfiguration = config.generationOptions.defaultConfiguration { + if allProjectConfigurations.first(where: { $0.name == defaultConfiguration }) != nil { + return defaultConfiguration + } else { + throw DefaultConfigurationFetcherError.defaultConfigurationNotFound( + defaultConfiguration, + available: allProjectConfigurations.map(\.name) + ) + } + } + + guard let debugConfigurationName = graph.projects.values.map(\.settings).flatMap(\.configurations.keys).sorted() + .first(where: { + $0.variant == .debug + })?.name + else { + throw DefaultConfigurationFetcherError.debugBuildConfigurationNotFound + } + + return debugConfigurationName + } +} diff --git a/Sources/TuistCore/Utils/RootDirectoryLocator.swift b/Sources/TuistCore/Utils/RootDirectoryLocator.swift index 72c3ea9735e..abe35c507f7 100644 --- a/Sources/TuistCore/Utils/RootDirectoryLocator.swift +++ b/Sources/TuistCore/Utils/RootDirectoryLocator.swift @@ -1,7 +1,9 @@ import Foundation -import TSCBasic +import Mockable +import Path import TuistSupport +@Mockable public protocol RootDirectoryLocating { /// Given a path, it finds the root directory by traversing up the hierarchy. /// @@ -16,7 +18,7 @@ public protocol RootDirectoryLocating { public final class RootDirectoryLocator: RootDirectoryLocating { private let fileHandler: FileHandling = FileHandler.shared /// This cache avoids having to traverse the directories hierarchy every time the locate method is called. - @Atomic private var cache: [AbsolutePath: AbsolutePath] = [:] + private let cache: ThreadSafe<[AbsolutePath: AbsolutePath]> = ThreadSafe([:]) public init() {} @@ -45,7 +47,7 @@ public final class RootDirectoryLocator: RootDirectoryLocating { // MARK: - Fileprivate fileprivate func cached(path: AbsolutePath) -> AbsolutePath? { - cache[path] + cache.value[path] } /// This method caches the root directory of path, and all its parents up to the root directory. @@ -54,10 +56,10 @@ public final class RootDirectoryLocator: RootDirectoryLocating { /// - path: Path for which we are caching the root directory. fileprivate func cache(rootDirectory: AbsolutePath, for path: AbsolutePath) { if path != rootDirectory { - _cache.modify { $0[path] = rootDirectory } + cache.mutate { $0[path] = rootDirectory } cache(rootDirectory: rootDirectory, for: path.parentDirectory) } else if path == rootDirectory { - _cache.modify { $0[path] = rootDirectory } + cache.mutate { $0[path] = rootDirectory } } } } diff --git a/Sources/TuistCore/Utils/XcodeProjectBuildDirectoryLocator.swift b/Sources/TuistCore/Utils/XcodeProjectBuildDirectoryLocator.swift index 82cc0947f3b..df43c79e888 100644 --- a/Sources/TuistCore/Utils/XcodeProjectBuildDirectoryLocator.swift +++ b/Sources/TuistCore/Utils/XcodeProjectBuildDirectoryLocator.swift @@ -1,8 +1,10 @@ import Foundation -import TSCBasic -import TuistGraph +import Mockable +import Path import TuistSupport +import XcodeGraph +@Mockable public protocol XcodeProjectBuildDirectoryLocating { /// Locates the build output directory for `xcodebuild` command. /// diff --git a/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift b/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift deleted file mode 100644 index f4007b01c6b..00000000000 --- a/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift +++ /dev/null @@ -1,176 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistSupport -@testable import TuistSupportTesting - -final class MockXcodeBuildController: XcodeBuildControlling { - var buildStub: (( - XcodeBuildTarget, - String, - XcodeBuildDestination?, - Bool, - AbsolutePath?, - Bool, - [XcodeBuildArgument] - ) -> [SystemEvent])? - - func build( - _ target: XcodeBuildTarget, - scheme: String, - destination: XcodeBuildDestination?, - rosetta: Bool, - derivedDataPath: AbsolutePath?, - clean: Bool, - arguments: [XcodeBuildArgument] - ) -> AsyncThrowingStream, Error> { - if let buildStub { - return buildStub( - target, - scheme, - destination, - rosetta, - derivedDataPath, - clean, - arguments - ).asAsyncThrowingStream() - } else { - return AsyncThrowingStream { - throw TestError( - "\(String(describing: MockXcodeBuildController.self)) received an unexpected call to build" - ) - } - } - } - - var testStub: ( - ( - XcodeBuildTarget, - String, - Bool, - XcodeBuildDestination, - Bool, - AbsolutePath?, - AbsolutePath?, - [XcodeBuildArgument], - Int, - [TestIdentifier], - [TestIdentifier], - TestPlanConfiguration? - ) - -> [SystemEvent] - )? - var testErrorStub: Error? - func test( - _ target: XcodeBuildTarget, - scheme: String, - clean: Bool, - destination: XcodeBuildDestination, - rosetta: Bool, - derivedDataPath: AbsolutePath?, - resultBundlePath: AbsolutePath?, - arguments: [XcodeBuildArgument], - retryCount: Int, - testTargets: [TestIdentifier], - skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration? - ) -> AsyncThrowingStream, Error> { - if let testStub { - let results = testStub( - target, - scheme, - clean, - destination, - rosetta, - derivedDataPath, - resultBundlePath, - arguments, - retryCount, - testTargets, - skipTestTargets, - testPlanConfiguration - ) - if let testErrorStub { - return AsyncThrowingStream { - throw testErrorStub - } - } else { - return results.asAsyncThrowingStream() - } - } else { - return AsyncThrowingStream { - throw TestError( - "\(String(describing: MockXcodeBuildController.self)) received an unexpected call to test" - ) - } - } - } - - var archiveStub: ( - (XcodeBuildTarget, String, Bool, AbsolutePath, [XcodeBuildArgument], AbsolutePath?) - -> [SystemEvent] - )? - func archive( - _ target: XcodeBuildTarget, - scheme: String, - clean: Bool, - archivePath: AbsolutePath, - arguments: [XcodeBuildArgument], - derivedDataPath: AbsolutePath? - ) -> AsyncThrowingStream, Error> { - if let archiveStub { - return archiveStub(target, scheme, clean, archivePath, arguments, derivedDataPath) - .asAsyncThrowingStream() - } else { - return AsyncThrowingStream { - throw TestError( - "\(String(describing: MockXcodeBuildController.self)) received an unexpected call to archive" - ) - } - } - } - - var createXCFrameworkStub: ( - ([XcodeBuildControllerCreateXCFrameworkArgument], AbsolutePath) - -> [SystemEvent] - )? - func createXCFramework( - arguments: [XcodeBuildControllerCreateXCFrameworkArgument], - output: AbsolutePath - ) -> AsyncThrowingStream, Error> { - if let createXCFrameworkStub { - return createXCFrameworkStub(arguments, output).asAsyncThrowingStream() - } else { - return AsyncThrowingStream { - throw TestError( - "\(String(describing: MockXcodeBuildController.self)) received an unexpected call to createXCFramework" - ) - } - } - } - - var showBuildSettingsStub: ((XcodeBuildTarget, String, String, AbsolutePath?) -> [String: XcodeBuildSettings])? - func showBuildSettings( - _ target: XcodeBuildTarget, - scheme: String, - configuration: String, - derivedDataPath: AbsolutePath? - ) throws -> [String: XcodeBuildSettings] { - if let showBuildSettingsStub { - return showBuildSettingsStub(target, scheme, configuration, derivedDataPath) - } else { - throw TestError( - "\(String(describing: MockXcodeBuildController.self)) received an unexpected call to showBuildSettings" - ) - } - } -} - -extension Collection { - func asAsyncThrowingStream() -> AsyncThrowingStream { - var iterator = makeIterator() - return AsyncThrowingStream { - iterator.next() - } - } -} diff --git a/Sources/TuistCoreTesting/Cache/MockCacheDirectoriesProvider.swift b/Sources/TuistCoreTesting/Cache/MockCacheDirectoriesProvider.swift deleted file mode 100644 index f16038b6698..00000000000 --- a/Sources/TuistCoreTesting/Cache/MockCacheDirectoriesProvider.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistCore - -public final class MockCacheDirectoriesProvider: CacheDirectoriesProviding { - private let directory: TemporaryDirectory - public var cacheDirectoryStub: AbsolutePath? - - private var cacheDirectory: AbsolutePath { - cacheDirectoryStub ?? directory.path.appending(component: "Cache") - } - - public func cacheDirectory(for category: CacheCategory) -> AbsolutePath { - cacheDirectory.appending(component: category.directoryName) - } - - public init() throws { - directory = try TemporaryDirectory(removeTreeOnDeinit: true) - } -} diff --git a/Sources/TuistCoreTesting/Cache/MockCacheDirectoriesProviderFactory.swift b/Sources/TuistCoreTesting/Cache/MockCacheDirectoriesProviderFactory.swift deleted file mode 100644 index b9efcedc246..00000000000 --- a/Sources/TuistCoreTesting/Cache/MockCacheDirectoriesProviderFactory.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph -import TuistSupport -@testable import TuistCore - -public final class MockCacheDirectoriesProviderFactory: CacheDirectoriesProviderFactoring { - public var cacheDirectoriesStub: ((Config?) -> CacheDirectoriesProviding)? - public var cacheDirectoriesConfig: Config? - private let provider: CacheDirectoriesProviding - - public init(provider: CacheDirectoriesProviding) { - self.provider = provider - } - - public func cacheDirectories(config: Config?) -> CacheDirectoriesProviding { - cacheDirectoriesConfig = config - return cacheDirectoriesStub?(config) ?? provider - } -} diff --git a/Sources/TuistCoreTesting/Cache/MockContentHasher.swift b/Sources/TuistCoreTesting/Cache/MockContentHasher.swift deleted file mode 100644 index 42d59844e7d..00000000000 --- a/Sources/TuistCoreTesting/Cache/MockContentHasher.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore - -public final class MockContentHasher: ContentHashing { - public init() {} - - public var hashDataSpy: Data? - public var hashDataCallCount = 0 - public func hash(_ data: Data) throws -> String { - hashDataSpy = data - hashDataCallCount += 1 - return hashDataSpy.map { "\(String(describing: $0.base64EncodedString()))-hash" } ?? "" - } - - public var hashStringCallCount: Int = 0 - public var hashStub: ((String) throws -> String)? - public func hash(_ string: String) throws -> String { - hashStringCallCount += 1 - return try hashStub?(string) ?? "\(string)-hash" - } - - public var hashStringsSpy: [String]? - public var hashStringsCallCount = 0 - public func hash(_ strings: [String]) throws -> String { - hashStringsSpy = strings - hashStringsCallCount += 1 - return strings.joined(separator: ";") - } - - public var stubHashForPath: [AbsolutePath: String] = [:] - public var hashPathCallCount = 0 - public func hash(path filePath: AbsolutePath) throws -> String { - hashPathCallCount += 1 - return stubHashForPath[filePath] ?? "" - } - - public var hashDictionarySpy: [String: String]? - public var hashDictionaryCallCount = 0 - public func hash(_ dictionary: [String: String]) throws -> String { - hashDictionaryCallCount += 1 - hashDictionarySpy = dictionary - return dictionary.map { "\($0):\($1)" }.joined(separator: "-") - } -} diff --git a/Sources/TuistCoreTesting/Cloud/CloudResponse+TestData.swift b/Sources/TuistCoreTesting/Cloud/CloudResponse+TestData.swift deleted file mode 100644 index 6b093651d38..00000000000 --- a/Sources/TuistCoreTesting/Cloud/CloudResponse+TestData.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -@testable import TuistCore - -extension CloudResponse { - static func test(status: String = "status", data: T) -> CloudResponse { - CloudResponse(status: status, data: data) - } -} diff --git a/Sources/TuistCoreTesting/Cloud/CloudResponseError+TestData.swift b/Sources/TuistCoreTesting/Cloud/CloudResponseError+TestData.swift deleted file mode 100644 index 9cac0a21de5..00000000000 --- a/Sources/TuistCoreTesting/Cloud/CloudResponseError+TestData.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -@testable import TuistCore - -extension CloudResponseError.Error { - public static func test(code: String = "Code", message: String = "Message") -> CloudResponseError.Error { - .init(code: code, message: message) - } -} - -extension CloudResponseError { - public static func test(status: String = "Error status", errors: [Error]? = [.test()]) -> CloudResponseError { - .init(status: status, errors: errors) - } -} diff --git a/Sources/TuistCoreTesting/Cloud/MockCloudClient.swift b/Sources/TuistCoreTesting/Cloud/MockCloudClient.swift deleted file mode 100644 index 09f4f3b038b..00000000000 --- a/Sources/TuistCoreTesting/Cloud/MockCloudClient.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation -import TuistSupport - -@testable import TuistCore - -public enum MockCloudClientingError: Error { - case mockedError -} - -public final class MockCloudClient: CloudClienting { - public init() {} - - // MARK: Factories - - public var invokedRequest = false - public var invokedRequestCount = 0 - public var invokedRequestParameterList = [Any]() - - private var stubbedResponse: HTTPURLResponse? - private var stubbedObject: Any? - private var stubbedError: Error? - - public var stubbedResponsePerURLRequest: [URLRequest: HTTPURLResponse] = [:] - public var stubbedObjectPerURLRequest: [URLRequest: Any] = [:] - public var stubbedErrorPerURLRequest: [URLRequest: Error] = [:] - - // MARK: Configurations - - public func mock(error: Error) { - stubbedError = error - stubbedObject = nil - stubbedResponse = nil - } - - public func mock(object: Any, response: HTTPURLResponse) { - stubbedError = nil - stubbedObject = object - stubbedResponse = response - } - - public func mock( - responsePerURLRequest: [URLRequest: HTTPURLResponse] = [:], - objectPerURLRequest: [URLRequest: Any] = [:], - errorPerURLRequest: [URLRequest: Error] = [:] - ) { - stubbedError = nil - stubbedObject = nil - stubbedResponse = HTTPURLResponse.test() - stubbedResponsePerURLRequest = responsePerURLRequest - stubbedObjectPerURLRequest = objectPerURLRequest - stubbedErrorPerURLRequest = errorPerURLRequest - } - - // MARK: Public Interface - - public func request(_ resource: HTTPResource) async throws -> (object: T, response: HTTPURLResponse) { - invokedRequest = true - invokedRequestCount += 1 - invokedRequestParameterList.append(resource) - - let urlRequest = resource.request() - let errorCandidate = stubbedErrorPerURLRequest[urlRequest] ?? stubbedError - if let error = errorCandidate { - throw error - } else { - let objectCandidate = stubbedObjectPerURLRequest[urlRequest] ?? stubbedObject - guard let object = objectCandidate as? T - else { - fatalError( - "This function input parameter type should be the same as the one provided in this object's initializer.\nReceived type: \(String(describing: objectCandidate.self))\nExpected type: \(T.self)" - ) - } - let responseCandidate = stubbedResponsePerURLRequest[urlRequest] ?? stubbedResponse - return (object, responseCandidate!) - } - } -} diff --git a/Sources/TuistCoreTesting/Extensions/TuistTestCase+LintingIssue.swift b/Sources/TuistCoreTesting/Extensions/TuistTestCase+LintingIssue.swift index e2977740f04..e2ba2195e99 100644 --- a/Sources/TuistCoreTesting/Extensions/TuistTestCase+LintingIssue.swift +++ b/Sources/TuistCoreTesting/Extensions/TuistTestCase+LintingIssue.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupportTesting import XCTest diff --git a/Sources/TuistCoreTesting/Graph/DependenciesGraph+TestData.swift b/Sources/TuistCoreTesting/Graph/DependenciesGraph+TestData.swift new file mode 100644 index 00000000000..e05b573528e --- /dev/null +++ b/Sources/TuistCoreTesting/Graph/DependenciesGraph+TestData.swift @@ -0,0 +1,767 @@ +import Foundation +import Path +import ProjectDescription +import TuistCore +import TuistSupport +import TuistSupportTesting + +extension TuistCore.DependenciesGraph { + /// A snapshot of `graph.json` file. + public static var testJson: String { + """ + { + "externalDependencies" : { + "RxSwift" : [ + { + "kind" : "xcframework", + "path" : "/Tuist/Dependencies/SwiftPackageManager/RxSwift.xcframework" + } + ] + }, + "externalProjects": [] + } + """ + } + + public static func test( + externalDependencies: [String: [TargetDependency]] = [:], + externalProjects: [Path: Project] = [:] + ) -> Self { + .init(externalDependencies: externalDependencies, externalProjects: externalProjects) + } + + public static func testXCFramework( + name: String = "Test", + // swiftlint:disable:next force_try + path: Path = .path(AbsolutePath.root.appending(try! RelativePath(validating: "Test.xcframework")).pathString) + ) -> Self { + let externalDependencies = [name: [TargetDependency.xcframework(path: path)]] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [:] + ) + } + + // swiftlint:disable:next function_body_length + public static func test( + spmFolder: Path, + packageFolder: Path, + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign], + fileHandler: FileHandler + ) throws -> Self { + try fileHandler.createFolder(try AbsolutePath(validating: "\(packageFolder.pathString)/customPath/resources")) + + let externalDependencies: [String: [TargetDependency]] = [ + "Tuist": [ + .project( + target: "Tuist", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "Tuist", + destinations: destinations, + product: .staticFramework, + productName: "Tuist", + bundleId: "Tuist", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + .glob( + "\(packageFolder.pathString)/customPath/customSources/**", + excluding: "\(packageFolder.pathString)/customPath/excluded/sources/**" + ), + ], + resources: [ + .folderReference(path: "\(packageFolder.pathString)/customPath/resources", tags: []), + ], + dependencies: [ + .target(name: "TuistKit"), + .project( + target: "ALibrary", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "ADependency"), + condition: .when([.ios]) + ), + .project( + target: "ALibraryUtils", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "ADependency"), + condition: .when([.ios]) + ), + .sdk(name: "WatchKit", type: .framework, status: .required, condition: .when([.watchos])), + ], + settings: Self.spmSettings(packageName: "Tuist", with: [ + "HEADER_SEARCH_PATHS": [ + "$(SRCROOT)/customPath/cSearchPath", + "$(SRCROOT)/customPath/cxxSearchPath", + ], + "OTHER_CFLAGS": ["CUSTOM_C_FLAG"], + "OTHER_CPLUSPLUSFLAGS": ["CUSTOM_CXX_FLAG"], + "OTHER_SWIFT_FLAGS": ["CUSTOM_SWIFT_FLAG1", "CUSTOM_SWIFT_FLAG2"], + "GCC_PREPROCESSOR_DEFINITIONS": ["CXX_DEFINE=CXX_VALUE", "C_DEFINE=C_VALUE"], + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "SWIFT_DEFINE", + ]) + ), + .target( + name: "TuistKit", + destinations: destinations, + product: .staticFramework, + productName: "TuistKit", + bundleId: "TuistKit", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/TuistKit/**", + ], + dependencies: [ + .project( + target: "AnotherLibrary", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "another-dependency") + ), + ], + settings: Self.spmSettings(packageName: "TuistKit") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "test", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings( + base: [ + "GCC_C_LANGUAGE_STANDARD": "c99", + ], + configurations: [ + .debug(name: .debug), + .release(name: .release), + ] + ), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } + + // swiftlint:disable:next function_body_length + public static func aDependency( + spmFolder: Path, + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + ) -> Self { + let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "ADependency") + + let externalDependencies: [String: [TargetDependency]] = [ + "ALibrary": [ + .project( + target: "ALibrary", + path: packageFolder + ), + .project( + target: "ALibraryUtils", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "ALibrary", + destinations: destinations, + product: .staticFramework, + productName: "ALibrary", + bundleId: "ALibrary", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/ALibrary/**", + ], + dependencies: [ + .target( + name: "ALibraryUtils" + ), + ], + settings: Self.spmSettings(packageName: "ALibrary") + ), + .target( + name: "ALibraryUtils", + destinations: destinations, + product: .staticFramework, + productName: "ALibraryUtils", + bundleId: "ALibraryUtils", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/ALibraryUtils/**", + ], + settings: Self.spmSettings(packageName: "ALibraryUtils") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "a-dependency", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings( + configurations: [ + .debug(name: .debug), + .release(name: .release), + ] + ), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } + + public static func anotherDependency( + spmFolder: Path, + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + ) -> Self { + let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "another-dependency") + + let externalDependencies: [String: [TargetDependency]] = [ + "AnotherLibrary": [ + .project( + target: "AnotherLibrary", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "AnotherLibrary", + destinations: destinations, + product: .staticFramework, + productName: "AnotherLibrary", + bundleId: "AnotherLibrary", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/AnotherLibrary/**", + ], + settings: Self.spmSettings(packageName: "AnotherLibrary") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "another-dependency", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings( + configurations: [ + .debug(name: .debug), + .release(name: .release), + ] + ), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } + + public static func alamofire( + spmFolder: Path, + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + ) -> Self { + let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "Alamofire") + + let externalDependencies: [String: [TargetDependency]] = [ + "Alamofire": [ + .project( + target: "Alamofire", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "Alamofire", + destinations: destinations, + product: .staticFramework, + productName: "Alamofire", + bundleId: "Alamofire", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Source/**", + ], + dependencies: [ + .sdk( + name: "CFNetwork", + type: .framework, + status: .required, + condition: .when([.ios, .macos, .tvos, .watchos]) + ), + ], + settings: Self.spmSettings(packageName: "Alamofire") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "Alamofire", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings(base: ["SWIFT_VERSION": "5.0.0"]), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } + + // swiftlint:disable:next function_body_length + public static func googleAppMeasurement( + spmFolder: Path, + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + ) -> Self { + let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleAppMeasurement") + let artifactsFolder = Self.artifactsFolder(spmFolder: spmFolder, packageName: "GoogleAppMeasurement") + + let externalDependencies = [ + "GoogleAppMeasurement": [ + TargetDependency.project( + target: "GoogleAppMeasurementTarget", + path: packageFolder + ), + ], + "GoogleAppMeasurementWithoutAdIdSupport": [ + TargetDependency.project( + target: "GoogleAppMeasurementWithoutAdIdSupportTarget", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "GoogleAppMeasurementTarget", + destinations: destinations, + product: .staticFramework, + productName: "GoogleAppMeasurementTarget", + bundleId: "GoogleAppMeasurementTarget", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/GoogleAppMeasurementWrapper/**", + ], + dependencies: [ + .xcframework(path: "\(artifactsFolder.pathString)/GoogleAppMeasurement.xcframework"), + .project( + target: "GULAppDelegateSwizzler", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "GULMethodSwizzler", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "GULNSData", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "GULNetwork", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "nanopb", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "nanopb") + ), + .sdk(name: "sqlite3", type: .library, status: .required), + .sdk(name: "c++", type: .library, status: .required), + .sdk(name: "z", type: .library, status: .required), + .sdk(name: "StoreKit", type: .framework, status: .required), + ], + settings: Self.spmSettings(packageName: "GoogleAppMeasurementTarget") + ), + .target( + name: "GoogleAppMeasurementWithoutAdIdSupportTarget", + destinations: destinations, + product: .staticFramework, + productName: "GoogleAppMeasurementWithoutAdIdSupportTarget", + bundleId: "GoogleAppMeasurementWithoutAdIdSupportTarget", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/GoogleAppMeasurementWithoutAdIdSupportWrapper/**", + ], + dependencies: [ + .xcframework( + path: "\(artifactsFolder.pathString)/GoogleAppMeasurementWithoutAdIdSupport.xcframework" + ), + .project( + target: "GULAppDelegateSwizzler", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "GULMethodSwizzler", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "GULNSData", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "GULNetwork", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + ), + .project( + target: "nanopb", + path: Self.packageFolder(spmFolder: spmFolder, packageName: "nanopb") + ), + .sdk(name: "sqlite3", type: .library, status: .required), + .sdk(name: "c++", type: .library, status: .required), + .sdk(name: "z", type: .library, status: .required), + .sdk(name: "StoreKit", type: .framework, status: .required), + ], + settings: Self.spmSettings(packageName: "GoogleAppMeasurementWithoutAdIdSupportTarget") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "GoogleAppMeasurement", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings( + base: [ + "GCC_C_LANGUAGE_STANDARD": "c99", + "CLANG_CXX_LANGUAGE_STANDARD": "gnu++14", + ], + configurations: [ + .debug(name: .debug), + .release(name: .release), + ] + ), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } + + // swiftlint:disable:next function_body_length + public static func googleUtilities( + spmFolder: Path, + customProductTypes: [String: Product] = [:], + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + ) -> Self { + let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") + + let externalDependencies: [String: [TargetDependency]] = [ + "GULAppDelegateSwizzler": [ + .project( + target: "GULAppDelegateSwizzler", + path: packageFolder + ), + ], + "GULMethodSwizzler": [ + .project( + target: "GULMethodSwizzler", + path: packageFolder + ), + ], + "GULNSData": [ + .project( + target: "GULNSData", + path: packageFolder + ), + ], + "GULNetwork": [ + .project( + target: "GULNetwork", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "GULAppDelegateSwizzler", + destinations: destinations, + product: customProductTypes["GULAppDelegateSwizzler"] ?? .staticFramework, + productName: "GULAppDelegateSwizzler", + bundleId: "GULAppDelegateSwizzler", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/GULAppDelegateSwizzler/**", + ], + settings: Self.spmSettings(packageName: "GULAppDelegateSwizzler") + ), + .target( + name: "GULMethodSwizzler", + destinations: destinations, + product: customProductTypes["GULMethodSwizzler"] ?? .staticFramework, + productName: "GULMethodSwizzler", + bundleId: "GULMethodSwizzler", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/GULMethodSwizzler/**", + ], + settings: Self.spmSettings(packageName: "GULMethodSwizzler") + ), + + .target( + name: "GULNSData", + destinations: destinations, + product: customProductTypes["GULNSData"] ?? .staticFramework, + productName: "GULNSData", + bundleId: "GULNSData", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/GULNSData/**", + ], + settings: Self.spmSettings(packageName: "GULNSData") + ), + .target( + name: "GULNetwork", + destinations: destinations, + product: customProductTypes["GULNetwork"] ?? .staticFramework, + productName: "GULNetwork", + bundleId: "GULNetwork", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/GULNetwork/**", + ], + settings: Self.spmSettings(packageName: "GULNetwork") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "GoogleUtilities", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings( + configurations: [ + .debug(name: .debug), + .release(name: .release), + ] + ), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } + + public static func nanopb( + spmFolder: Path, + destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + ) -> Self { + let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "nanopb") + + let externalDependencies = [ + "nanopb": [ + TargetDependency.project( + target: "nanopb", + path: packageFolder + ), + ], + ] + + let targets: [Target] = [ + .target( + name: "nanopb", + destinations: destinations, + product: .staticFramework, + productName: "nanopb", + bundleId: "nanopb", + deploymentTargets: resolveDeploymentTargets(for: destinations), + infoPlist: .default, + sources: [ + "\(packageFolder.pathString)/Sources/nanopb/**", + ], + settings: Self.spmSettings(packageName: "nanopb") + ), + ] + + return .init( + externalDependencies: externalDependencies, + externalProjects: [ + packageFolder: .init( + name: "nanopb", + options: .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: .settings( + configurations: [ + .debug(name: .debug), + .release(name: .release), + ] + ), + targets: targets, + resourceSynthesizers: .default + ), + ] + ) + } +} + +extension DependenciesGraph { + fileprivate static func artifactsFolder(spmFolder: Path, packageName: String) -> Path { + Path("\(spmFolder.pathString)/artifacts/\(packageName)") + } + + fileprivate static func packageFolder(spmFolder: Path, packageName: String) -> Path { + Path("\(spmFolder.pathString)/checkouts/\(packageName)") + } + + static func spmSettings( + packageName: String, + baseSettings: Settings = .settings(), + with customSettings: SettingsDictionary = [:], + moduleMap: String? = nil + ) -> Settings { + let defaultSpmSettings: SettingsDictionary = [ + "ALWAYS_SEARCH_USER_PATHS": "YES", + "GCC_WARN_INHIBIT_ALL_WARNINGS": "YES", + "SWIFT_SUPPRESS_WARNINGS": "YES", + "CLANG_ENABLE_OBJC_WEAK": "NO", + "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER": "NO", + "ENABLE_STRICT_OBJC_MSGSEND": "NO", + "FRAMEWORK_SEARCH_PATHS": ["$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks"], + "GCC_NO_COMMON_BLOCKS": "NO", + "USE_HEADERMAP": "NO", + "OTHER_SWIFT_FLAGS": ["-package-name", packageName.quotedIfContainsSpaces], + ] + var settingsDictionary = defaultSpmSettings.combine(with: customSettings) + + if let moduleMap { + settingsDictionary["MODULEMAP_FILE"] = .string(moduleMap) + } + + if case let .array(headerSearchPaths) = settingsDictionary["HEADER_SEARCH_PATHS"] { + settingsDictionary["HEADER_SEARCH_PATHS"] = .array(["$(inherited)"] + headerSearchPaths) + } + + if case let .array(cDefinitions) = settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] { + settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] = .array( + ["$(inherited)"] + (cDefinitions + ["SWIFT_PACKAGE=1"]) + .sorted() + ) + } else { + settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] = .array(["$(inherited)", "SWIFT_PACKAGE=1"]) + } + + if case let .string(swiftDefinitions) = settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] { + settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = + .string("$(inherited) SWIFT_PACKAGE \(swiftDefinitions)") + } else { + settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = .string("$(inherited) SWIFT_PACKAGE") + } + + if case let .array(cFlags) = settingsDictionary["OTHER_CFLAGS"] { + settingsDictionary["OTHER_CFLAGS"] = .array(["$(inherited)"] + cFlags) + } + + if case let .array(cxxFlags) = settingsDictionary["OTHER_CPLUSPLUSFLAGS"] { + settingsDictionary["OTHER_CPLUSPLUSFLAGS"] = .array(["$(inherited)"] + cxxFlags) + } + + if case let .array(swiftFlags) = settingsDictionary["OTHER_SWIFT_FLAGS"] { + settingsDictionary["OTHER_SWIFT_FLAGS"] = .array(["$(inherited)"] + swiftFlags) + } + + if case let .array(linkerFlags) = settingsDictionary["OTHER_LDFLAGS"] { + settingsDictionary["OTHER_LDFLAGS"] = .array(["$(inherited)"] + linkerFlags) + } + + return .settings( + base: baseSettings.base.combine(with: settingsDictionary), + configurations: baseSettings.configurations, + defaultSettings: baseSettings.defaultSettings + ) + } +} + +extension SettingsDictionary { + /// Combines two `SettingsDictionary`. Instead of overriding values for a duplicate key, it combines them. + func combine(with settings: SettingsDictionary) -> SettingsDictionary { + merging(settings, uniquingKeysWith: { oldValue, newValue in + let newValues: [String] + switch newValue { + case let .string(value): + newValues = [value] + case let .array(values): + newValues = values + } + switch oldValue { + case let .array(values): + return .array(values + newValues) + case let .string(value): + return .array(value.split(separator: " ").map(String.init) + newValues) + } + }) + } +} + +// MARK: - Helpers + +extension DependenciesGraph { + fileprivate static func resolveDeploymentTargets(for destinations: Destinations) -> DeploymentTargets { + let platforms = destinations.platforms + let applicableVersions = PLATFORM_TEST_VERSION.filter { platforms.contains($0.key) } + + return .multiplatform( + iOS: applicableVersions[Platform.iOS], + macOS: applicableVersions[Platform.macOS], + watchOS: applicableVersions[Platform.watchOS], + tvOS: applicableVersions[Platform.tvOS], + visionOS: applicableVersions[Platform.visionOS] + ) + } +} diff --git a/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift b/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift index 4a3d73d3005..f25f0aa31e3 100644 --- a/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift +++ b/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift @@ -1,15 +1,13 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph extension GraphDependencyReference { public static func testFramework( path: AbsolutePath = "/frameworks/tuist.framework", binaryPath: AbsolutePath = "/frameworks/tuist.framework/tuist", - isCarthage: Bool = false, dsymPath: AbsolutePath? = nil, bcsymbolmapPaths: [AbsolutePath] = [], linking: BinaryLinking = .dynamic, @@ -21,7 +19,6 @@ extension GraphDependencyReference { GraphDependencyReference.framework( path: path, binaryPath: binaryPath, - isCarthage: isCarthage, dsymPath: dsymPath, bcsymbolmapPaths: bcsymbolmapPaths, linking: linking, diff --git a/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift b/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift index 5a46f4520a9..9a46859ddc2 100644 --- a/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift +++ b/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift @@ -1,8 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -@testable import TuistGraphTesting +import XcodeGraph public final class MockGraphLoader: GraphLoading { public init() {} diff --git a/Sources/TuistCoreTesting/Graph/WorkspaceWithProjects+TestData.swift b/Sources/TuistCoreTesting/Graph/WorkspaceWithProjects+TestData.swift index 0ba99859edb..8473c96b457 100644 --- a/Sources/TuistCoreTesting/Graph/WorkspaceWithProjects+TestData.swift +++ b/Sources/TuistCoreTesting/Graph/WorkspaceWithProjects+TestData.swift @@ -1,7 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph -import TuistGraphTesting +import Path +import XcodeGraph @testable import TuistCore extension WorkspaceWithProjects { diff --git a/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift b/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift deleted file mode 100644 index 1572f2b29fa..00000000000 --- a/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift +++ /dev/null @@ -1,689 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph -@testable import TuistCore - -// swiftlint:disable:next type_body_length -final class MockGraphTraverser: GraphTraversing { - var invokedNameGetter = false - var invokedNameGetterCount = 0 - var stubbedName: String! = "" - - var name: String { - invokedNameGetter = true - invokedNameGetterCount += 1 - return stubbedName - } - - var invokedHasPackagesGetter = false - var invokedHasPackagesGetterCount = 0 - var stubbedHasPackages: Bool! = false - - var hasPackages: Bool { - invokedHasPackagesGetter = true - invokedHasPackagesGetterCount += 1 - return stubbedHasPackages - } - - var invokedHasRemotePackagesGetter = false - var invokedHasRemotePackagesGetterCount = 0 - var stubbedHasRemotePackages: Bool! = false - - var hasRemotePackages: Bool { - invokedHasRemotePackagesGetter = true - invokedHasRemotePackagesGetterCount += 1 - return stubbedHasRemotePackages - } - - var invokedPathGetter = false - var invokedPathGetterCount = 0 - var stubbedPath: AbsolutePath! - - var path: AbsolutePath { - invokedPathGetter = true - invokedPathGetterCount += 1 - return stubbedPath - } - - var invokedWorkspaceGetter = false - var invokedWorkspaceGetterCount = 0 - var stubbedWorkspace: Workspace! - - var workspace: Workspace { - invokedWorkspaceGetter = true - invokedWorkspaceGetterCount += 1 - return stubbedWorkspace - } - - var invokedProjectsGetter = false - var invokedProjectsGetterCount = 0 - var stubbedProjects: [AbsolutePath: Project]! = [:] - - var projects: [AbsolutePath: Project] { - invokedProjectsGetter = true - invokedProjectsGetterCount += 1 - return stubbedProjects - } - - var invokedTargetsGetter = false - var invokedTargetsGetterCount = 0 - var stubbedTargets: [AbsolutePath: [String: Target]]! = [:] - - var targets: [AbsolutePath: [String: Target]] { - invokedTargetsGetter = true - invokedTargetsGetterCount += 1 - return stubbedTargets - } - - var invokedDependenciesGetter = false - var invokedDependenciesGetterCount = 0 - var stubbedDependencies: [GraphDependency: Set]! = [:] - - var dependencies: [GraphDependency: Set] { - invokedDependenciesGetter = true - invokedDependenciesGetterCount += 1 - return stubbedDependencies - } - - var invokedApps = false - var invokedAppsCount = 0 - var stubbedAppsResult: Set! = [] - - func apps() -> Set { - invokedApps = true - invokedAppsCount += 1 - return stubbedAppsResult - } - - var invokedRootTargets = false - var invokedRootTargetsCount = 0 - var stubbedRootTargetsResult: Set! = [] - - func rootTargets() -> Set { - invokedRootTargets = true - invokedRootTargetsCount += 1 - return stubbedRootTargetsResult - } - - var invokedRootProjects = false - var invokedRootProjectsCount = 0 - var stubbedRootProjectsResult: Set! = [] - - func rootProjects() -> Set { - invokedRootProjects = true - invokedRootProjectsCount += 1 - return stubbedRootProjectsResult - } - - var invokedAllTargets = false - var invokedAllTargetsCount = 0 - var stubbedAllTargetsResult: Set! = [] - - func allTargets() -> Set { - invokedAllTargets = true - invokedAllTargetsCount += 1 - return stubbedAllTargetsResult - } - - var invokedAllTargetsTopologicalSorted = false - var invokedAllTargetsTopologicalSortedCount = 0 - var stubbedAllTargetsTopologicalSortedResult: [GraphTarget]! = [] - - func allTargetsTopologicalSorted() throws -> [GraphTarget] { - invokedAllTargetsTopologicalSorted = true - invokedAllTargetsTopologicalSortedCount += 1 - return stubbedAllTargetsTopologicalSortedResult - } - - var invokedAllInternalTargets = false - var invokedAllInternalTargetsCount = 0 - var stubbedAllInternalTargetsResult: Set! = [] - - func allInternalTargets() -> Set { - invokedAllInternalTargets = true - invokedAllInternalTargetsCount += 1 - return stubbedAllInternalTargetsResult - } - - var invokedAllTestPlans = false - var invokedAllTestPlansCount = 0 - var stubbedAllTestPlansResult: Set! = [] - - func allTestPlans() -> Set { - invokedAllTestPlans = true - invokedAllTestPlansCount += 1 - return stubbedAllTestPlansResult - } - - var invokedTestPlan = false - var invokedTestPlanCount = 0 - var invokedTestPlanParameters: String? - var invokedTestPlanParametersList = [String]() - var stubbedTestPlanResult: TestPlan? - - func testPlan(name: String) -> TestPlan? { - invokedTestPlan = true - invokedTestPlanCount += 1 - invokedTestPlanParameters = name - invokedTestPlanParametersList.append(name) - return stubbedTestPlanResult - } - - var invokedPrecompiledFrameworksPaths = false - var invokedPrecompiledFrameworksPathsCount = 0 - var stubbedPrecompiledFrameworksPathsResult: Set! = [] - - func precompiledFrameworksPaths() -> Set { - invokedPrecompiledFrameworksPaths = true - invokedPrecompiledFrameworksPathsCount += 1 - return stubbedPrecompiledFrameworksPathsResult - } - - var invokedTargetsProduct = false - var invokedTargetsProductCount = 0 - var invokedTargetsProductParameters: (product: Product, Void)? - var invokedTargetsProductParametersList = [(product: Product, Void)]() - var stubbedTargetsProductResult: Set! = [] - - func targets(product: Product) -> Set { - invokedTargetsProduct = true - invokedTargetsProductCount += 1 - invokedTargetsProductParameters = (product, ()) - invokedTargetsProductParametersList.append((product, ())) - return stubbedTargetsProductResult - } - - var invokedBuildsForMacCatalyst = false - var invokedBuildsForMacCatalystCount = 0 - var invokedBuildsForMacCatalystParameters: (path: AbsolutePath, name: String)? - var invokedBuildsForMacCatalystParametersList = [(path: AbsolutePath, name: String)]() - var stubbedBuildsForMacCatalystResult: Bool! - - func buildsForMacCatalyst(path: TSCBasic.AbsolutePath, name: String) -> Bool { - invokedBuildsForMacCatalyst = true - invokedBuildsForMacCatalystCount += 1 - invokedBuildsForMacCatalystParameters = (path, name) - invokedBuildsForMacCatalystParametersList.append((path, name)) - return stubbedBuildsForMacCatalystResult - } - - var invokedTarget = false - var invokedTargetCount = 0 - var invokedTargetParameters: (path: AbsolutePath, name: String)? - var invokedTargetParametersList = [(path: AbsolutePath, name: String)]() - var stubbedTargetResult: GraphTarget! - - func target(path: AbsolutePath, name: String) -> GraphTarget? { - invokedTarget = true - invokedTargetCount += 1 - invokedTargetParameters = (path, name) - invokedTargetParametersList.append((path, name)) - return stubbedTargetResult - } - - var invokedTargetsAt = false - var invokedTargetsAtCount = 0 - var invokedTargetsAtParameters: (path: AbsolutePath, Void)? - var invokedTargetsAtParametersList = [(path: AbsolutePath, Void)]() - var stubbedTargetsAtResult: Set! = [] - - func targets(at path: AbsolutePath) -> Set { - invokedTargetsAt = true - invokedTargetsAtCount += 1 - invokedTargetsAtParameters = (path, ()) - invokedTargetsAtParametersList.append((path, ())) - 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 - var invokedDirectLocalTargetDependenciesParameters: ( - path: AbsolutePath, - name: String - )? - var invokedDirectLocalTargetDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedDirectLocalTargetDependenciesResult: Set! = [] - - func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set { - invokedDirectLocalTargetDependencies = true - invokedDirectLocalTargetDependenciesCount += 1 - invokedDirectLocalTargetDependenciesParameters = (path, name) - invokedDirectLocalTargetDependenciesParametersList.append((path, name)) - return stubbedDirectLocalTargetDependenciesResult - } - - var invokedDirectLocalTargetDependenciesWithConditions = false - - var invokedDirectLocalTargetDependenciesWithConditionsCount = 0 - var invokedDirectLocalTargetDependenciesWithConditionsParameters: ( - path: AbsolutePath, - name: String - )? - - var invokedDirectTargetDependencies = false - var invokedDirectTargetDependenciesCount = 0 - var invokedDirectTargetDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedDirectTargetDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedDirectTargetDependenciesResult: Set! = [] - - func directTargetDependencies(path: AbsolutePath, name: String) -> Set { - invokedDirectTargetDependencies = true - invokedDirectTargetDependenciesCount += 1 - invokedDirectTargetDependenciesParameters = (path, name) - invokedDirectTargetDependenciesParametersList.append((path, name)) - return stubbedDirectTargetDependenciesResult - } - - var invokedAppExtensionDependencies = false - var invokedAppExtensionDependenciesCount = 0 - var invokedAppExtensionDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedAppExtensionDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedAppExtensionDependenciesResult: Set! = [] - - func appExtensionDependencies(path: AbsolutePath, name: String) -> Set { - invokedAppExtensionDependencies = true - invokedAppExtensionDependenciesCount += 1 - invokedAppExtensionDependenciesParameters = (path, name) - invokedAppExtensionDependenciesParametersList.append((path, name)) - return stubbedAppExtensionDependenciesResult - } - - var invokedResourceBundleDependencies = false - var invokedResourceBundleDependenciesCount = 0 - var invokedResourceBundleDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedResourceBundleDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedResourceBundleDependenciesResult: Set! = [] - - func resourceBundleDependencies(path: AbsolutePath, name: String) -> Set { - invokedResourceBundleDependencies = true - invokedResourceBundleDependenciesCount += 1 - invokedResourceBundleDependenciesParameters = (path, name) - invokedResourceBundleDependenciesParametersList.append((path, name)) - return stubbedResourceBundleDependenciesResult - } - - var invokedDirectStaticDependencies = false - var invokedDirectStaticDependenciesCount = 0 - var invokedDirectStaticDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedDirectStaticDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedDirectStaticDependenciesResult: Set! = [] - - func directStaticDependencies(path: AbsolutePath, name: String) -> Set { - invokedDirectStaticDependencies = true - invokedDirectStaticDependenciesCount += 1 - invokedDirectStaticDependenciesParameters = (path, name) - invokedDirectStaticDependenciesParametersList.append((path, name)) - return stubbedDirectStaticDependenciesResult - } - - var invokedAppClipDependencies = false - var invokedAppClipDependenciesCount = 0 - var invokedAppClipDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedAppClipDependenciesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedAppClipDependenciesResult: GraphTargetReference! - - func appClipDependencies(path: AbsolutePath, name: String) -> GraphTargetReference? { - invokedAppClipDependencies = true - invokedAppClipDependenciesCount += 1 - invokedAppClipDependenciesParameters = (path, name) - invokedAppClipDependenciesParametersList.append((path, name)) - 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)? - var invokedEmbeddableFrameworksParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedEmbeddableFrameworksResult: Set! = [] - - func embeddableFrameworks(path: AbsolutePath, name: String) -> Set { - invokedEmbeddableFrameworks = true - invokedEmbeddableFrameworksCount += 1 - invokedEmbeddableFrameworksParameters = (path, name) - invokedEmbeddableFrameworksParametersList.append((path, name)) - return stubbedEmbeddableFrameworksResult - } - - var invokedLinkableDependencies = false - var invokedLinkableDependenciesCount = 0 - var invokedLinkableDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedLinkableDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedLinkableDependenciesError: Error? - var stubbedLinkableDependenciesResult: Set! = [] - - func linkableDependencies(path: AbsolutePath, name: String) throws -> Set { - invokedLinkableDependencies = true - invokedLinkableDependenciesCount += 1 - invokedLinkableDependenciesParameters = (path, name) - invokedLinkableDependenciesParametersList.append((path, name)) - if let error = stubbedLinkableDependenciesError { - throw error - } - return stubbedLinkableDependenciesResult - } - - var invokedSearchablePathDependencies = false - var invokedSearchablePathDependenciesCount = 0 - var invokedSearchablePathDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedSearchablePathDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedSearchablePathDependenciesError: Error? - var stubbedSearchablePathDependenciesResult: Set! = [] - - func searchablePathDependencies(path: AbsolutePath, name: String) throws -> Set { - invokedSearchablePathDependencies = true - invokedSearchablePathDependenciesCount += 1 - invokedSearchablePathDependenciesParameters = (path, name) - invokedSearchablePathDependenciesParametersList.append((path, name)) - if let error = stubbedSearchablePathDependenciesError { - throw error - } - return stubbedSearchablePathDependenciesResult - } - - var invokedCopyProductDependencies = false - var invokedCopyProductDependenciesCount = 0 - var invokedCopyProductDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedCopyProductDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedCopyProductDependenciesResult: Set! = [] - - func copyProductDependencies(path: AbsolutePath, name: String) -> Set { - invokedCopyProductDependencies = true - invokedCopyProductDependenciesCount += 1 - invokedCopyProductDependenciesParameters = (path, name) - invokedCopyProductDependenciesParametersList.append((path, name)) - return stubbedCopyProductDependenciesResult - } - - var invokedLibrariesPublicHeadersFolders = false - var invokedLibrariesPublicHeadersFoldersCount = 0 - var invokedLibrariesPublicHeadersFoldersParameters: ( - path: AbsolutePath, - name: String - )? - var invokedLibrariesPublicHeadersFoldersParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedLibrariesPublicHeadersFoldersResult: Set! = [] - - func librariesPublicHeadersFolders(path: AbsolutePath, name: String) -> Set { - invokedLibrariesPublicHeadersFolders = true - invokedLibrariesPublicHeadersFoldersCount += 1 - invokedLibrariesPublicHeadersFoldersParameters = (path, name) - invokedLibrariesPublicHeadersFoldersParametersList.append((path, name)) - return stubbedLibrariesPublicHeadersFoldersResult - } - - var invokedLibrariesSearchPaths = false - var invokedLibrariesSearchPathsCount = 0 - var invokedLibrariesSearchPathsParameters: (path: AbsolutePath, name: String)? - var invokedLibrariesSearchPathsParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedLibrariesSearchPathsResult: Set! = [] - - func librariesSearchPaths(path: AbsolutePath, name: String) throws -> Set { - invokedLibrariesSearchPaths = true - invokedLibrariesSearchPathsCount += 1 - invokedLibrariesSearchPathsParameters = (path, name) - invokedLibrariesSearchPathsParametersList.append((path, name)) - return stubbedLibrariesSearchPathsResult - } - - var invokedLibrariesSwiftIncludePaths = false - var invokedLibrariesSwiftIncludePathsCount = 0 - var invokedLibrariesSwiftIncludePathsParameters: (path: AbsolutePath, name: String)? - var invokedLibrariesSwiftIncludePathsParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedLibrariesSwiftIncludePathsResult: Set! = [] - - func librariesSwiftIncludePaths(path: AbsolutePath, name: String) -> Set { - invokedLibrariesSwiftIncludePaths = true - invokedLibrariesSwiftIncludePathsCount += 1 - invokedLibrariesSwiftIncludePathsParameters = (path, name) - invokedLibrariesSwiftIncludePathsParametersList.append((path, name)) - return stubbedLibrariesSwiftIncludePathsResult - } - - var invokedRunPathSearchPaths = false - var invokedRunPathSearchPathsCount = 0 - var invokedRunPathSearchPathsParameters: (path: AbsolutePath, name: String)? - var invokedRunPathSearchPathsParametersList = [(path: AbsolutePath, name: String)]() - var stubbedRunPathSearchPathsResult: Set! = [] - - func runPathSearchPaths(path: AbsolutePath, name: String) -> Set { - invokedRunPathSearchPaths = true - invokedRunPathSearchPathsCount += 1 - invokedRunPathSearchPathsParameters = (path, name) - invokedRunPathSearchPathsParametersList.append((path, name)) - return stubbedRunPathSearchPathsResult - } - - var invokedHostTargetFor = false - var invokedHostTargetForCount = 0 - var invokedHostTargetForParameters: (path: AbsolutePath, name: String)? - var invokedHostTargetForParametersList = [(path: AbsolutePath, name: String)]() - var stubbedHostTargetForResult: GraphTarget! - - func hostTargetFor(path: AbsolutePath, name: String) -> GraphTarget? { - invokedHostTargetFor = true - invokedHostTargetForCount += 1 - invokedHostTargetForParameters = (path, name) - invokedHostTargetForParametersList.append((path, name)) - return stubbedHostTargetForResult - } - - var invokedAllProjectDependencies = false - var invokedAllProjectDependenciesCount = 0 - var invokedAllProjectDependenciesParameters: (path: AbsolutePath, Void)? - var invokedAllProjectDependenciesParametersList = [(path: AbsolutePath, Void)]() - var stubbedAllProjectDependenciesError: Error? - var stubbedAllProjectDependenciesResult: Set! = [] - - func allProjectDependencies(path: AbsolutePath) throws -> Set { - invokedAllProjectDependencies = true - invokedAllProjectDependenciesCount += 1 - invokedAllProjectDependenciesParameters = (path, ()) - invokedAllProjectDependenciesParametersList.append((path, ())) - if let error = stubbedAllProjectDependenciesError { - throw error - } - return stubbedAllProjectDependenciesResult - } - - var invokedDependsOnXCTest = false - var invokedDependsOnXCTestCount = 0 - var invokedDependsOnXCTestParameters: (path: AbsolutePath, name: String)? - var invokedDependsOnXCTestParametersList = [(path: AbsolutePath, name: String)]() - var stubbedDependsOnXCTestResult: Bool! = false - - func dependsOnXCTest(path: AbsolutePath, name: String) -> Bool { - invokedDependsOnXCTest = true - invokedDependsOnXCTestCount += 1 - invokedDependsOnXCTestParameters = (path, name) - invokedDependsOnXCTestParametersList.append((path, name)) - return stubbedDependsOnXCTestResult - } - - var schemesStub: (() -> [Scheme])? - func schemes() -> [Scheme] { - schemesStub?() ?? [] - } - - var invokedExtensionKitExtensionDependencies = false - var invokedExtensionKitExtensionDependenciesCount = 0 - var invokedExtensionKitExtensionDependenciesParameters: (path: AbsolutePath, name: String)? - var invokedExtensionKitExtensionDependenciesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedExtensionKitExtensionDependenciesResult: Set! = [] - - func extensionKitExtensionDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { - invokedExtensionKitExtensionDependencies = true - invokedExtensionKitExtensionDependenciesCount += 1 - invokedExtensionKitExtensionDependenciesParameters = (path, name) - invokedExtensionKitExtensionDependenciesParametersList.append((path, name)) - 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)? - var invokedDirectSwiftMacroExecutablesParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedDirectSwiftMacroExecutablesResult: Set! = [] - - func directSwiftMacroExecutables(path: TSCBasic.AbsolutePath, name: String) -> Set { - invokedDirectSwiftMacroExecutables = true - invokedDirectSwiftMacroExecutablesCount += 1 - invokedDirectSwiftMacroExecutablesParameters = (path, name) - invokedDirectSwiftMacroExecutablesParametersList.append((path, name)) - return stubbedDirectSwiftMacroExecutablesResult - } - - 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 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/TuistCoreTesting/Mappers/MockProjectMapper.swift b/Sources/TuistCoreTesting/Mappers/MockProjectMapper.swift index f93b05c8190..d1a58618f33 100644 --- a/Sources/TuistCoreTesting/Mappers/MockProjectMapper.swift +++ b/Sources/TuistCoreTesting/Mappers/MockProjectMapper.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistCore final class MockProjectMapper: ProjectMapping { diff --git a/Sources/TuistCoreTesting/Mappers/MockWorkspaceMapper.swift b/Sources/TuistCoreTesting/Mappers/MockWorkspaceMapper.swift index f41261b9d80..9a1e3859901 100644 --- a/Sources/TuistCoreTesting/Mappers/MockWorkspaceMapper.swift +++ b/Sources/TuistCoreTesting/Mappers/MockWorkspaceMapper.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistCore public final class MockWorkspaceMapper: WorkspaceMapping { diff --git a/Sources/TuistCoreTesting/MetadataProviders/MockFrameworkMetadataProvider.swift b/Sources/TuistCoreTesting/MetadataProviders/MockFrameworkMetadataProvider.swift index e2f759963a5..8b100bb4546 100644 --- a/Sources/TuistCoreTesting/MetadataProviders/MockFrameworkMetadataProvider.swift +++ b/Sources/TuistCoreTesting/MetadataProviders/MockFrameworkMetadataProvider.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistCore public final class MockFrameworkMetadataProvider: MockPrecompiledMetadataProvider, FrameworkMetadataProviding { diff --git a/Sources/TuistCoreTesting/MetadataProviders/MockLibraryMetadataProvider.swift b/Sources/TuistCoreTesting/MetadataProviders/MockLibraryMetadataProvider.swift index c44647ff95e..a8e38b88dd8 100644 --- a/Sources/TuistCoreTesting/MetadataProviders/MockLibraryMetadataProvider.swift +++ b/Sources/TuistCoreTesting/MetadataProviders/MockLibraryMetadataProvider.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistCore public final class MockLibraryMetadataProvider: MockPrecompiledMetadataProvider, LibraryMetadataProviding { diff --git a/Sources/TuistCoreTesting/MetadataProviders/MockPrecompiledMetadataProvider.swift b/Sources/TuistCoreTesting/MetadataProviders/MockPrecompiledMetadataProvider.swift index 9a4ff304ad4..d18b14e9484 100644 --- a/Sources/TuistCoreTesting/MetadataProviders/MockPrecompiledMetadataProvider.swift +++ b/Sources/TuistCoreTesting/MetadataProviders/MockPrecompiledMetadataProvider.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistCore public class MockPrecompiledMetadataProvider: PrecompiledMetadataProviding { diff --git a/Sources/TuistCoreTesting/MetadataProviders/MockXCFrameworkMetadataProvider.swift b/Sources/TuistCoreTesting/MetadataProviders/MockXCFrameworkMetadataProvider.swift index fe7e944f7e6..c8a9cc03cee 100644 --- a/Sources/TuistCoreTesting/MetadataProviders/MockXCFrameworkMetadataProvider.swift +++ b/Sources/TuistCoreTesting/MetadataProviders/MockXCFrameworkMetadataProvider.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistCore public final class MockXCFrameworkMetadataProvider: MockPrecompiledMetadataProvider, XCFrameworkMetadataProviding { diff --git a/Sources/TuistCoreTesting/NodeLoaders/MockFrameworkNodeLoader.swift b/Sources/TuistCoreTesting/NodeLoaders/MockFrameworkNodeLoader.swift index 4c41b47c387..c22c2c226bb 100644 --- a/Sources/TuistCoreTesting/NodeLoaders/MockFrameworkNodeLoader.swift +++ b/Sources/TuistCoreTesting/NodeLoaders/MockFrameworkNodeLoader.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph public final class MockFrameworkLoader: FrameworkLoading { public init() {} diff --git a/Sources/TuistCoreTesting/NodeLoaders/MockXCFrameworkLoader.swift b/Sources/TuistCoreTesting/NodeLoaders/MockXCFrameworkLoader.swift index 34352b42408..389e5deaa3b 100644 --- a/Sources/TuistCoreTesting/NodeLoaders/MockXCFrameworkLoader.swift +++ b/Sources/TuistCoreTesting/NodeLoaders/MockXCFrameworkLoader.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph public final class MockXCFrameworkLoader: XCFrameworkLoading { public init() {} diff --git a/Sources/TuistCoreTesting/Simulator/MockSimulatorController.swift b/Sources/TuistCoreTesting/Simulator/MockSimulatorController.swift deleted file mode 100644 index 7909da6f5e7..00000000000 --- a/Sources/TuistCoreTesting/Simulator/MockSimulatorController.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version -import TuistGraph -import TuistSupport -@testable import TuistCore -@testable import TuistSupportTesting - -public final class MockSimulatorController: SimulatorControlling { - public init() {} - - public var findAvailableDevicesStub: ((Platform, Version?, Version?, String?) -> [SimulatorDeviceAndRuntime])? - public func findAvailableDevices( - platform: TuistGraph.Platform, - version: TSCUtility.Version?, - minVersion: TSCUtility.Version?, - deviceName: String? - ) async throws -> [TuistCore.SimulatorDeviceAndRuntime] { - findAvailableDevicesStub?(platform, version, minVersion, deviceName) ?? [SimulatorDeviceAndRuntime.test()] - } - - public var findAvailableDeviceStub: ((Platform, Version?, Version?, String?) -> SimulatorDeviceAndRuntime)? - public func findAvailableDevice( - platform: Platform, - version: Version?, - minVersion: Version?, - deviceName: String? - ) async throws -> SimulatorDeviceAndRuntime { - findAvailableDeviceStub?(platform, version, minVersion, deviceName) ?? SimulatorDeviceAndRuntime.test() - } - - public var installAppStub: ((AbsolutePath, SimulatorDevice) throws -> Void)? - public func installApp(at path: AbsolutePath, device: SimulatorDevice) throws { - try installAppStub?(path, device) - } - - public var launchAppStub: ((String, SimulatorDevice, [String]) throws -> Void)? - public func launchApp(bundleId: String, device: SimulatorDevice, arguments: [String]) throws { - try launchAppStub?(bundleId, device, arguments) - } - - public var destinationStub: ((Platform) -> String)? - public func destination(for targetPlatform: Platform, version _: Version?, deviceName _: String?) async throws -> String { - destinationStub?(targetPlatform) ?? "id=\(SimulatorDeviceAndRuntime.test().device.udid)" - } - - public var macOSDestinationStub: (() -> String)? - public func macOSDestination() -> String { - macOSDestinationStub?() ?? "platform=macOS,arch=arm64" - } -} diff --git a/Sources/TuistCoreTesting/Simulator/SimulatorDevice+TestData.swift b/Sources/TuistCoreTesting/Simulator/SimulatorDevice+TestData.swift deleted file mode 100644 index 4c5718aac32..00000000000 --- a/Sources/TuistCoreTesting/Simulator/SimulatorDevice+TestData.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistCore - -extension SimulatorDevice { - static func test( - dataPath: AbsolutePath = "/Library/Developer/CoreSimulator/Devices/3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC/data", - logPath: AbsolutePath = "/Library/Logs/CoreSimulator/3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC", - udid: String = "3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC", - isAvailable: Bool = true, - deviceTypeIdentifier: String = "com.apple.CoreSimulator.SimDeviceType.iPad-Air--3rd-generation-", - state: String = "Shutdown", - name: String = "iPad Air (3rd generation)", - availabilityError: String? = nil, - runtimeIdentifier: String = "com.apple.CoreSimulator.SimRuntime.iOS-13-5" - ) -> SimulatorDevice { - SimulatorDevice( - dataPath: dataPath, - logPath: logPath, - udid: udid, - isAvailable: isAvailable, - deviceTypeIdentifier: deviceTypeIdentifier, - state: state, - name: name, - availabilityError: availabilityError, - runtimeIdentifier: runtimeIdentifier - ) - } -} diff --git a/Sources/TuistCoreTesting/Simulator/SimulatorDeviceAndRuntime+TestData.swift b/Sources/TuistCoreTesting/Simulator/SimulatorDeviceAndRuntime+TestData.swift deleted file mode 100644 index 997c8247368..00000000000 --- a/Sources/TuistCoreTesting/Simulator/SimulatorDeviceAndRuntime+TestData.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistCore - -extension SimulatorDeviceAndRuntime { - static func test( - device: SimulatorDevice = .test(), - runtime: SimulatorRuntime = .test() - ) -> SimulatorDeviceAndRuntime { - SimulatorDeviceAndRuntime(device: device, runtime: runtime) - } -} diff --git a/Sources/TuistCoreTesting/Simulator/SimulatorRuntime+TestData.swift b/Sources/TuistCoreTesting/Simulator/SimulatorRuntime+TestData.swift deleted file mode 100644 index fbaf5a61fc4..00000000000 --- a/Sources/TuistCoreTesting/Simulator/SimulatorRuntime+TestData.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistCore - -extension SimulatorRuntime { - static func test( - bundlePath: AbsolutePath = - "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime", - buildVersion: String = "17F61", - runtimeRoot: AbsolutePath = - "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot", - identifier: String = "com.apple.CoreSimulator.SimRuntime.iOS-13-5", - version: SimulatorRuntimeVersion = "13.5", - isAvailable: Bool = true, - name: String = "iOS 13.5" - ) -> SimulatorRuntime { - SimulatorRuntime( - bundlePath: bundlePath, - buildVersion: buildVersion, - runtimeRoot: runtimeRoot, - identifier: identifier, - version: version, - isAvailable: isAvailable, - name: name - ) - } -} diff --git a/Sources/TuistCoreTesting/Utils/MockRootDirectoryLocator.swift b/Sources/TuistCoreTesting/Utils/MockRootDirectoryLocator.swift deleted file mode 100644 index 36ab0d71c4c..00000000000 --- a/Sources/TuistCoreTesting/Utils/MockRootDirectoryLocator.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistCore - -public final class MockRootDirectoryLocator: RootDirectoryLocating { - public var locateArgs: [AbsolutePath] = [] - public var locateStub: AbsolutePath? - - public func locate(from path: AbsolutePath) -> AbsolutePath? { - locateArgs.append(path) - return locateStub - } -} diff --git a/Sources/TuistCoreTesting/Utils/MockXcodeProjectBuildDirectoryLocator.swift b/Sources/TuistCoreTesting/Utils/MockXcodeProjectBuildDirectoryLocator.swift deleted file mode 100644 index 186e1a2ea19..00000000000 --- a/Sources/TuistCoreTesting/Utils/MockXcodeProjectBuildDirectoryLocator.swift +++ /dev/null @@ -1,25 +0,0 @@ -import TSCBasic -import TuistCore -import TuistGraph - -public final class MockXcodeProjectBuildDirectoryLocator: XcodeProjectBuildDirectoryLocating { - public init() {} - - // swiftlint:disable:next type_name - enum MockXcodeProjectBuildDirectoryLocatorError: Error { - case noStub - } - - public var locateStub: ((Platform, AbsolutePath, AbsolutePath?, String) throws -> AbsolutePath)? - public func locate( - platform: Platform, - projectPath: AbsolutePath, - derivedDataPath: AbsolutePath?, - configuration: String - ) throws -> AbsolutePath { - guard let stub = locateStub else { - throw MockXcodeProjectBuildDirectoryLocatorError.noStub - } - return try stub(platform, projectPath, derivedDataPath, configuration) - } -} diff --git a/Sources/TuistDependencies/Carthage/CarthageInteractor.swift b/Sources/TuistDependencies/Carthage/CarthageInteractor.swift deleted file mode 100644 index a102187ec22..00000000000 --- a/Sources/TuistDependencies/Carthage/CarthageInteractor.swift +++ /dev/null @@ -1,230 +0,0 @@ -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - Carthage Interactor Errors - -enum CarthageInteractorError: FatalError, Equatable { - /// Thrown when Carthage cannot be found. - case carthageNotFound - /// Thrown when `Cartfile` cannot be found in the temporary directory after Carthage installation - case cartfileNotFound - /// Thrown when `Cartfile.resolved` cannot be found in temporary directory after Carthage installation. - case cartfileResolvedNotFound - /// Thrown when `Carthage/Build` directory cannot be found in temporary directory after Carthage installation. - case buildDirectoryNotFound - - /// Error type. - var type: ErrorType { - switch self { - case .cartfileNotFound, - .cartfileResolvedNotFound, - .buildDirectoryNotFound: - return .bug - case .carthageNotFound: - return .abort - } - } - - /// Error description. - var description: String { - switch self { - case .carthageNotFound: - return """ - Carthage was not found in the environment. - It's possible that the tool is not installed or hasn't been exposed to your environment." - """ - case .cartfileNotFound: - return "The Cartfile file was not found after resolving the dependencies using the Carthage." - case .cartfileResolvedNotFound: - return "The Cartfile.resolved lockfile was not found after resolving the dependencies using the Carthage." - case .buildDirectoryNotFound: - return "The Carthage/Build directory was not found after resolving the dependencies using the Carthage." - } - } -} - -// MARK: - Carthage Interacting - -public protocol CarthageInteracting { - /// Installs `Carthage` dependencies - /// - Parameters: - /// - dependenciesDirectory: The path to the directory that contains the `Tuist/Dependencies/` directory. - /// - dependencies: List of dependencies to install using `Carthage`. - /// - platforms: List of platforms for which you want to install dependencies. - /// - shouldUpdate: Indicates whether dependencies should be updated or fetched based on the lockfile. - /// - Returns: A graph that represents dependencies installed using `Carthage`. - func install( - dependenciesDirectory: AbsolutePath, - dependencies: TuistGraph.CarthageDependencies, - platforms: Set, - shouldUpdate: Bool - ) throws -> TuistCore.DependenciesGraph - - /// Removes all cached `Carthage` dependencies. - /// - Parameter dependenciesDirectory: The path to the directory that contains the `Tuist/Dependencies/` directory. - func clean(dependenciesDirectory: AbsolutePath) throws -} - -// MARK: - Carthage Interactor - -public final class CarthageInteractor: CarthageInteracting { - private let carthageController: CarthageControlling - private let carthageGraphGenerator: CarthageGraphGenerating - - public init( - carthageController: CarthageControlling = CarthageController.shared, - carthageGraphGenerator: CarthageGraphGenerating = CarthageGraphGenerator() - ) { - self.carthageController = carthageController - self.carthageGraphGenerator = carthageGraphGenerator - } - - public func install( - dependenciesDirectory: AbsolutePath, - dependencies: TuistGraph.CarthageDependencies, - platforms: Set, - shouldUpdate: Bool - ) throws -> TuistCore.DependenciesGraph { - logger.info("Installing Carthage dependencies.", metadata: .subsection) - - guard carthageController.canUseSystemCarthage() else { - throw CarthageInteractorError.carthageNotFound - } - - let pathsProvider = CarthagePathsProvider(dependenciesDirectory: dependenciesDirectory) - - try loadDependencies(pathsProvider: pathsProvider, dependencies: dependencies) - - if shouldUpdate { - try carthageController.update( - at: pathsProvider.dependenciesDirectory, - platforms: platforms, - printOutput: true - ) - } else { - try carthageController.bootstrap( - at: pathsProvider.dependenciesDirectory, - platforms: platforms, - printOutput: true - ) - } - - try saveDependencies(pathsProvider: pathsProvider) - - let dependenciesGraph = try carthageGraphGenerator - .generate(at: pathsProvider.destinationCarthageBuildDirectory) - - logger.info("Carthage dependencies installed successfully.", metadata: .subsection) - - return dependenciesGraph - } - - public func clean(dependenciesDirectory: AbsolutePath) throws { - let carthageDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.carthageDirectoryName) - let cartfileResolvedPath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - .appending(component: Constants.DependenciesDirectory.cartfileResolvedName) - - try FileHandler.shared.delete(carthageDirectory) - try FileHandler.shared.delete(cartfileResolvedPath) - } - - // MARK: - Installation - - /// Loads lockfile and dependencies into working directory if they had been saved before. - private func loadDependencies(pathsProvider: CarthagePathsProvider, dependencies: TuistGraph.CarthageDependencies) throws { - // copy `Cartfile.resolved` directory from previous run if exist - if FileHandler.shared.exists(pathsProvider.destinationCartfileResolvedPath) { - try copy( - from: pathsProvider.destinationCartfileResolvedPath, - to: pathsProvider.temporaryCartfileResolvedPath - ) - } - - // create `Cartfile` - let cartfileContent = dependencies.cartfileValue() - let cartfilePath = pathsProvider.temporaryCartfilePath - try FileHandler.shared.createFolder(cartfilePath.removingLastComponent()) - try FileHandler.shared.write(cartfileContent, path: cartfilePath, atomically: true) - - logger.debug("Cartfile:", metadata: .subsection) - logger.debug("\(cartfileContent)") - } - - /// Saves lockfile resolved dependencies in `Tuist/Dependencies` directory. - private func saveDependencies(pathsProvider: CarthagePathsProvider) throws { - guard FileHandler.shared.exists(pathsProvider.temporaryCartfilePath) else { - throw CarthageInteractorError.cartfileNotFound - } - - guard FileHandler.shared.exists(pathsProvider.temporaryCartfileResolvedPath) else { - throw CarthageInteractorError.cartfileResolvedNotFound - } - guard FileHandler.shared.exists(pathsProvider.destinationCarthageBuildDirectory) else { - throw CarthageInteractorError.buildDirectoryNotFound - } - - try copy( - from: pathsProvider.temporaryCartfilePath, - to: pathsProvider.destinationCartfilePath - ) - - try copy( - from: pathsProvider.temporaryCartfileResolvedPath, - to: pathsProvider.destinationCartfileResolvedPath - ) - - // remove temporary files - try? FileHandler.shared.delete(pathsProvider.temporaryCartfilePath) - try? FileHandler.shared.delete(pathsProvider.temporaryCartfileResolvedPath) - } - - // MARK: - Helpers - - private func copy(from fromPath: AbsolutePath, to toPath: AbsolutePath) throws { - if FileHandler.shared.exists(toPath) { - try FileHandler.shared.replace(toPath, with: fromPath) - } else { - try FileHandler.shared.createFolder(toPath.removingLastComponent()) - try FileHandler.shared.copy(from: fromPath, to: toPath) - } - } -} - -// MARK: - Models - -private struct CarthagePathsProvider { - let dependenciesDirectory: AbsolutePath - - let destinationCartfilePath: AbsolutePath - let destinationCartfileResolvedPath: AbsolutePath - let destinationCarthageDirectory: AbsolutePath - let destinationCarthageBuildDirectory: AbsolutePath - - let temporaryCartfilePath: AbsolutePath - let temporaryCartfileResolvedPath: AbsolutePath - - init(dependenciesDirectory: AbsolutePath) { - self.dependenciesDirectory = dependenciesDirectory - - destinationCartfilePath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.carthageDirectoryName) - .appending(component: Constants.DependenciesDirectory.cartfileName) - destinationCartfileResolvedPath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - .appending(component: Constants.DependenciesDirectory.cartfileResolvedName) - destinationCarthageDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.carthageDirectoryName) - destinationCarthageBuildDirectory = destinationCarthageDirectory - .appending(component: "Build") - - temporaryCartfilePath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.cartfileName) - temporaryCartfileResolvedPath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.cartfileResolvedName) - } -} diff --git a/Sources/TuistDependencies/Carthage/Models/CarthageVersionFile.swift b/Sources/TuistDependencies/Carthage/Models/CarthageVersionFile.swift deleted file mode 100644 index f05796fa8ae..00000000000 --- a/Sources/TuistDependencies/Carthage/Models/CarthageVersionFile.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import TuistGraph - -/// A model that represents the `Carthage` version file. -/// Reference: https://github.com/Carthage/Carthage/blob/master/Documentation/VersionFile.md#version-files -struct CarthageVersionFile: Decodable, Equatable { - enum CodingKeys: String, CodingKey { - case iOS - case macOS = "Mac" - case watchOS - case tvOS - case visionOS - } - - let iOS: [Product]? - let macOS: [Product]? - let watchOS: [Product]? - let tvOS: [Product]? - let visionOS: [Product]? - - /// Returns all products. - var allProducts: [Product] { - [iOS, macOS, watchOS, tvOS, visionOS] - .compactMap { $0 } - .flatMap { $0 } - } -} - -// MARK: - Models - -extension CarthageVersionFile { - struct Product: Decodable, Equatable { - let name: String - let container: String? - } -} diff --git a/Sources/TuistDependencies/Carthage/Utils/CarthageController.swift b/Sources/TuistDependencies/Carthage/Utils/CarthageController.swift deleted file mode 100644 index a3bbcd4e556..00000000000 --- a/Sources/TuistDependencies/Carthage/Utils/CarthageController.swift +++ /dev/null @@ -1,179 +0,0 @@ -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - Carthage Controller Error - -enum CarthageControllerError: FatalError, Equatable { - /// Thrown when Carthage cannot be found in the environment. - case carthageNotFound - /// Thrown when version of Carthage cannot be determined. - case unrecognizedCarthageVersion - /// Thrown when version of Carthage installed in environment does not support XCFrameworks production. - case xcframeworksProductionNotSupported(installedVersion: Version) - - /// Error type. - var type: ErrorType { - switch self { - case .carthageNotFound, - .unrecognizedCarthageVersion, - .xcframeworksProductionNotSupported: - return .abort - } - } - - /// Error description. - var description: String { - switch self { - case .carthageNotFound: - return """ - Carthage was not found in the environment. - It's possible that the tool is not installed or hasn't been exposed to your environment. - """ - case .unrecognizedCarthageVersion: - return """ - The version of Carthage cannot be determined. - It's possible that the tool is not installed or hasn't been exposed to your environment. - """ - case let .xcframeworksProductionNotSupported(installedVersion): - return """ - The version of Carthage installed in your environment (\( - installedVersion - .description - )) doesn't suppport production of XCFrameworks. - You have to update the tool to at least 0.37.0 version. - """ - } - } -} - -// MARK: - Carthage Controlling - -/// Protocol that defines an interface to interact with the Carthage. -public protocol CarthageControlling { - /// Returns true if Carthage is available in the environment. - func canUseSystemCarthage() -> Bool - - /// Return version of Carthage that is available in the environment. - func carthageVersion() throws -> Version - - /// Checkouts and builds the project's dependencies - /// - Parameters: - /// - path: Directory where project's dependencies will be installed. - /// - platforms: The platforms to build for. - /// - printOutput: When true it prints the Carthage's output. - func bootstrap(at path: AbsolutePath, platforms: Set, printOutput: Bool) throws - - /// Updates and rebuilds the project's dependencies - /// - Parameters: - /// - path: Directory where project's dependencies will be installed. - /// - platforms: The platforms to build for. - /// - printOutput: When true it prints the Carthage's output. - func update(at path: AbsolutePath, platforms: Set, printOutput: Bool) throws -} - -// MARK: - Carthage Controller - -public final class CarthageController: CarthageControlling { - /// Shared instance. - public static var shared: CarthageControlling = CarthageController() - - /// Cached response of `carthage version` command. - @Atomic - private var cachedCarthageVersion: Version? - - public func canUseSystemCarthage() -> Bool { - do { - _ = try System.shared.which("carthage") - return true - } catch { - return false - } - } - - public func carthageVersion() throws -> Version { - // Return cached value if available - if let cached = cachedCarthageVersion { - return cached - } - - guard let output = try? System.shared.capture(["/usr/bin/env", "carthage", "version"]).spm_chomp() else { - throw CarthageControllerError.carthageNotFound - } - - guard let version = Version(output) else { - throw CarthageControllerError.unrecognizedCarthageVersion - } - - cachedCarthageVersion = version - return version - } - - public func bootstrap(at path: AbsolutePath, platforms: Set, printOutput: Bool) throws { - guard try isXCFrameworksProductionSupported() else { - throw CarthageControllerError.xcframeworksProductionNotSupported(installedVersion: try carthageVersion()) - } - - let command = buildCarthageCommand(path: path, platforms: platforms, subcommand: "bootstrap") - - printOutput ? - try System.shared.runAndPrint(command) : - try System.shared.run(command) - } - - public func update(at path: AbsolutePath, platforms: Set, printOutput: Bool) throws { - guard try isXCFrameworksProductionSupported() else { - throw CarthageControllerError.xcframeworksProductionNotSupported(installedVersion: try carthageVersion()) - } - - let command = buildCarthageCommand(path: path, platforms: platforms, subcommand: "update") - - printOutput ? - try System.shared.runAndPrint(command) : - try System.shared.run(command) - } - - // MARK: - Helpers - - private func buildCarthageCommand( - path: AbsolutePath, - platforms: Set, - subcommand: String - ) -> [String] { - var commandComponents: [String] = [ - "carthage", - subcommand, - "--project-directory", - path.pathString, - ] - - if !platforms.isEmpty { - commandComponents += [ - "--platform", - platforms - .filter { $0 != .macCatalyst } // Carthage does not support catalyst - .map(\.caseValue) - .sorted() - .joined(separator: ","), - ] - } - - commandComponents += [ - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ] - - return commandComponents - } - - private func isXCFrameworksProductionSupported() throws -> Bool { - // Carthage has supported XCFrameworks production since 0.37.0 - // More info here: https://github.com/Carthage/Carthage/releases/tag/0.37.0 - try carthageVersion() >= Version(0, 37, 0) - } -} diff --git a/Sources/TuistDependencies/Carthage/Utils/CarthageGraphGenerator.swift b/Sources/TuistDependencies/Carthage/Utils/CarthageGraphGenerator.swift deleted file mode 100644 index 1779182e090..00000000000 --- a/Sources/TuistDependencies/Carthage/Utils/CarthageGraphGenerator.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistSupport - -/// A protocol that defines an interface to generate the `DependenciesGraph` for the `Carthage` dependencies. -public protocol CarthageGraphGenerating { - /// Generates the `DependenciesGraph` for the `Carthage` dependencies. - /// - Parameter path: The path to the directory that contains the `Carthage/Build` directory where `Carthage` installed - /// dependencies. - func generate(at path: AbsolutePath) throws -> DependenciesGraph -} - -public final class CarthageGraphGenerator: CarthageGraphGenerating { - public init() {} - - public func generate(at path: AbsolutePath) throws -> DependenciesGraph { - let versionFilePaths = try FileHandler.shared - .contentsOfDirectory(path) - .filter { $0.extension == "version" } - - let jsonDecoder = JSONDecoder() - let products = try versionFilePaths - .map { try FileHandler.shared.readFile($0) } - .map { try jsonDecoder.decode(CarthageVersionFile.self, from: $0) } - - return DependenciesGraph( - externalDependencies: groupDependencies(products: products.flatMap(\.allProducts)), - externalProjects: [:] - ) - } -} - -// MARK: - Helpers - -extension CarthageGraphGenerator { - private func groupDependencies(products: [CarthageVersionFile.Product]) -> [String: [TargetDependency]] { - Dictionary(grouping: products, by: \.name) - .compactMapValues { products in - guard let product = products.first else { return nil } - - guard let xcFrameworkName = product.container else { - logger.warning("\(product.name) was not added to the DependenciesGraph", metadata: .subsection) - return nil - } - - var pathString = "" - pathString += Constants.tuistDirectoryName - pathString += "/" - pathString += Constants.DependenciesDirectory.name - pathString += "/" - pathString += Constants.DependenciesDirectory.carthageDirectoryName - pathString += "/Build/" - pathString += xcFrameworkName - - return [.xcframework(path: Path(pathString))] - } - } -} diff --git a/Sources/TuistDependencies/DependenciesController.swift b/Sources/TuistDependencies/DependenciesController.swift deleted file mode 100644 index db4b81777a2..00000000000 --- a/Sources/TuistDependencies/DependenciesController.swift +++ /dev/null @@ -1,277 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - Dependencies Controller Error - -enum DependenciesControllerError: FatalError, Equatable { - /// Thrown when the same dependency is defined more than once. - case duplicatedDependency(String, [ProjectDescription.TargetDependency], [ProjectDescription.TargetDependency]) - - /// Thrown when the same project is defined more than once. - case duplicatedProject(Path, ProjectDescription.Project, ProjectDescription.Project) - - /// Thrown when platforms for dependencies to install are not determined in `Dependencies.swift`. - case noPlatforms - - /// Error type. - var type: ErrorType { - switch self { - case .duplicatedDependency, .duplicatedProject, .noPlatforms: - return .abort - } - } - - // Error description. - var description: String { - switch self { - case let .duplicatedDependency(name, first, second): - return """ - The \(name) dependency is defined twice across different dependency managers: - First: \(first) - Second: \(second) - """ - case let .duplicatedProject(name, first, second): - return """ - The \(name) project is defined twice across different dependency managers: - First: \(first) - Second: \(second) - """ - case .noPlatforms: - return "Platforms were not determined. Select platforms in `Dependencies.swift` manifest file." - } - } -} - -// MARK: - Dependencies Controlling - -/// `DependenciesControlling` controls: -/// 1. Fetching/updating dependencies defined in `./Tuist/Dependencies.swift` by running appropriate dependencies managers -/// (for example, `Carthage` or `SPM`). -/// 2. Compiling fetched/updated dependencies into `.framework.`/`.xcframework.`. -/// 3. Saving compiled frameworks under `./Tuist/Dependencies/*`. -/// 4. Generating dependencies graph under `./Tuist/Dependencies/graph.json`. -public protocol DependenciesControlling { - /// Fetches dependencies. - /// - Parameter path: Directory where project's dependencies will be fetched. - /// - Parameter dependencies: List of dependencies to fetch. - /// - Parameter swiftVersion: The specified version of Swift. If `nil` is passed then the environment’s version will be used. - func fetch( - at path: AbsolutePath, - dependencies: TuistGraph.Dependencies, - 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. - /// - dependencies: List of dependencies to update. - /// - 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, - dependencies: TuistGraph.Dependencies, - 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. - /// - path: Directory where dependencies graph will be saved. - func save( - dependenciesGraph: TuistGraph.DependenciesGraph, - to path: AbsolutePath - ) throws -} - -// MARK: - Dependencies Controller - -public final class DependenciesController: DependenciesControlling { - private let carthageInteractor: CarthageInteracting - private let swiftPackageManagerInteractor: SwiftPackageManagerInteracting - private let dependenciesGraphController: DependenciesGraphControlling - - public init( - carthageInteractor: CarthageInteracting = CarthageInteractor(), - swiftPackageManagerInteractor: SwiftPackageManagerInteracting = SwiftPackageManagerInteractor(), - dependenciesGraphController: DependenciesGraphControlling = DependenciesGraphController() - ) { - self.carthageInteractor = carthageInteractor - self.swiftPackageManagerInteractor = swiftPackageManagerInteractor - self.dependenciesGraphController = dependenciesGraphController - } - - public func fetch( - at path: AbsolutePath, - dependencies: TuistGraph.Dependencies, - swiftVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - try install( - at: path, - dependencies: dependencies, - shouldUpdate: false, - swiftVersion: swiftVersion - ) - } - - 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, - swiftVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - try install( - at: path, - dependencies: dependencies, - shouldUpdate: true, - swiftVersion: swiftVersion - ) - } - - public func save( - dependenciesGraph: TuistGraph.DependenciesGraph, - to path: AbsolutePath - ) throws { - try dependenciesGraphController.save(dependenciesGraph, to: path) - } - - // MARK: - Helpers - - private func install( - at path: AbsolutePath, - dependencies: TuistGraph.Dependencies, - shouldUpdate: Bool, - swiftVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - let dependenciesDirectory = path - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - let platforms = dependencies.platforms - - guard !platforms.isEmpty else { - throw DependenciesControllerError.noPlatforms - } - - var dependenciesGraph = TuistCore.DependenciesGraph.none - - if let carthageDependencies = dependencies.carthage, !carthageDependencies.dependencies.isEmpty { - let carthageDependenciesGraph = try carthageInteractor.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: carthageDependencies, - platforms: platforms, - shouldUpdate: shouldUpdate - ) - dependenciesGraph = try dependenciesGraph.merging(with: carthageDependenciesGraph) - } else { - try carthageInteractor.clean(dependenciesDirectory: dependenciesDirectory) - } - - if let swiftPackageManagerDependencies = dependencies.swiftPackageManager { - let swiftPackageManagerDependenciesGraph = try swiftPackageManagerInteractor.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: swiftPackageManagerDependencies, - platforms: platforms, - shouldUpdate: shouldUpdate, - swiftToolsVersion: swiftVersion - ) - dependenciesGraph = try dependenciesGraph.merging(with: swiftPackageManagerDependenciesGraph) - } else { - try swiftPackageManagerInteractor.clean(dependenciesDirectory: dependenciesDirectory) - } - - return dependenciesGraph - } -} - -extension TuistCore.DependenciesGraph { - public func merging(with other: Self) throws -> Self { - var mergedExternalDependencies: [String: [ProjectDescription.TargetDependency]] = - externalDependencies - - for (name, dependency) in other.externalDependencies { - if let alreadyPresent = mergedExternalDependencies[name] { - throw DependenciesControllerError.duplicatedDependency(name, alreadyPresent, dependency) - } - mergedExternalDependencies[name] = dependency - } - - let mergedExternalProjects = try other.externalProjects.reduce(into: externalProjects) { result, entry in - if let alreadyPresent = result[entry.key] { - throw DependenciesControllerError.duplicatedProject(entry.key, alreadyPresent, entry.value) - } - result[entry.key] = entry.value - } - - return .init(externalDependencies: mergedExternalDependencies, externalProjects: mergedExternalProjects) - } -} diff --git a/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift b/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift deleted file mode 100644 index 499e06ac2bd..00000000000 --- a/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift +++ /dev/null @@ -1,145 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - Dependencies Graph Controller Errors - -enum DependenciesGraphControllerError: FatalError, Equatable { - case failedToDecodeDependenciesGraph - case failedToEncodeDependenciesGraph - /// Thrown when there is a `Dependencies.swift` but no `graph.json` - case dependenciesWerentFetched - - var type: ErrorType { - switch self { - case .dependenciesWerentFetched, .failedToDecodeDependenciesGraph: - return .abort - case .failedToEncodeDependenciesGraph: - return .bug - } - } - - var description: String { - switch self { - case .dependenciesWerentFetched: - return "`Tuist/Dependencies.swift` file is defined but `Tuist/Dependencies/graph.json` cannot be found. Run `tuist fetch` first" - case .failedToDecodeDependenciesGraph: - return "Couldn't decode the DependenciesGraph from the serialized JSON file. Running `tuist fetch` should solve the problem." - case .failedToEncodeDependenciesGraph: - return "Couldn't encode the DependenciesGraph as a JSON file." - } - } -} - -// MARK: - Dependencies Graph Controlling - -/// A protocol that defines an interface to save and load the `DependenciesGraph` using a `graph.json` file. -public protocol DependenciesGraphControlling { - /// Saves the `DependenciesGraph` as `graph.json`. - /// - Parameters: - /// - dependenciesGraph: A model that will be saved. - /// - path: Directory where project's dependencies graph will be saved. - func save(_ dependenciesGraph: TuistGraph.DependenciesGraph, to path: AbsolutePath) throws - - /// Loads the `DependenciesGraph` from `graph.json` file. - /// - Parameter path: Directory where project's dependencies graph will be loaded. - func load(at path: AbsolutePath) throws -> TuistGraph.DependenciesGraph - - /// Removes cached `graph.json`. - /// - Parameter path: Directory where project's dependencies graph was saved. - func clean(at path: AbsolutePath) throws -} - -// MARK: - Dependencies Graph Controller - -public final class DependenciesGraphController: DependenciesGraphControlling { - private let rootDirectoryLocator: RootDirectoryLocating - - /// Default constructor. - public init(rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator()) { - self.rootDirectoryLocator = rootDirectoryLocator - } - - public func save(_ dependenciesGraph: TuistGraph.DependenciesGraph, to path: AbsolutePath) throws { - let jsonEncoder = JSONEncoder() - jsonEncoder.outputFormatting = .prettyPrinted - - let encodedGraph = try jsonEncoder.encode(dependenciesGraph) - - guard let encodedGraphContent = String(data: encodedGraph, encoding: .utf8) else { - throw DependenciesGraphControllerError.failedToEncodeDependenciesGraph - } - - let graphPath = graphPath(at: path) - - try FileHandler.shared.touch(graphPath) - try FileHandler.shared.write(encodedGraphContent, path: graphPath, atomically: true) - } - - public func load(at path: AbsolutePath) throws -> TuistGraph.DependenciesGraph { - // Search for the dependency graph at the root directory - // This can be the directory of this project or in case of nested projects - // the root of the overall project - guard let rootDirectory = rootDirectoryLocator.locate(from: path) else { - return .none - } - - let dependenciesPath = dependenciesPath(at: rootDirectory) - - guard FileHandler.shared.exists(dependenciesPath) - || FileHandler.shared.exists( - rootDirectory.appending(components: [ - Constants.tuistDirectoryName, - Constants.DependenciesDirectory.packageSwiftName, - ]) - ) - else { - return .none - } - - let rootGraphPath = graphPath(at: rootDirectory) - - guard FileHandler.shared.exists(rootGraphPath) else { - throw DependenciesGraphControllerError.dependenciesWerentFetched - } - - let graphData = try FileHandler.shared.readFile(rootGraphPath) - - do { - return try JSONDecoder().decode(TuistGraph.DependenciesGraph.self, from: graphData) - } catch { - logger - .debug( - "Failed to load dependencies graph, running `tuist fetch` should solve the problem.\nError: \(error)" - ) - throw DependenciesGraphControllerError.failedToDecodeDependenciesGraph - } - } - - public func clean(at path: AbsolutePath) throws { - let graphPath = graphPath(at: path) - - try FileHandler.shared.delete(graphPath) - } - - // MARK: - Helpers - - private func dependenciesPath(at path: AbsolutePath) -> AbsolutePath { - path - .appending(components: [ - Constants.tuistDirectoryName, - Constants.DependenciesDirectory.dependenciesFileName, - ]) - } - - private func graphPath(at path: AbsolutePath) -> AbsolutePath { - path - .appending(components: [ - Constants.tuistDirectoryName, - Constants.DependenciesDirectory.name, - Constants.DependenciesDirectory.graphName, - ]) - } -} diff --git a/Sources/TuistDependencies/Mappers/ExternalDependencyPathWorkspaceMapper.swift b/Sources/TuistDependencies/Mappers/ExternalDependencyPathWorkspaceMapper.swift new file mode 100644 index 00000000000..5a7edd37bde --- /dev/null +++ b/Sources/TuistDependencies/Mappers/ExternalDependencyPathWorkspaceMapper.swift @@ -0,0 +1,47 @@ +import Foundation +import Path +import TuistCore +import TuistSupport +import XcodeGraph + +public final class ExternalDependencyPathWorkspaceMapper: WorkspaceMapping { + public init() {} + + public func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) { + var workspace = workspace + let mappedProjects = try workspace.projects.map(map(project:)) + workspace.projects = mappedProjects.map(\.0) + return ( + workspace, + mappedProjects.flatMap(\.1) + ) + } + + // MARK: - Helpers + + private func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { + guard project.isExternal, + // We don't want to update local packages (which are defined outside the `checkouts` directory in `.build` + project.path.parentDirectory.parentDirectory.basename == Constants.SwiftPackageManager.packageBuildDirectoryName + else { return (project, []) } + var project = project + let xcodeProjBasename = project.xcodeProjPath.basename + let derivedDirectory = project.path.parentDirectory.parentDirectory.appending( + components: Constants.DerivedDirectory.dependenciesDerivedDirectory, project.name + ) + project.xcodeProjPath = derivedDirectory.appending(component: xcodeProjBasename) + + var base = project.settings.base + // Keep the value if already defined + if base["SRCROOT"] == nil { + base["SRCROOT"] = SettingValue(stringLiteral: project.sourceRootPath.pathString) + } + project.settings = project.settings.with( + base: base + ) + return ( + project, + [] + ) + } +} diff --git a/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift b/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift index f4388b9057c..ae17d879581 100644 --- a/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift +++ b/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift @@ -1,6 +1,6 @@ import Foundation import TuistCore -import TuistGraph +import XcodeGraph /** When Swift Packages don't declare the platforms that they support, the Swift Package Manager defaults the value @@ -12,40 +12,34 @@ import TuistGraph public struct ExternalProjectsPlatformNarrowerGraphMapper: GraphMapping { // swiftlint:disable:this type_name public init() {} - public func map(graph: Graph) async throws -> (Graph, [TuistCore.SideEffectDescriptor]) { + public func map( + graph: Graph, + environment: MapperEnvironment + ) async throws -> (Graph, [TuistCore.SideEffectDescriptor], MapperEnvironment) { + logger.debug("Transforming graph \(graph.name): Aligning external target platforms with locals'") + // If the project has no external dependencies we skip this. if graph.projects.values.first(where: { $0.isExternal }) == nil { - return (graph, []) + return (graph, [], environment) } 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 - ) } + project.targets = Dictionary(uniqueKeysWithValues: project.targets.map { _, target in + let mappedTarget = mapTarget( + target: target, + project: project, + externalTargetSupportedPlatforms: externalTargetSupportedPlatforms + ) + return (mappedTarget.name, mappedTarget) + }) return (projectPath, project) }) - return (graph, []) + return (graph, [], environment) } private func mapTarget( diff --git a/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift b/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift index 923c50a166b..b3979879b16 100644 --- a/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift +++ b/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift @@ -1,6 +1,6 @@ import Foundation import TuistCore -import TuistGraph +import XcodeGraph /** External dependencies might contain targets that are only relevant in development, but that @@ -10,24 +10,31 @@ import TuistGraph public struct PruneOrphanExternalTargetsGraphMapper: GraphMapping { public init() {} - public func map(graph: TuistGraph.Graph) async throws -> (TuistGraph.Graph, [TuistCore.SideEffectDescriptor]) { + public func map( + graph: XcodeGraph.Graph, + environment: MapperEnvironment + ) async throws -> (XcodeGraph.Graph, [TuistCore.SideEffectDescriptor], MapperEnvironment) { + logger.debug("Transforming graph \(graph.name): Tree-shaking orphan external targets (e.g. test targets)") + 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 + + graph.projects = Dictionary(uniqueKeysWithValues: graph.projects.map { projectPath, project in + var project = project + project.targets = Dictionary(uniqueKeysWithValues: project.targets.compactMap { _, 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) + var target = target + if orphanExternalTargets.contains(graphTarget) || target.destinations.isEmpty { + target.prune = true } + return (target.name, target) }) - return (projectPath, targets) + return (projectPath, project) }) - return (graph, []) + return (graph, [], environment) } } diff --git a/Sources/TuistDependencies/SwiftPackageManager/SwiftPackageManagerInteractor.swift b/Sources/TuistDependencies/SwiftPackageManager/SwiftPackageManagerInteractor.swift deleted file mode 100644 index 204d2741f6c..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/SwiftPackageManagerInteractor.swift +++ /dev/null @@ -1,251 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - Swift Package Manager Interactor Errors - -enum SwiftPackageManagerInteractorError: FatalError, Equatable { - /// Thrown when `Package.swift` cannot be found in temporary directory after `Swift Package Manager` installation. - case packageSwiftNotFound - /// Thrown when `Package.resolved` cannot be found in temporary directory after `Swift Package Manager` installation. - case packageResolvedNotFound - /// Thrown when `.build` directory cannot be found in temporary directory after `Swift Package Manager` installation. - case buildDirectoryNotFound - - /// Error type. - var type: ErrorType { - switch self { - case .packageSwiftNotFound, - .packageResolvedNotFound, - .buildDirectoryNotFound: - return .bug - } - } - - /// Error description. - var description: String { - switch self { - case .packageSwiftNotFound: - return "The Package.swift file was not found after resolving the dependencies using the Swift Package Manager." - case .packageResolvedNotFound: - return "The Package.resolved lockfile was not found after resolving the dependencies using the Swift Package Manager." - case .buildDirectoryNotFound: - return "The .build directory was not found after resolving the dependencies using the Swift Package Manager" - } - } -} - -// MARK: - Swift Package Manager Interacting - -public protocol SwiftPackageManagerInteracting { - /// Installs `Swift Package Manager` dependencies. - /// - Parameters: - /// - dependenciesDirectory: The path to the directory that contains the `Tuist/Dependencies/` directory. - /// - dependencies: List of dependencies to install using `Swift Package Manager`. - /// - platforms: Set of supported platforms. - /// - shouldUpdate: Indicates whether dependencies should be updated or fetched based on the lockfile. - /// - swiftToolsVersion: The version of Swift tools that will be used to resolve dependencies. If `nil` is passed then the - /// environment’s version will be used. - func install( - dependenciesDirectory: AbsolutePath, - dependencies: TuistGraph.SwiftPackageManagerDependencies, - platforms: Set, - shouldUpdate: Bool, - swiftToolsVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph - - /// Removes all cached `Swift Package Manager` dependencies. - /// - Parameter dependenciesDirectory: The path to the directory that contains the `Tuist/Dependencies/` directory. - func clean(dependenciesDirectory: AbsolutePath) throws -} - -// MARK: - Swift Package Manager Interactor - -public final class SwiftPackageManagerInteractor: SwiftPackageManagerInteracting { - private let fileHandler: FileHandling - private let swiftPackageManagerController: SwiftPackageManagerControlling - private let swiftPackageManagerGraphGenerator: SwiftPackageManagerGraphGenerating - - public init( - fileHandler: FileHandling = FileHandler.shared, - swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController(), - swiftPackageManagerGraphGenerator: SwiftPackageManagerGraphGenerating = SwiftPackageManagerGraphGenerator( - swiftPackageManagerController: SwiftPackageManagerController() - ) - ) { - self.fileHandler = fileHandler - self.swiftPackageManagerController = swiftPackageManagerController - self.swiftPackageManagerGraphGenerator = swiftPackageManagerGraphGenerator - } - - public func install( - dependenciesDirectory: AbsolutePath, - dependencies: TuistGraph.SwiftPackageManagerDependencies, - platforms: Set, - shouldUpdate: Bool, - swiftToolsVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - logger.info("Installing Swift Package Manager dependencies.", metadata: .subsection) - - let pathsProvider = SwiftPackageManagerPathsProvider( - dependenciesDirectory: dependenciesDirectory, - packagesOrManifest: dependencies.packagesOrManifest - ) - - try loadDependencies(pathsProvider: pathsProvider, dependencies: dependencies, swiftToolsVersion: swiftToolsVersion) - - if shouldUpdate { - try swiftPackageManagerController.update(at: pathsProvider.destinationSwiftPackageManagerDirectory, printOutput: true) - } else { - try swiftPackageManagerController.resolve( - at: pathsProvider.destinationSwiftPackageManagerDirectory, - printOutput: true - ) - } - - try saveDependencies(pathsProvider: pathsProvider) - - let dependenciesGraph = try swiftPackageManagerGraphGenerator.generate( - at: pathsProvider.destinationBuildDirectory, - productTypes: dependencies.productTypes, - platforms: platforms, - baseSettings: dependencies.baseSettings, - targetSettings: dependencies.targetSettings, - swiftToolsVersion: swiftToolsVersion, - projectOptions: dependencies.projectOptions - ) - - logger.info("Swift Package Manager dependencies installed successfully.", metadata: .subsection) - - return dependenciesGraph - } - - public func clean(dependenciesDirectory: AbsolutePath) throws { - for packagesOrManifest in [TuistGraph.PackagesOrManifest.packages([]), .manifest] { - let pathsProvider = SwiftPackageManagerPathsProvider( - dependenciesDirectory: dependenciesDirectory, - packagesOrManifest: packagesOrManifest - ) - try fileHandler.delete(pathsProvider.destinationSwiftPackageManagerDirectory) - try fileHandler.delete(pathsProvider.destinationPackageResolvedPath) - } - } - - // MARK: - Installation - - /// Loads lockfile and dependencies into working directory if they had been saved before. - private func loadDependencies( - pathsProvider: SwiftPackageManagerPathsProvider, - dependencies: TuistGraph.SwiftPackageManagerDependencies, - swiftToolsVersion: TSCUtility.Version? - ) throws { - let version = try swiftToolsVersion ?? - TSCUtility.Version(versionString: try System.shared.swiftVersion(), usesLenientParsing: true) - let isLegacy = version < TSCUtility.Version(5, 6, 0) - - // copy `Package.resolved` directory from lockfiles folder - if fileHandler.exists(pathsProvider.destinationPackageResolvedPath) { - try copy( - from: pathsProvider.destinationPackageResolvedPath, - to: pathsProvider.temporaryPackageResolvedPath - ) - } - - // create `Package.swift` - let packageManifestPath = pathsProvider.destinationPackageSwiftPath - try fileHandler.createFolder(packageManifestPath.removingLastComponent()) - let manifest = dependencies.manifest( - isLegacy: isLegacy, - packageManifestFolder: packageManifestPath.removingLastComponent() - ) - switch manifest { - case let .content(content): - try fileHandler.write(content, path: packageManifestPath, atomically: true) - case .manifest: - if fileHandler.exists(packageManifestPath) { - try fileHandler.replace(packageManifestPath, with: pathsProvider.sourcePackageSwiftPath) - } else { - try fileHandler.copy(from: pathsProvider.sourcePackageSwiftPath, to: packageManifestPath) - } - } - - // set `swift-tools-version` in `Package.swift` - try swiftPackageManagerController.setToolsVersion( - at: pathsProvider.destinationSwiftPackageManagerDirectory, - to: version - ) - - let generatedManifestContent = try fileHandler.readTextFile(packageManifestPath) - logger.debug("Package.swift:", metadata: .subsection) - logger.debug("\(generatedManifestContent)") - } - - /// Saves lockfile resolved dependencies in `Tuist/Dependencies` directory. - private func saveDependencies(pathsProvider: SwiftPackageManagerPathsProvider) throws { - guard fileHandler.exists(pathsProvider.destinationPackageSwiftPath) else { - throw SwiftPackageManagerInteractorError.packageSwiftNotFound - } - guard fileHandler.exists(pathsProvider.destinationBuildDirectory) else { - throw SwiftPackageManagerInteractorError.buildDirectoryNotFound - } - - if fileHandler.exists(pathsProvider.temporaryPackageResolvedPath) { - try copy( - from: pathsProvider.temporaryPackageResolvedPath, - to: pathsProvider.destinationPackageResolvedPath - ) - } - - // remove temporary files - try? FileHandler.shared.delete(pathsProvider.temporaryPackageResolvedPath) - } - - // MARK: - Helpers - - private func copy(from fromPath: AbsolutePath, to toPath: AbsolutePath) throws { - if fileHandler.exists(toPath) { - try fileHandler.replace(toPath, with: fromPath) - } else { - try fileHandler.createFolder(toPath.removingLastComponent()) - try fileHandler.copy(from: fromPath, to: toPath) - } - } -} - -// MARK: - Models - -private struct SwiftPackageManagerPathsProvider { - let destinationSwiftPackageManagerDirectory: AbsolutePath - let destinationPackageSwiftPath: AbsolutePath - let destinationPackageResolvedPath: AbsolutePath - let destinationBuildDirectory: AbsolutePath - let sourcePackageSwiftPath: AbsolutePath - - let temporaryPackageResolvedPath: AbsolutePath - - init(dependenciesDirectory: AbsolutePath, packagesOrManifest: TuistGraph.PackagesOrManifest) { - let tuistDirectory = dependenciesDirectory.removingLastComponent() - destinationPackageSwiftPath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.swiftPackageManagerDirectoryName) - .appending(component: Constants.DependenciesDirectory.packageSwiftName) - switch packagesOrManifest { - case .packages: - destinationPackageResolvedPath = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - .appending(component: Constants.DependenciesDirectory.packageResolvedName) - case .manifest: - destinationPackageResolvedPath = tuistDirectory - .appending(component: Constants.DependenciesDirectory.packageResolvedName) - } - destinationSwiftPackageManagerDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.swiftPackageManagerDirectoryName) - destinationBuildDirectory = destinationSwiftPackageManagerDirectory.appending(component: ".build") - sourcePackageSwiftPath = tuistDirectory.appending(component: Constants.DependenciesDirectory.packageSwiftName) - - temporaryPackageResolvedPath = destinationSwiftPackageManagerDirectory - .appending(component: Constants.DependenciesDirectory.packageResolvedName) - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/ManifestMapping/ProjectOption+ManifestMapper.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/ManifestMapping/ProjectOption+ManifestMapper.swift deleted file mode 100644 index 6b80e1cfd51..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/ManifestMapping/ProjectOption+ManifestMapper.swift +++ /dev/null @@ -1,81 +0,0 @@ -import ProjectDescription -import TuistGraph - -extension ProjectDescription.Project.Options { - /// Maps a TuistGraph.ProjectOption instance into a ProjectDescription.ProjectOption instance. - /// - Parameters: - /// - manifest: Manifest representation of project options. - static func from(manifest: TuistGraph.Project.Options) -> Self { - options( - automaticSchemesOptions: .from(manifest: manifest.automaticSchemesOptions), - disableBundleAccessors: manifest.disableBundleAccessors, - disableShowEnvironmentVarsInScriptPhases: manifest.disableShowEnvironmentVarsInScriptPhases, - disableSynthesizedResourceAccessors: manifest.disableSynthesizedResourceAccessors, - textSettings: .textSettings( - usesTabs: manifest.textSettings.usesTabs, - indentWidth: manifest.textSettings.indentWidth, - tabWidth: manifest.textSettings.tabWidth, - wrapsLines: manifest.textSettings.wrapsLines - ) - ) - } -} - -extension ProjectDescription.Project.Options.AutomaticSchemesOptions { - static func from( - manifest: TuistGraph.Project.Options.AutomaticSchemesOptions - ) -> Self { - switch manifest { - case let .enabled( - targetSchemesGrouping, - codeCoverageEnabled, - testingOptions, - testLanguage, - testRegion, - testScreenCaptureFormat, - runLanguage, - runRegion - ): - return .enabled( - targetSchemesGrouping: .from(manifest: targetSchemesGrouping), - codeCoverageEnabled: codeCoverageEnabled, - testingOptions: .from(manifest: testingOptions), - testLanguage: testLanguage.map { .init(identifier: $0) }, - testRegion: testRegion, - testScreenCaptureFormat: testScreenCaptureFormat.map { .from(manifest: $0) }, - runLanguage: runLanguage.map { .init(identifier: $0) }, - runRegion: runRegion - ) - case .disabled: - return .disabled - } - } -} - -extension ProjectDescription.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping { - static func from( - manifest: TuistGraph.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping - ) -> Self { - switch manifest { - case .singleScheme: - return .singleScheme - case let .byNameSuffix(build, test, run): - return .byNameSuffix(build: build, test: test, run: run) - case .notGrouped: - return .notGrouped - } - } -} - -extension ProjectDescription.ScreenCaptureFormat { - static func from( - manifest: TuistGraph.ScreenCaptureFormat - ) -> Self { - switch manifest { - case .screenshots: - return .screenshots - case .screenRecording: - return .screenRecording - } - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/ManifestMapping/TestingOptions+ManifestMapper.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/ManifestMapping/TestingOptions+ManifestMapper.swift deleted file mode 100644 index 345c65af6fc..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/ManifestMapping/TestingOptions+ManifestMapper.swift +++ /dev/null @@ -1,23 +0,0 @@ -import ProjectDescription -import TuistGraph - -extension ProjectDescription.TestingOptions { - /// Maps a TuistGraph.TestingOptions instance into a ProjectDescription.TestingOptions instance. - /// - Parameters: - /// - manifest: Manifest representation of testing options. - static func from( - manifest: TuistGraph.TestingOptions - ) -> Self { - var options: Self = [] - - if manifest.contains(.parallelizable) { - options.insert(.parallelizable) - } - - if manifest.contains(.randomExecutionOrdering) { - options.insert(.randomExecutionOrdering) - } - - return options - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfo+graphPlatform.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfo+graphPlatform.swift deleted file mode 100644 index d5463e0aabd..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfo+graphPlatform.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import ProjectDescription -import TuistGraph -import TuistSupport - -extension PackageInfo.Platform { - func graphPlatform() throws -> TuistGraph.Platform { - switch platformName.lowercased() { - case "ios", "maccatalyst": - return .iOS - case "macos": - return .macOS - case "tvos": - return .tvOS - case "watchos": - return .watchOS - case "visionos": - return .visionOS - default: - throw PackageInfoMapperError.unknownPlatform(platformName) - } - } - - func destinations() throws -> ProjectDescription.Destinations { - switch platformName.lowercased() { - case "ios": - return [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - case "maccatalyst": - return [.macCatalyst] - case "macos": - return [.mac] - case "tvos": - return [.appleTv] - case "watchos": - return [.appleWatch] - case "visionos": - return [.appleVision] - default: - throw PackageInfoMapperError.unknownPlatform(platformName) - } - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift deleted file mode 100644 index 842dcb91209..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift +++ /dev/null @@ -1,1446 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - PackageInfo Mapper Errors - -enum PackageInfoMapperError: FatalError, Equatable { - /// Thrown when the default path folder is not present. - case defaultPathNotFound(AbsolutePath, String) - - /// Thrown when the parsing of minimum deployment target failed. - case minDeploymentTargetParsingFailed(ProjectDescription.Platform) - - /// Thrown when no supported platforms are found for a package. - case noSupportedPlatforms( - name: String, - configured: Set, - package: Set - ) - - /// Thrown when `PackageInfo.Target.Dependency.byName` dependency cannot be resolved. - case unknownByNameDependency(String) - - /// Thrown when `PackageInfo.Platform` name cannot be mapped to a `DeploymentTarget`. - case unknownPlatform(String) - - /// Thrown when `PackageInfo.Target.Dependency.product` dependency cannot be resolved. - case unknownProductDependency(String, String) - - /// Thrown when a target defined in a product is not present in the package - case unknownProductTarget(package: String, product: String, target: String) - - /// Thrown when unsupported `PackageInfo.Target.TargetBuildSettingDescription` `Tool`/`SettingName` pair is found. - case unsupportedSetting( - PackageInfo.Target.TargetBuildSettingDescription.Tool, - PackageInfo.Target.TargetBuildSettingDescription.SettingName - ) - - /// Thrown when a binary target defined in a package doesn't have a corresponding artifact - case missingBinaryArtifact(package: String, target: String) - - case modulemapMissing(moduleMapPath: String, package: String, target: String) - - /// Error type. - var type: ErrorType { - switch self { - case .noSupportedPlatforms, .unknownByNameDependency, .unknownPlatform, .unknownProductDependency, .unknownProductTarget, - .modulemapMissing: - return .abort - case .minDeploymentTargetParsingFailed, .defaultPathNotFound, .unsupportedSetting, .missingBinaryArtifact: - return .bug - } - } - - /// Error description. - var description: String { - switch self { - case let .defaultPathNotFound(packageFolder, targetName): - return """ - Default source path not found for target \(targetName) in package at \(packageFolder.pathString). \ - Source path must be one of \(PackageInfoMapper.predefinedSourceDirectories.map { "\($0)/\(targetName)" }) - """ - case let .minDeploymentTargetParsingFailed(platform): - return "The minimum deployment target for \(platform) platform cannot be parsed." - case let .noSupportedPlatforms(name, configured, package): - return "No supported platform found for the \(name) dependency. Configured: \(configured), package: \(package)." - case let .unknownByNameDependency(name): - return "The package associated to the \(name) dependency cannot be found." - case let .unknownPlatform(platform): - return "The \(platform) platform is not supported." - case let .unknownProductDependency(name, package): - return "The product \(name) of package \(package) cannot be found." - case let .unknownProductTarget(package, product, target): - return "The target \(target) of product \(product) cannot be found in package \(package)." - case let .unsupportedSetting(tool, setting): - return "The \(tool) and \(setting) pair is not a supported setting." - case let .missingBinaryArtifact(package, target): - return "The artifact for binary target \(target) of package \(package) cannot be found." - case let .modulemapMissing(moduleMapPath, package, target): - return "Target \(target) of package \(package) is a system library. Module map is missing at \(moduleMapPath)." - } - } -} - -// MARK: - PackageInfo Mapper - -/// Protocol that allows to map a `PackageInfo` to a `ProjectDescription.Project`. -public protocol PackageInfoMapping { - /// Preprocesses SwiftPackageManager dependencies. - /// - Parameters: - /// - packageInfos: All available `PackageInfo`s - /// - idToPackage: Mapping from an identifier to its package - /// - packageToFolder: Mapping from a package name to its local folder - /// - packageToTargetsToArtifactPaths: Mapping from a package name its targets' names to artifacts' paths - /// - platforms: The configured platforms - /// - Returns: Mapped project - func preprocess( - packageInfos: [String: PackageInfo], - idToPackage: [String: String], - packageToFolder: [String: AbsolutePath], - packageToTargetsToArtifactPaths: [String: [String: AbsolutePath]] - ) throws -> PackageInfoMapper.PreprocessInfo - - /// Maps a `PackageInfo` to a `ProjectDescription.Project`. - /// - Parameters: - /// - packageInfo: `PackageInfo` to be mapped - /// - packageInfos: All available `PackageInfo`s - /// - name: Name of the package - /// - path: Path of the package - /// - productTypes: Product type mapping - /// - baseSettings: Base settings - /// - targetSettings: Settings to apply to denoted targets - /// - configuration: Configure automatic schemes and resource accessors generation for Swift Package - /// - projectOptions: Additional options related to the `Project` - /// - targetToPlatform: Mapping from a target name to its platform - /// - minDeploymentTargets: Minimum support deployment target per platform - /// - platforms: Set of supported platforms - /// - targetToProducts: Mapping from a target name to its products - /// - targetToResolvedDependencies: Mapping from a target name to its dependencies - /// - targetToModuleMap: Mapping from a target name to its module map - /// - packageToProject: Mapping from a package name to its path - /// - swiftToolsVersion: The version of Swift tools that will be used to map dependencies - /// - Returns: Mapped project - func map( - packageInfo: PackageInfo, - packageInfos: [String: PackageInfo], - name: String, - path: AbsolutePath, - productTypes: [String: TuistGraph.Product], - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary], - projectOptions: TuistGraph.Project.Options?, - minDeploymentTargets: ProjectDescription.DeploymentTargets, - destinations: ProjectDescription.Destinations, - targetToProducts: [String: Set], - targetToResolvedDependencies: [String: [PackageInfoMapper.ResolvedDependency]], - macroDependencies: Set, - targetToModuleMap: [String: ModuleMap], - packageToProject: [String: AbsolutePath], - swiftToolsVersion: TSCUtility.Version? - ) throws -> ProjectDescription.Project? -} - -// swiftlint:disable:next type_body_length -public final class PackageInfoMapper: PackageInfoMapping { - public struct PreprocessInfo { - let platformToMinDeploymentTarget: ProjectDescription.DeploymentTargets - let productToExternalDependencies: [String: [ProjectDescription.TargetDependency]] - let targetToProducts: [String: Set] - let targetToResolvedDependencies: [String: [PackageInfoMapper.ResolvedDependency]] - let targetToModuleMap: [String: ModuleMap] - let macroDependencies: Set - } - - // Predefined source directories, in order of preference. - // https://github.com/apple/swift-package-manager/blob/751f0b2a00276be2c21c074f4b21d952eaabb93b/Sources/PackageLoading/PackageBuilder.swift#L488 - fileprivate static let predefinedSourceDirectories = ["Sources", "Source", "src", "srcs"] - fileprivate let moduleMapGenerator: SwiftPackageManagerModuleMapGenerating - - public init(moduleMapGenerator: SwiftPackageManagerModuleMapGenerating = SwiftPackageManagerModuleMapGenerator()) { - self.moduleMapGenerator = moduleMapGenerator - } - - /// Resolves all SwiftPackageManager dependencies. - /// - Parameters: - /// - packageInfos: All available `PackageInfo`s - /// - idToPackage: Mapping from an identifier to its package - /// - packageToFolder: Mapping from a package name to its local folder - /// - packageToTargetsToArtifactPaths: Mapping from a package name its targets' names to artifacts' paths - /// - platforms: The configured platforms - /// - Returns: Mapped project - public func preprocess( // swiftlint:disable:this function_body_length - packageInfos: [String: PackageInfo], - idToPackage: [String: String], - packageToFolder: [String: AbsolutePath], - packageToTargetsToArtifactPaths: [String: [String: AbsolutePath]] - ) throws -> PreprocessInfo { - let targetDependencyToFramework: [String: Path] = try packageInfos.reduce(into: [:]) { result, packageInfo in - try packageInfo.value.targets.forEach { target in - guard target.type == .binary else { return } - if let path = target.path { - // local binary - result[target.name] = Path( - packageToFolder[packageInfo.key]!.appending(try RelativePath(validating: path)) - .pathString - ) - } else { - // remote binaries are checked out by SPM in artifacts//.xcframework - // or in artifacts//.xcframework when using SPM 5.6 and later - guard let artifactPath = packageToTargetsToArtifactPaths[packageInfo.key]?[target.name] else { - throw PackageInfoMapperError.missingBinaryArtifact(package: packageInfo.key, target: target.name) - } - result[target.name] = Path(artifactPath.pathString) - } - } - } - - let targetToProducts: [String: Set] = packageInfos.values.reduce(into: [:]) { result, packageInfo in - for product in packageInfo.products { - var targetsToProcess = Set(product.targets) - while !targetsToProcess.isEmpty { - let target = targetsToProcess.removeFirst() - let alreadyProcessed = result[target]?.contains(product) ?? false - guard !alreadyProcessed else { - continue - } - result[target, default: []].insert(product) - let dependencies = packageInfo.targets.first(where: { $0.name == target })!.dependencies - for dependency in dependencies { - switch dependency { - case let .target(name, _): - targetsToProcess.insert(name) - case let .byName(name, _) where packageInfo.targets.contains(where: { $0.name == name }): - targetsToProcess.insert(name) - case .byName, .product: - continue - } - } - } - } - } - - let resolvedDependencies: [String: [ResolvedDependency]] = try packageInfos.values - .reduce(into: [:]) { result, packageInfo in - try packageInfo.targets - .filter { - targetToProducts[$0.name] != nil - } - .forEach { target in - guard result[target.name] == nil else { return } - result[target.name] = try ResolvedDependency.from( - dependencies: target.dependencies, - packageInfo: packageInfo, - packageInfos: packageInfos, - idToPackage: idToPackage, - targetDependencyToFramework: targetDependencyToFramework - ) - } - } - - var macroTargetsAndDescendants = Set(packageInfos.values.flatMap { $0.targets.filter { $0.type == .macro }.map(\.name) }) - var visited: Set = [] - var macroDependencies = Set() - - while !macroTargetsAndDescendants.isEmpty { - guard let targetName = macroTargetsAndDescendants.popFirst(), !visited.contains(targetName) else { - continue - } - - visited.insert(targetName) - - for dependency in resolvedDependencies[targetName] ?? [] { - macroDependencies.insert(dependency) - let dependencyTargetName = dependency.targetName - if let dependencyTargetName, !visited.contains(dependencyTargetName) { - macroTargetsAndDescendants.insert(dependencyTargetName) - } - } - } - - var externalDependencies: [String: [ProjectDescription.TargetDependency]] = .init() - externalDependencies = try packageInfos - .reduce(into: [:]) { result, packageInfo in - for product in packageInfo.value.products { - result[product.name] = try product.targets.flatMap { target in - try ResolvedDependency.fromTarget( - name: target, - targetDependencyToFramework: targetDependencyToFramework, - condition: nil - ) - .map { - switch $0 { - case let .xcframework(path, condition): - return .xcframework(path: path, condition: condition) - case let .target(name, condition): - return .project( - target: name, - path: Path(packageToFolder[packageInfo.key]!.pathString), - condition: condition - ) - case .externalTarget: - throw PackageInfoMapperError.unknownProductTarget( - package: packageInfo.key, - product: product.name, - target: target - ) - } - } - } - } - } - - let version = try Version(versionString: try System.shared.swiftVersion(), usesLenientParsing: true) - let minDeploymentTargets = ProjectDescription.DeploymentTargets.oldestVersions(for: version) - - let targetToModuleMap: [String: ModuleMap] - targetToModuleMap = try packageInfos.reduce(into: [:]) { result, packageInfo 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 - /// https://github.com/apple/swift-package-manager/blob/main/Sources/PackageLoading/ModuleMapGenerator.swift - let packagePath = try target.basePath(packageFolder: packageToFolder[packageInfo.key]!) - let moduleMapPath = packagePath.appending(component: ModuleMap.filename) - - guard FileHandler.shared.exists(moduleMapPath), !FileHandler.shared.isFolder(moduleMapPath) else { - throw PackageInfoMapperError.modulemapMissing( - moduleMapPath: moduleMapPath.pathString, - package: packageInfo.key, - target: target.name - ) - } - - result[target.name] = ModuleMap.custom(moduleMapPath) - case .regular: - result[target.name] = try moduleMapGenerator.generate( - moduleName: target.name, - publicHeadersPath: target.publicHeadersPath(packageFolder: packageToFolder[packageInfo.key]!) - ) - default: - continue - } - } - } - - return .init( - platformToMinDeploymentTarget: minDeploymentTargets, - productToExternalDependencies: externalDependencies, - targetToProducts: targetToProducts, - targetToResolvedDependencies: resolvedDependencies, - targetToModuleMap: targetToModuleMap, - macroDependencies: macroDependencies - ) - } - - /** - There are certain Swift Package targets that need to run on macOS. Examples of these are Swift Macros. - It's important that we take that into account when generating and serializing the graph, which contains information - about targets' macros, into disk. It's important to note that these targets require its dependencies, direct or transitive, - to compile for macOS too. This function traverses the graph and returns all the targets that need to compile for macOS - in a set. The set is then used in the serialization logic when: - - - Unfolding the target into platform-specific targets. - - Declaring dependencies. - - All the complexity associated to this might go away once we have support for multi-platform targets. - */ - private func macOSTargets( - _ resolvedDependencies: [String: [ResolvedDependency]], - packageInfos: [String: PackageInfo] - ) -> Set { - let targetTypes = packageInfos.reduce(into: [String: PackageInfo.Target.TargetType]()) { partialResult, item in - for target in item.value.targets { - partialResult[target.name] = target.type - } - } - - var targets = Set() - - func visit(target: String, parentMacOS: Bool) { - let isMacOS = targetTypes[target] == .macro || parentMacOS - if isMacOS { - targets.insert(target) - } - let dependencies = resolvedDependencies[target] ?? [] - for dependency in dependencies { - switch dependency { - case let .target(name, _): - visit(target: name, parentMacOS: isMacOS) - case let .externalTarget(_, name, _): - visit(target: name, parentMacOS: isMacOS) - case .xcframework: - break - } - } - } - - for target in resolvedDependencies.keys.sorted() { - visit(target: target, parentMacOS: false) - } - - return targets - } - - // swiftlint:disable:next function_body_length - public func map( - packageInfo: PackageInfo, - packageInfos: [String: PackageInfo], - name: String, - path: AbsolutePath, - productTypes: [String: TuistGraph.Product], - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary], - projectOptions: TuistGraph.Project.Options?, - minDeploymentTargets: ProjectDescription.DeploymentTargets, - destinations: ProjectDescription.Destinations, - targetToProducts: [String: Set], - targetToResolvedDependencies: [String: [PackageInfoMapper.ResolvedDependency]], - macroDependencies: Set, - targetToModuleMap: [String: ModuleMap], - packageToProject: [String: AbsolutePath], - swiftToolsVersion: TSCUtility.Version? - ) throws -> ProjectDescription.Project? { - // Hardcoded mapping for some well known libraries, until the logic can handle those properly - let productTypes = productTypes.merging( - // Force dynamic frameworks - Dictionary( - uniqueKeysWithValues: [ - "Checksum", // https://github.com/rnine/Checksum - "RxSwift", // https://github.com/ReactiveX/RxSwift - ].map { - ($0, .framework) - } - ), - uniquingKeysWith: { userDefined, _ in userDefined } - ) - - let targetSettings = targetSettings.merging( - // Force enable testing search paths - Dictionary( - uniqueKeysWithValues: [ - "Nimble", // https://github.com/Quick/Nimble - "Quick", // https://github.com/Quick/Quick - "RxTest", // https://github.com/ReactiveX/RxSwift - "RxTest-Dynamic", // https://github.com/ReactiveX/RxSwift - "SnapshotTesting", // https://github.com/pointfreeco/swift-snapshot-testing - "SwiftyMocky", // https://github.com/MakeAWishFoundation/SwiftyMocky - "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"]) - } - ), - uniquingKeysWith: { userDefined, defaultDictionary in - userDefined.merging(defaultDictionary, uniquingKeysWith: { userDefined, _ in userDefined }) - } - ) - - let targets: [ProjectDescription.Target] = try packageInfo.targets - .compactMap { target -> ProjectDescription.Target? in - guard let products = targetToProducts[target.name] else { return nil } - - return try ProjectDescription.Target.from( - target: target, - products: products, - packageName: name, - packageInfo: packageInfo, - packageInfos: packageInfos, - packageFolder: path, - packageToProject: packageToProject, - productTypes: productTypes, - baseSettings: baseSettings, - targetSettings: targetSettings, - packageDestinations: destinations, - minDeploymentTargets: minDeploymentTargets, - targetToResolvedDependencies: targetToResolvedDependencies, - targetToModuleMap: targetToModuleMap, - macroDependencies: macroDependencies - ) - } - - guard !targets.isEmpty else { - return nil - } - - let options: ProjectDescription.Project.Options - if let projectOptions { - options = .from(manifest: projectOptions) - } else { - options = .options( - automaticSchemesOptions: .disabled, - disableSynthesizedResourceAccessors: true - ) - } - - return ProjectDescription.Project( - name: name, - options: options, - settings: packageInfo.projectSettings( - swiftToolsVersion: swiftToolsVersion, - buildConfigs: baseSettings.configurations.map { key, _ in key } - ), - targets: targets, - resourceSynthesizers: .default - ) - } - - fileprivate class func sanitize(targetName: String) -> String { - targetName.replacingOccurrences(of: ".", with: "_") - } -} - -extension ProjectDescription.Target { - // swiftlint:disable:next function_body_length - fileprivate static func from( - target: PackageInfo.Target, - products: Set, - packageName: String, - packageInfo: PackageInfo, - packageInfos: [String: PackageInfo], - packageFolder: AbsolutePath, - packageToProject: [String: AbsolutePath], - productTypes: [String: TuistGraph.Product], - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary], - packageDestinations: ProjectDescription.Destinations, - minDeploymentTargets: ProjectDescription.DeploymentTargets, - targetToResolvedDependencies: [String: [PackageInfoMapper.ResolvedDependency]], - targetToModuleMap: [String: ModuleMap], - macroDependencies: Set - ) throws -> Self? { - guard target.type.isSupported else { - logger.debug("Target \(target.name) of type \(target.type) ignored") - return nil - } - - guard let product = ProjectDescription.Product.from( - name: target.name, - type: target.type, - products: products, - productTypes: productTypes - ) - else { - logger.debug("Target \(target.name) ignored by product type") - return nil - } - - let path = try target.basePath(packageFolder: packageFolder) - - let moduleMap = targetToModuleMap[target.name] - - var destinations: ProjectDescription.Destinations - if target.type == .macro { - destinations = Set([.mac]) - } else { - // All packages implicitly support all platforms, we constrain this with the platforms defined in `Dependencies.swift` - destinations = packageDestinations.intersection(Set(Destination.allCases)) - } - - if macroDependencies.contains(where: { dependency in - switch dependency { - case let .externalTarget(_, targetName, _), .target(let targetName, condition: _): - return target.name == targetName - default: - return false - } - }) { - destinations.insert(.mac) - } - - let deploymentTargets = try ProjectDescription.DeploymentTargets.from( - minDeploymentTargets: minDeploymentTargets, - package: packageInfo.platforms, - destinations: destinations, - packageName: packageName - ) - - var publicHeadersPath: AbsolutePath? - var headers: ProjectDescription.Headers? - var sources: SourceFilesList? - var resources: ResourceFileElements? - - if target.type.supportsPublicHeaderPath { - publicHeadersPath = try target.publicHeadersPath(packageFolder: packageFolder) - headers = try Headers.from(moduleMap: moduleMap, publicHeadersPath: publicHeadersPath!) - } - - if target.type.supportsSources { - sources = try SourceFilesList.from(sources: target.sources, path: path, excluding: target.exclude) - } - - if target.type.supportsResources { - resources = try ResourceFileElements.from( - sources: target.sources, - resources: target.resources, - path: path, - excluding: target.exclude - ) - } - - var dependencies: [ProjectDescription.TargetDependency] = [] - - if target.type.supportsDependencies { - let resolvedDependencies = targetToResolvedDependencies[target.name] ?? [] - - dependencies = try ProjectDescription.TargetDependency.from( - resolvedDependencies: resolvedDependencies, - settings: target.settings, - packageToProject: packageToProject - ) - } - - let settings = try Settings.from( - target: target, - targetDestinations: destinations, - packageFolder: packageFolder, - packageName: packageName, - packageInfos: packageInfos, - packageToProject: packageToProject, - targetToResolvedDependencies: targetToResolvedDependencies, - settings: target.settings, - platforms: packageInfo.platforms, - targetToModuleMap: targetToModuleMap, - baseSettings: baseSettings, - targetSettings: targetSettings - ) - - return ProjectDescription.Target( - name: PackageInfoMapper.sanitize(targetName: target.name), - destinations: destinations, - product: product, - productName: PackageInfoMapper - .sanitize(targetName: target.name) - .replacingOccurrences(of: "-", with: "_"), - bundleId: target.name - .replacingOccurrences(of: "_", with: "."), - deploymentTargets: deploymentTargets, - infoPlist: .default, - sources: sources, - resources: resources, - headers: headers, - dependencies: dependencies, - settings: settings - ) - } -} - -extension ProjectDescription.DeploymentTargets { - /// A dictionary that contains the oldest supported version of each platform - public static func oldestVersions(for swiftVersion: TSCUtility.Version) -> ProjectDescription.DeploymentTargets { - if swiftVersion < Version(5, 7, 0) { - return DeploymentTargets( - iOS: "9.0", - macOS: "10.10", - watchOS: "2.0", - tvOS: "9.0", - visionOS: "1.0" - ) - } else if swiftVersion < Version(5, 9, 0) { - return DeploymentTargets( - iOS: "11.0", - macOS: "10.13", - watchOS: "4.0", - tvOS: "11.0", - visionOS: "1.0" - ) - } else { - return DeploymentTargets( - iOS: "12.0", - macOS: "10.13", - watchOS: "4.0", - tvOS: "12.0", - visionOS: "1.0" - ) - } - } - - fileprivate static func from( - minDeploymentTargets: ProjectDescription.DeploymentTargets, - package: [PackageInfo.Platform], - destinations: ProjectDescription.Destinations, - packageName _: String - ) throws -> Self { - let versionPairs: [(ProjectDescription.Platform, String)] = package.compactMap { packagePlatform in - guard let tuistPlatform = ProjectDescription.Platform(rawValue: packagePlatform.tuistPlatformName) else { return nil } - return (tuistPlatform, packagePlatform.version) - } - // maccatalyst and iOS will be the same, this chooses the first one defined, hopefully they dont disagree - let platformInfos = Dictionary(versionPairs) { first, _ in first } - let destinationPlatforms = destinations.platforms - - func versionFor(platform: ProjectDescription.Platform) throws -> String? { - guard destinationPlatforms.contains(platform) else { return nil } - return try max(minDeploymentTargets[platform], platformInfos[platform]) - } - - return .init( - iOS: try versionFor(platform: .iOS), - macOS: try versionFor(platform: .macOS), - watchOS: try versionFor(platform: .watchOS), - tvOS: try versionFor(platform: .tvOS), - visionOS: try versionFor(platform: .visionOS) - ) - } - - fileprivate static func max(_ lVersionString: String?, _ rVersionString: String?) throws -> String? { - guard let rVersionString else { return lVersionString } - guard let lVersionString else { return nil } - let lVersion = try Version(versionString: lVersionString, usesLenientParsing: true) - let rVersion = try Version(versionString: rVersionString, usesLenientParsing: true) - return lVersion > rVersion ? lVersionString : rVersionString - } -} - -extension ProjectDescription.Product { - fileprivate static func from( - name: String, - type: PackageInfo.Target.TargetType, - products: Set, - productTypes: [String: TuistGraph.Product] - ) -> Self? { - // Swift Macros are command line tools that run in the host (macOS) at compilation time. - if type == .macro { - return .macro - } - - if let productType = productTypes[name] { - return ProjectDescription.Product.from(product: productType) - } - - var hasLibraryProducts = false - let product: ProjectDescription.Product? = products.map(\.type).reduce(nil) { result, productType in - switch productType { - case let .library(type): - hasLibraryProducts = true - switch type { - case .automatic: - return result - case .static: - return .staticFramework - case .dynamic: - if result == .staticFramework { - // If any of the products is static, the target must be static - return result - } else { - return .framework - } - } - case .executable, .plugin, .test: - return result - } - } - - if product != nil { - return product - } else if hasLibraryProducts { - // only automatic products, default to static framework - return .staticFramework - } else { - // only executable, plugin, or test products, ignore it - return nil - } - } -} - -extension SourceFilesList { - fileprivate static func from(sources: [String]?, path: AbsolutePath, excluding: [String]) throws -> Self? { - let sourcesPaths: [AbsolutePath] - if let customSources = sources { - sourcesPaths = try customSources.map { source in - let absolutePath = path.appending(try RelativePath(validating: source)) - if absolutePath.extension == nil { - return absolutePath.appending(component: "**") - } - return absolutePath - } - } else { - sourcesPaths = [path.appending(component: "**")] - } - guard !sourcesPaths.isEmpty else { return nil } - return .init( - globs: try sourcesPaths.map { absolutePath -> ProjectDescription.SourceFileGlob in - .glob( - Path(absolutePath.pathString), - excluding: try excluding.map { - let excludePath = path.appending(try RelativePath(validating: $0)) - let excludeGlob = excludePath.extension != nil ? excludePath : excludePath.appending(component: "**") - return Path(excludeGlob.pathString) - } - ) - } - ) - } -} - -extension ResourceFileElements { - fileprivate static func from( - sources: [String]?, - resources: [PackageInfo.Target.Resource], - path: AbsolutePath, - excluding: [String] - ) throws -> Self? { - /// Handles the conversion of a `.copy` resource rule of SPM - /// - /// - Parameters: - /// - resourceAbsolutePath: The absolute path of that resource - /// - Returns: A ProjectDescription.ResourceFileElement mapped from a `.copy` resource rule of SPM - func handleCopyResource(resourceAbsolutePath: AbsolutePath) -> ProjectDescription.ResourceFileElement { - .folderReference(path: Path(resourceAbsolutePath.pathString)) - } - - /// Handles the conversion of a `.process` resource rule of SPM - /// - /// - Parameters: - /// - resourceAbsolutePath: The absolute path of that resource - /// - Returns: A ProjectDescription.ResourceFileElement mapped from a `.process` resource rule of SPM - func handleProcessResource(resourceAbsolutePath: AbsolutePath) throws -> ProjectDescription.ResourceFileElement { - let absolutePathGlob = resourceAbsolutePath.extension != nil ? resourceAbsolutePath : resourceAbsolutePath - .appending(component: "**") - return .glob( - pattern: Path(absolutePathGlob.pathString), - excluding: try excluding.map { - let excludePath = path.appending(try RelativePath(validating: $0)) - let excludeGlob = excludePath.extension != nil ? excludePath : excludePath.appending(component: "**") - return Path(excludeGlob.pathString) - } - ) - } - - var resourceFileElements: [ProjectDescription.ResourceFileElement] = try resources.map { - let resourceAbsolutePath = path.appending(try RelativePath(validating: $0.path)) - - switch $0.rule { - case .copy: - // Single files or opaque directories are handled like a .process rule - if !FileHandler.shared.isFolder(resourceAbsolutePath) || resourceAbsolutePath.isOpaqueDirectory { - return try handleProcessResource(resourceAbsolutePath: resourceAbsolutePath) - } else { - return handleCopyResource(resourceAbsolutePath: resourceAbsolutePath) - } - case .process: - return try handleProcessResource(resourceAbsolutePath: resourceAbsolutePath) - } - } - - // Add default resources path if necessary - // They are handled like a `.process` rule - if sources == nil { - resourceFileElements += try defaultResourcePaths(from: path) - .map { try handleProcessResource(resourceAbsolutePath: $0) } - } - - // Check for empty resource files - guard !resourceFileElements.isEmpty else { return nil } - - return .init(resources: resourceFileElements) - } - - // These files are automatically added as resource if they are inside targets directory. - // Check https://developer.apple.com/documentation/swift_packages/bundling_resources_with_a_swift_package - private static let defaultSpmResourceFileExtensions = [ - "xib", - "storyboard", - "xcdatamodeld", - "xcmappingmodel", - "xcassets", - "strings", - ] - - private static func defaultResourcePaths(from path: AbsolutePath) -> [AbsolutePath] { - ResourceFileElements.defaultSpmResourceFileExtensions.flatMap { - FileHandler.shared.glob(path, glob: "**/*.\($0)") - } - } -} - -extension ProjectDescription.TargetDependency { - fileprivate static func from( - resolvedDependencies: [PackageInfoMapper.ResolvedDependency], - settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting], - packageToProject: [String: AbsolutePath] - ) throws -> [Self] { - let targetDependencies = resolvedDependencies.compactMap { dependency -> Self? in - switch dependency { - case let .target(name, condition): - return .target(name: name, condition: condition) - case let .xcframework(path, condition): - return .xcframework(path: path, condition: condition) - case let .externalTarget(project, target, condition): - return .project( - target: target, - path: Path(packageToProject[project]!.pathString), - condition: condition - ) - } - } - - let linkerDependencies: [ProjectDescription.TargetDependency] = settings.compactMap { setting in - do { - let condition = try ProjectDescription.PlatformCondition.from(setting.condition) - - switch (setting.tool, setting.name) { - case (.linker, .linkedFramework): - return .sdk(name: setting.value[0], type: .framework, status: .required, condition: condition) - case (.linker, .linkedLibrary): - return .sdk(name: setting.value[0], type: .library, status: .required, condition: condition) - case (.c, _), (.cxx, _), (_, .enableUpcomingFeature), (.swift, _), (.linker, .headerSearchPath), ( - .linker, - .define - ), - (.linker, .unsafeFlags), (_, .enableExperimentalFeature): - return nil - } - } catch { - return nil - } - } - - return targetDependencies + linkerDependencies - } -} - -extension ProjectDescription.Headers { - fileprivate static func from(moduleMap: ModuleMap?, publicHeadersPath: AbsolutePath) throws -> Self? { - guard let moduleMap else { return nil } - // As per SPM logic, headers should be added only when using the umbrella header without modulemap: - // https://github.com/apple/swift-package-manager/blob/9b9bed7eaf0f38eeccd0d8ca06ae08f6689d1c3f/Sources/Xcodeproj/pbxproj.swift#L588-L609 - switch moduleMap { - case .header, .nestedHeader: - let publicHeaders = try FileHandler.shared.filesAndDirectoriesContained(in: publicHeadersPath)! - .filter { $0.extension == "h" } - let list: [FileListGlob] = publicHeaders.map { .glob(Path($0.pathString)) } - return .headers(public: .list(list)) - case .none, .custom, .directory: - return nil - } - } -} - -extension ProjectDescription.Settings { - // swiftlint:disable:next function_body_length - fileprivate static func from( - target: PackageInfo.Target, - targetDestinations _: ProjectDescription.Destinations, - packageFolder: AbsolutePath, - packageName: String, - packageInfos: [String: PackageInfo], - packageToProject: [String: AbsolutePath], - targetToResolvedDependencies: [String: [PackageInfoMapper.ResolvedDependency]], - settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting], - platforms: [PackageInfo.Platform], - targetToModuleMap: [String: ModuleMap], - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary] - ) throws -> Self? { - let mainPath = try target.basePath(packageFolder: packageFolder) - let mainRelativePath = mainPath.relative(to: packageFolder) - - var dependencyHeaderSearchPaths: [String] = [] - let moduleMap = targetToModuleMap[target.name] - if let moduleMap { - if moduleMap != .none, target.type != .system { - let publicHeadersPath = try target.publicHeadersPath(packageFolder: packageFolder) - let publicHeadersRelativePath = publicHeadersPath.relative(to: packageFolder) - dependencyHeaderSearchPaths.append("$(SRCROOT)/\(publicHeadersRelativePath.pathString)") - } - } - - let allDependencies = Self.recursiveTargetDependencies( - of: target, - packageName: packageName, - packageInfos: packageInfos, - targetToResolvedDependencies: targetToResolvedDependencies - ) - - dependencyHeaderSearchPaths += try allDependencies - .compactMap { dependency in - // Add dependencies search paths if they require a modulemap - guard let packagePath = packageToProject[dependency.package] else { return nil } - let headersPath = try dependency.target.publicHeadersPath(packageFolder: packagePath) - // Not all the targets have module maps. For example, Swift Macro packages do not. - guard let moduleMap = targetToModuleMap[dependency.target.name] else { return nil } - - switch moduleMap { - case .none, .header, .nestedHeader: - return nil - case .directory, .custom: - return "$(SRCROOT)/\(headersPath.relative(to: packageFolder))" - } - } - .sorted() - - var settingsDictionary: TuistGraph.SettingsDictionary = [ - // Xcode settings configured by SPM by default - "ALWAYS_SEARCH_USER_PATHS": "YES", - "CLANG_ENABLE_OBJC_WEAK": "NO", - "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER": "NO", - "ENABLE_STRICT_OBJC_MSGSEND": "NO", - "FRAMEWORK_SEARCH_PATHS": ["$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks"], - "GCC_NO_COMMON_BLOCKS": "NO", - "USE_HEADERMAP": "NO", - // Disable warnings in generated projects - "GCC_WARN_INHIBIT_ALL_WARNINGS": "YES", - "SWIFT_SUPPRESS_WARNINGS": "YES", - ] - - let mapper = SettingsMapper( - headerSearchPaths: dependencyHeaderSearchPaths, - mainRelativePath: mainRelativePath, - settings: settings - ) - - let resolvedSettings = try mapper.settingsForPlatforms(platforms) - - settingsDictionary.merge(resolvedSettings) { $1 } - - if let moduleMapPath = moduleMap?.path { - settingsDictionary["MODULEMAP_FILE"] = .string("$(SRCROOT)/\(moduleMapPath.relative(to: packageFolder))") - } - - var mappedSettingsDictionary = ProjectDescription.SettingsDictionary.from(settingsDictionary: settingsDictionary) - - if let settingsToOverride = targetSettings[target.name] { - let projectDescriptionSettingsToOverride = ProjectDescription.SettingsDictionary - .from(settingsDictionary: settingsToOverride) - mappedSettingsDictionary.merge(projectDescriptionSettingsToOverride) - } - - return .from(settings: baseSettings, adding: mappedSettingsDictionary, packageFolder: packageFolder) - } - - fileprivate struct PackageTarget: Hashable { - let package: String - let target: PackageInfo.Target - } - - fileprivate static func recursiveTargetDependencies( - of target: PackageInfo.Target, - packageName: String, - packageInfos: [String: PackageInfo], - targetToResolvedDependencies: [String: [PackageInfoMapper.ResolvedDependency]] - ) -> Set { - let result = transitiveClosure( - [PackageTarget(package: packageName, target: target)], - successors: { packageTarget in - let resolvedDependencies = targetToResolvedDependencies[packageTarget.target.name] ?? [] - return resolvedDependencies.flatMap { resolvedDependency -> [PackageTarget] in - switch resolvedDependency { - case let .target(name, _): - guard let packageInfo = packageInfos[packageTarget.package], - let target = packageInfo.targets.first(where: { $0.name == name }) - else { - return [] - } - return [PackageTarget(package: packageTarget.package, target: target)] - case let .externalTarget(package, target, _): - guard let packageInfo = packageInfos[package] else { return [] } - return packageInfo.targets - .filter { $0.name == target } - .map { - PackageTarget(package: package, target: $0) - } - case .xcframework: - return [] - } - } - } - ) - return result - } -} - -extension ProjectDescription.PackagePlatform { - fileprivate func destinations() throws -> ProjectDescription.Destinations { - switch self { - case .iOS: - return [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - case .macCatalyst: - return [.macCatalyst] - case .macOS: - return [.mac] - case .tvOS: - return [.appleTv] - case .watchOS: - return [.appleWatch] - case .visionOS: - return [.appleVision] - } - } -} - -extension ProjectDescription.Product { - fileprivate static func from(product: TuistGraph.Product) -> Self { - switch product { - case .app: - return .app - case .staticLibrary: - return .staticLibrary - case .dynamicLibrary: - return .dynamicLibrary - case .framework: - return .framework - case .staticFramework: - return .staticFramework - case .unitTests: - return .unitTests - case .uiTests: - return .uiTests - case .bundle: - return .bundle - case .commandLineTool: - return .commandLineTool - case .appExtension: - return .appExtension - case .watch2App: - return .watch2App - case .watch2Extension: - return .watch2Extension - case .tvTopShelfExtension: - return .tvTopShelfExtension - case .messagesExtension: - return .messagesExtension - case .stickerPackExtension: - return .stickerPackExtension - case .appClip: - return .appClip - case .xpc: - return .xpc - case .systemExtension: - return .systemExtension - case .extensionKitExtension: - return .extensionKitExtension - case .macro: - return .macro - } - } -} - -extension ProjectDescription.SettingsDictionary { - fileprivate static func from(settingsDictionary: TuistGraph.SettingsDictionary) -> Self { - settingsDictionary.mapValues { value in - switch value { - case let .string(stringValue): - return ProjectDescription.SettingValue.string(stringValue) - case let .array(arrayValue): - return ProjectDescription.SettingValue.array(arrayValue) - } - } - } -} - -extension ProjectDescription.Settings { - fileprivate static func from( - settings: TuistGraph.Settings, - adding: ProjectDescription.SettingsDictionary, - packageFolder: AbsolutePath - ) -> Self { - .settings( - base: .from(settingsDictionary: settings.base).merging(adding, uniquingKeysWith: { $1 }), - configurations: settings.configurations - .map { buildConfiguration, configuration in - .from(buildConfiguration: buildConfiguration, configuration: configuration, packageFolder: packageFolder) - } - .sorted { $0.name.rawValue < $1.name.rawValue }, - defaultSettings: .from(defaultSettings: settings.defaultSettings) - ) - } -} - -extension ProjectDescription.Configuration { - fileprivate static func from( - buildConfiguration: BuildConfiguration, - configuration: TuistGraph.Configuration?, - packageFolder: AbsolutePath - ) -> Self { - let name = ConfigurationName(stringLiteral: buildConfiguration.name) - let settings = ProjectDescription.SettingsDictionary.from(settingsDictionary: configuration?.settings ?? [:]) - let xcconfig = configuration?.xcconfig.map { Path($0.relative(to: packageFolder).pathString) } - switch buildConfiguration.variant { - case .debug: - return .debug(name: name, settings: settings, xcconfig: xcconfig) - case .release: - return .release(name: name, settings: settings, xcconfig: xcconfig) - } - } -} - -extension ProjectDescription.DefaultSettings { - fileprivate static func from(defaultSettings: TuistGraph.DefaultSettings) -> Self { - switch defaultSettings { - case let .recommended(excluding): - return .recommended(excluding: excluding) - case let .essential(excluding): - return .essential(excluding: excluding) - case .none: - return .none - } - } -} - -extension PackageInfo { - fileprivate func projectSettings( - swiftToolsVersion: TSCUtility.Version?, - buildConfigs: [BuildConfiguration]? = nil - ) -> ProjectDescription.Settings? { - var settingsDictionary: ProjectDescription.SettingsDictionary = [:] - - if let cLanguageStandard { - settingsDictionary["GCC_C_LANGUAGE_STANDARD"] = .string(cLanguageStandard) - } - - if let cxxLanguageStandard { - settingsDictionary["CLANG_CXX_LANGUAGE_STANDARD"] = .string(cxxLanguageStandard) - } - - if let swiftLanguageVersion = swiftVersion(for: swiftToolsVersion) { - settingsDictionary["SWIFT_VERSION"] = .string(swiftLanguageVersion) - } - - if let buildConfigs { - let configs = buildConfigs - .sorted() - .map { config -> ProjectDescription.Configuration in - switch config.variant { - case .debug: - return ProjectDescription.Configuration.debug(name: .configuration(config.name)) - case .release: - return ProjectDescription.Configuration.release(name: .configuration(config.name)) - } - } - return .settings(base: settingsDictionary, configurations: configs) - } else { - return settingsDictionary.isEmpty ? nil : .settings(base: settingsDictionary) - } - } - - private func swiftVersion(for configuredSwiftVersion: TSCUtility.Version?) -> String? { - /// Take the latest swift version compatible with the configured one - let maxAllowedSwiftLanguageVersion = swiftLanguageVersions? - .filter { - guard let configuredSwiftVersion else { - return true - } - return $0 <= configuredSwiftVersion - } - .sorted() - .last - - return maxAllowedSwiftLanguageVersion?.description - } -} - -extension PackageInfo.Target { - /// The path used as base for all the relative paths of the package (e.g. sources, resources, headers) - func basePath(packageFolder: AbsolutePath) throws -> AbsolutePath { - if let path { - return packageFolder.appending(try RelativePath(validating: path)) - } else { - let firstMatchingPath = PackageInfoMapper.predefinedSourceDirectories - .map { packageFolder.appending(components: [$0, name]) } - .first(where: { FileHandler.shared.exists($0) }) - guard let mainPath = firstMatchingPath else { - throw PackageInfoMapperError.defaultPathNotFound(packageFolder, name) - } - return mainPath - } - } - - func publicHeadersPath(packageFolder: AbsolutePath) throws -> AbsolutePath { - let mainPath = try basePath(packageFolder: packageFolder) - return mainPath.appending(try RelativePath(validating: publicHeadersPath ?? "include")) - } -} - -extension PackageInfoMapper { - public enum ResolvedDependency: Equatable, Hashable { - case target(name: String, condition: ProjectDescription.PlatformCondition? = nil) - case xcframework(path: Path, condition: ProjectDescription.PlatformCondition? = nil) - case externalTarget(package: String, target: String, condition: ProjectDescription.PlatformCondition? = nil) - - public func hash(into hasher: inout Hasher) { - switch self { - case let .target(name, condition): - hasher.combine("target") - hasher.combine(name) - hasher.combine(condition) - case let .xcframework(path, condition): - hasher.combine("package") - hasher.combine(path) - hasher.combine(condition) - case let .externalTarget(package, target, condition): - hasher.combine("externalTarget") - hasher.combine(package) - hasher.combine(target) - hasher.combine(condition) - } - } - - fileprivate var condition: ProjectDescription.PlatformCondition? { - switch self { - case let .target(_, condition): - return condition - case let .xcframework(_, condition): - return condition - case let .externalTarget(_, _, condition): - return condition - } - } - - fileprivate var targetName: String? { - switch self { - case let .target(targetName, _), let .externalTarget(_, targetName, _): - return targetName - case .xcframework: - return nil - } - } - - fileprivate static func from( - dependencies: [PackageInfo.Target.Dependency], - packageInfo: PackageInfo, - packageInfos: [String: PackageInfo], - idToPackage: [String: String], - targetDependencyToFramework: [String: Path] - ) throws -> [ResolvedDependency] { - try dependencies.flatMap { dependency -> [Self] in - switch dependency { - case let .target(name, condition): - return Self.fromTarget( - name: name, - targetDependencyToFramework: targetDependencyToFramework, - condition: condition - ) - case let .product(name, package, _, condition): - return try Self.fromProduct( - package: idToPackage[package.lowercased()] ?? package, - product: name, - packageInfos: packageInfos, - targetDependencyToFramework: targetDependencyToFramework, - condition: condition - ) - case let .byName(name, condition): - if packageInfo.targets.contains(where: { $0.name == name }) { - return Self.fromTarget( - name: name, - targetDependencyToFramework: targetDependencyToFramework, - condition: condition - ) - } else { - guard let packageNameAndInfo = packageInfos - .first(where: { $0.value.products.contains { $0.name == name } }) - else { - throw PackageInfoMapperError.unknownByNameDependency(name) - } - - return try Self.fromProduct( - package: packageNameAndInfo.key, - product: name, - packageInfos: packageInfos, - targetDependencyToFramework: targetDependencyToFramework, - condition: condition - ) - } - } - } - } - - fileprivate static func fromTarget( - name: String, - targetDependencyToFramework: [String: Path], - condition packageConditionDescription: PackageInfo.PackageConditionDescription? - ) -> [Self] { - do { - let condition = try ProjectDescription.PlatformCondition.from(packageConditionDescription) - - if let framework = targetDependencyToFramework[name] { - return [.xcframework(path: framework, condition: condition)] - } else { - return [.target(name: PackageInfoMapper.sanitize(targetName: name), condition: condition)] - } - } catch { - return [] - } - } - - private static func fromProduct( - package: String, - product: String, - packageInfos: [String: PackageInfo], - targetDependencyToFramework: [String: Path], - condition packageConditionDescription: PackageInfo.PackageConditionDescription? - ) throws -> [Self] { - guard let packageProduct = packageInfos[package]?.products.first(where: { $0.name == product }) else { - throw PackageInfoMapperError.unknownProductDependency(product, package) - } - do { - let condition = try ProjectDescription.PlatformCondition.from(packageConditionDescription) - - return packageProduct.targets.map { name in - if let framework = targetDependencyToFramework[name] { - return .xcframework(path: framework, condition: condition) - } else { - return .externalTarget( - package: package, - target: PackageInfoMapper.sanitize(targetName: name), - condition: condition - ) - } - } - } catch { - return [] - } - } - } -} - -extension ProjectDescription.PlatformCondition { - struct OnlyConditionsWithUnsupportedPlatforms: Error {} - - /// Map from a package condition to ProjectDescription.PlatformCondition - /// - Parameter condition: condition representing platforms that a given dependency applies to - /// - Returns: set of PlatformFilters to be used with `GraphDependencyRefrence` - /// throws `OnlyConditionsWithUnsupportedPlatforms` if the condition only contains platforms not supported by Tuist (e.g - /// `windows`) - fileprivate static func from(_ condition: PackageInfo.PackageConditionDescription?) throws -> Self? { - guard let condition else { return nil } - let filters: [ProjectDescription.PlatformFilter] = condition.platformNames.compactMap { name in - switch name { - case "ios": - return .ios - case "maccatalyst": - return .catalyst - case "macos": - return .macos - case "tvos": - return .tvos - case "watchos": - return .watchos - case "visionos": - return .visionos - default: - return nil - } - } - - // If empty, we know there are no supported platforms and this dependency should not be included in the graph - if filters.isEmpty { - throw OnlyConditionsWithUnsupportedPlatforms() - } - - return .when(Set(filters)) - } -} - -extension PackageInfo.Platform { - var tuistPlatformName: String { - // catalyst is mapped to iOS platform in tuist - platformName == "maccatalyst" ? "ios" : platformName - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift deleted file mode 100644 index e3d7bb0dd91..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift +++ /dev/null @@ -1,138 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistGraph -import TuistSupport - -struct SettingsMapper { - init( - headerSearchPaths: [String], - mainRelativePath: RelativePath, - settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting] - ) { - self.headerSearchPaths = headerSearchPaths - self.settings = settings - self.mainRelativePath = mainRelativePath - } - - private let headerSearchPaths: [String] - private let mainRelativePath: RelativePath - private let settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting] - - // `nil` means settings without a condition - private func settingsForPlatform(_ platformName: String?) throws - -> [PackageInfo.Target.TargetBuildSettingDescription.Setting] - { - settings.filter { setting in - if let platformName, setting.hasConditions { - return setting.condition?.platformNames.contains(platformName) == true - } else { - return !setting.hasConditions - } - } - } - - // swiftlint:disable:next function_body_length - func settingsDictionaryForPlatform(_ platform: PackageInfo.Platform?) throws -> TuistGraph.SettingsDictionary { - var headerSearchPaths = headerSearchPaths - var defines = ["SWIFT_PACKAGE": "1"] - var swiftDefines = "SWIFT_PACKAGE" - var cFlags: [String] = [] - var cxxFlags: [String] = [] - var swiftFlags: [String] = [] - var linkerFlags: [String] = [] - - var settingsDictionary = TuistGraph.SettingsDictionary() - let platformSettings = try settingsForPlatform(platform?.platformName) - - for setting in platformSettings { - switch (setting.tool, setting.name) { - case (.c, .headerSearchPath), (.cxx, .headerSearchPath): - headerSearchPaths.append("$(SRCROOT)/\(mainRelativePath.pathString)/\(setting.value[0])") - case (.c, .define), (.cxx, .define): - let (name, value) = setting.extractDefine - defines[name] = value - case (.c, .unsafeFlags): - cFlags.append(contentsOf: setting.value) - case (.cxx, .unsafeFlags): - cxxFlags.append(contentsOf: setting.value) - case (.swift, .define): - swiftDefines.append(" \(setting.value[0])") - case (.swift, .unsafeFlags): - swiftFlags.append(contentsOf: setting.value) - case (.swift, .enableUpcomingFeature): - swiftFlags.append("-enable-upcoming-feature \(setting.value[0])") - case (.swift, .enableExperimentalFeature): - swiftFlags.append("-enable-experimental-feature \(setting.value[0])") - case (.linker, .unsafeFlags): - linkerFlags.append(contentsOf: setting.value) - case (.linker, .linkedFramework), (.linker, .linkedLibrary): - // Handled as dependency - continue - - case (.c, .linkedFramework), (.c, .linkedLibrary), (.cxx, .linkedFramework), (.cxx, .linkedLibrary), - (.swift, .headerSearchPath), (.swift, .linkedFramework), (.swift, .linkedLibrary), - (.linker, .headerSearchPath), (.linker, .define), (_, .enableUpcomingFeature), - (_, .enableExperimentalFeature): - throw PackageInfoMapperError.unsupportedSetting(setting.tool, setting.name) - } - } - - if !headerSearchPaths.isEmpty { - settingsDictionary["HEADER_SEARCH_PATHS"] = .array(["$(inherited)"] + headerSearchPaths.map { $0 }) - } - - if !defines.isEmpty { - let sortedDefines = defines.sorted { $0.key < $1.key } - settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] = .array(["$(inherited)"] + sortedDefines - .map { key, value in - "\(key)=\(value.spm_shellEscaped())" - }) - } - - if !swiftDefines.isEmpty { - settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = "$(inherited) \(swiftDefines)" - } - - if !cFlags.isEmpty { - settingsDictionary["OTHER_CFLAGS"] = .array(["$(inherited)"] + cFlags) - } - - if !cxxFlags.isEmpty { - settingsDictionary["OTHER_CPLUSPLUSFLAGS"] = .array(["$(inherited)"] + cxxFlags) - } - - if !swiftFlags.isEmpty { - settingsDictionary["OTHER_SWIFT_FLAGS"] = .array(["$(inherited)"] + swiftFlags) - } - - if !linkerFlags.isEmpty { - settingsDictionary["OTHER_LDFLAGS"] = .array(["$(inherited)"] + linkerFlags) - } - - return settingsDictionary - } - - func settingsForPlatforms(_ platforms: [PackageInfo.Platform]) throws -> TuistGraph.SettingsDictionary { - var resolvedSettings = try settingsDictionaryForPlatform(nil) - - for platform in platforms.sorted(by: { $0.platformName < $1.platformName }) { - let platformSettings = try settingsDictionaryForPlatform(platform) - resolvedSettings.overlay(with: platformSettings, for: try platform.graphPlatform()) - } - - return resolvedSettings - } -} - -extension PackageInfo.Target.TargetBuildSettingDescription.Setting { - fileprivate var extractDefine: (name: String, value: String) { - let define = value[0] - if define.contains("=") { - let split = define.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) - return (name: String(split[0]), value: String(split[1])) - } else { - return (name: define, value: "1") - } - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/SwiftPackageManagerGraphGenerator.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/SwiftPackageManagerGraphGenerator.swift deleted file mode 100644 index 22f03e3577d..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/SwiftPackageManagerGraphGenerator.swift +++ /dev/null @@ -1,205 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport - -// MARK: - Swift Package Manager Graph Generator Errors - -enum SwiftPackageManagerGraphGeneratorError: FatalError, Equatable { - /// Thrown when `SwiftPackageManagerWorkspaceState.Dependency.Kind` is not one of the expected values. - case unsupportedDependencyKind(String) - /// Thrown when `SwiftPackageManagerWorkspaceState.packageRef.path` is not present in a local swift package. - case missingPathInLocalSwiftPackage(String) - - /// Error type. - var type: ErrorType { - switch self { - case .unsupportedDependencyKind, .missingPathInLocalSwiftPackage: - return .bug - } - } - - /// Error description. - var description: String { - switch self { - case let .unsupportedDependencyKind(name): - return "The dependency kind \(name) is not supported." - case let .missingPathInLocalSwiftPackage(name): - return "The local package \(name) does not contain the path in the generated `workspace-state.json` file." - } - } -} - -// MARK: - Swift Package Manager Graph Generator - -/// A protocol that defines an interface to generate the `DependenciesGraph` for the `SwiftPackageManager` dependencies. -public protocol SwiftPackageManagerGraphGenerating { - /// Generates the `DependenciesGraph` for the `SwiftPackageManager` dependencies. - /// - Parameter path: The path to the directory that contains the `checkouts` directory where `SwiftPackageManager` installed - /// dependencies. - /// - Parameter productTypes: The custom `Product` types to be used for SPM targets. - /// - Parameter platforms: The supported platforms. - /// - Parameter baseSettings: base `Settings` for targets. - /// - Parameter targetSettings: `SettingsDictionary` overrides for targets. - /// - Parameter swiftToolsVersion: The version of Swift tools that will be used to generate dependencies. - /// - Parameter projectOptions: The custom configurations for generated projects. - func generate( - at path: AbsolutePath, - productTypes: [String: TuistGraph.Product], - platforms: Set, - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary], - swiftToolsVersion: TSCUtility.Version?, - projectOptions: [String: TuistGraph.Project.Options] - ) throws -> TuistCore.DependenciesGraph -} - -public final class SwiftPackageManagerGraphGenerator: SwiftPackageManagerGraphGenerating { - private let swiftPackageManagerController: SwiftPackageManagerControlling - private let packageInfoMapper: PackageInfoMapping - - public init( - swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController(), - packageInfoMapper: PackageInfoMapping = PackageInfoMapper() - ) { - self.swiftPackageManagerController = swiftPackageManagerController - self.packageInfoMapper = packageInfoMapper - } - - // swiftlint:disable:next function_body_length - public func generate( - at path: AbsolutePath, - productTypes: [String: TuistGraph.Product], - platforms: Set, - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary], - swiftToolsVersion: TSCUtility.Version?, - projectOptions: [String: TuistGraph.Project.Options] - ) throws -> TuistCore.DependenciesGraph { - let checkoutsFolder = path.appending(component: "checkouts") - let workspacePath = path.appending(component: "workspace-state.json") - - let workspaceState = try JSONDecoder() - .decode(SwiftPackageManagerWorkspaceState.self, from: try FileHandler.shared.readFile(workspacePath)) - let packageInfos: [ - // swiftlint:disable:next large_tuple - (id: String, name: String, folder: AbsolutePath, targetToArtifactPaths: [String: AbsolutePath], info: PackageInfo) - ] - packageInfos = try workspaceState.object.dependencies.map(context: .concurrent) { dependency in - let name = dependency.packageRef.name - let packageFolder: AbsolutePath - switch dependency.packageRef.kind { - case "remote", "remoteSourceControl": - packageFolder = checkoutsFolder.appending(component: dependency.subpath) - case "local", "fileSystem", "localSourceControl": - // Depending on the swift version, the information is available either in `path` or in `location` - guard let path = dependency.packageRef.path ?? dependency.packageRef.location else { - throw SwiftPackageManagerGraphGeneratorError.missingPathInLocalSwiftPackage(name) - } - packageFolder = try AbsolutePath(validating: path) - case "registry": - let registryFolder = path.appending(try RelativePath(validating: "registry/downloads")) - packageFolder = registryFolder.appending(try RelativePath(validating: dependency.subpath)) - default: - throw SwiftPackageManagerGraphGeneratorError.unsupportedDependencyKind(dependency.packageRef.kind) - } - - let packageInfo = try swiftPackageManagerController.loadPackageInfo(at: packageFolder) - let targetToArtifactPaths = try workspaceState.object.artifacts - .filter { $0.packageRef.identity == dependency.packageRef.identity } - .reduce(into: [:]) { result, artifact in - result[artifact.targetName] = try AbsolutePath(validating: artifact.path) - } - - return ( - id: dependency.packageRef.identity.lowercased(), - name: name, - folder: packageFolder, - targetToArtifactPaths: targetToArtifactPaths, - info: packageInfo - ) - } - - let idToPackage: [String: String] = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.id, $0.name) }) - let packageToProject = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.name, $0.folder) }) - let packageInfoDictionary = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.name, $0.info) }) - let packageToFolder = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.name, $0.folder) }) - let packageToTargetsToArtifactPaths = Dictionary(uniqueKeysWithValues: packageInfos.map { - ($0.name, $0.targetToArtifactPaths) - }) - - let preprocessInfo = try packageInfoMapper.preprocess( - packageInfos: packageInfoDictionary, - idToPackage: idToPackage, - packageToFolder: packageToFolder, - packageToTargetsToArtifactPaths: packageToTargetsToArtifactPaths - ) - - let destinations: ProjectDescription.Destinations = Set(platforms.flatMap { platform -> ProjectDescription.Destinations in - switch platform { - case .iOS: - [.iPhone, .iPad, .appleVisionWithiPadDesign, .macWithiPadDesign] - case .macCatalyst: - [.macCatalyst] - case .macOS: - [.mac] - case .tvOS: - [.appleTv] - case .watchOS: - [.appleWatch] - case .visionOS: - [.appleVision] - } - }) - - let externalProjects: [Path: ProjectDescription.Project] = try packageInfos.reduce(into: [:]) { result, packageInfo in - let manifest = try packageInfoMapper.map( - packageInfo: packageInfo.info, - packageInfos: packageInfoDictionary, - name: packageInfo.name, - path: packageInfo.folder, - productTypes: productTypes, - baseSettings: baseSettings, - targetSettings: targetSettings, - projectOptions: projectOptions[packageInfo.name], - minDeploymentTargets: preprocessInfo.platformToMinDeploymentTarget, - destinations: destinations, - targetToProducts: preprocessInfo.targetToProducts, - targetToResolvedDependencies: preprocessInfo.targetToResolvedDependencies, - macroDependencies: preprocessInfo.macroDependencies, - targetToModuleMap: preprocessInfo.targetToModuleMap, - packageToProject: packageToProject, - swiftToolsVersion: swiftToolsVersion - ) - result[Path(packageInfo.folder.pathString)] = manifest - } - - return DependenciesGraph( - externalDependencies: preprocessInfo.productToExternalDependencies, - externalProjects: externalProjects - ) - } -} - -extension ProjectDescription.Platform { - /// Maps a TuistGraph.Platform instance into a ProjectDescription.Platform instance. - /// - Parameters: - /// - graph: Graph representation of platform model. - static func from(graph: TuistGraph.Platform) -> ProjectDescription.Platform { - switch graph { - case .macOS: - return .macOS - case .iOS: - return .iOS - case .tvOS: - return .tvOS - case .watchOS: - return .watchOS - case .visionOS: - return .visionOS - } - } -} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/SwiftPackageManagerModuleMapGenerator.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/SwiftPackageManagerModuleMapGenerator.swift deleted file mode 100644 index 21dcfa9deff..00000000000 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/SwiftPackageManagerModuleMapGenerator.swift +++ /dev/null @@ -1,96 +0,0 @@ -import TSCBasic -import TuistSupport - -/// The type of modulemap file -public enum ModuleMap: Equatable { - /// No headers and hence no modulemap file - case none - /// Custom modulemap file provided in SPM package - case custom(AbsolutePath) - /// Umbrella header provided in SPM package - case header - /// Nested umbrella header provided in SPM package - case nestedHeader - /// No umbrella header provided in SPM package, define umbrella directory - case directory(AbsolutePath) - - var path: AbsolutePath? { - switch self { - case let .custom(path), let .directory(path): - return path - case .none, .header, .nestedHeader: - return nil - } - } - - /// Name of the module map file recognized by the Clang and Swift compilers. - public static let filename = "module.modulemap" -} - -/// Protocol that allows to generate a modulemap for an SPM target. -/// It implements the Swift Package Manager logic -/// [documented here](https://github.com/apple/swift-package-manager/blob/main/Documentation/Usage.md#creating-c-language-targets) -/// and -/// [implemented here](https://github.com/apple/swift-package-manager/blob/main/Sources/PackageLoading/ModuleMapGenerator.swift). -public protocol SwiftPackageManagerModuleMapGenerating { - func generate(moduleName: String, publicHeadersPath: AbsolutePath) throws -> ModuleMap -} - -public final class SwiftPackageManagerModuleMapGenerator: SwiftPackageManagerModuleMapGenerating { - public init() {} - - public func generate( - moduleName: String, - publicHeadersPath: AbsolutePath - ) throws -> ModuleMap { - let umbrellaHeaderPath = publicHeadersPath.appending(component: moduleName + ".h") - let nestedUmbrellaHeaderPath = publicHeadersPath.appending(component: moduleName).appending(component: moduleName + ".h") - - if let customModuleMapPath = try Self.customModuleMapPath(publicHeadersPath: publicHeadersPath) { - // User defined modulemap exists, use it - return .custom(customModuleMapPath) - } else if FileHandler.shared.exists(umbrellaHeaderPath) { - // If 'PublicHeadersDir/ModuleName.h' exists, then use it as the umbrella header. - // When umbrella header is present, no need to define a modulemap as it is generated by Xcode - return .header - } else if FileHandler.shared.exists(nestedUmbrellaHeaderPath) { - // If 'PublicHeadersDir/ModuleName/ModuleName.h' exists, then use it as the umbrella header. - return .nestedHeader - } else if FileHandler.shared.exists(publicHeadersPath) { - // Otherwise, consider the public headers folder as umbrella directory - let sanitizedModuleName = moduleName.replacingOccurrences(of: "-", with: "_") - let generatedModuleMapContent = - """ - module \(sanitizedModuleName) { - umbrella "\(publicHeadersPath.pathString)" - export * - } - - """ - let generatedModuleMapPath = publicHeadersPath.appending(component: "\(moduleName).modulemap") - try FileHandler.shared.write(generatedModuleMapContent, path: generatedModuleMapPath, atomically: true) - return .directory(generatedModuleMapPath) - } else { - return .none - } - } - - static func customModuleMapPath(publicHeadersPath: AbsolutePath) throws -> AbsolutePath? { - guard FileHandler.shared.exists(publicHeadersPath) else { return nil } - - let moduleMapPath = try RelativePath(validating: ModuleMap.filename) - let publicHeadersFolderContent = try FileHandler.shared.contentsOfDirectory(publicHeadersPath) - - if publicHeadersFolderContent.contains(publicHeadersPath.appending(moduleMapPath)) { - return publicHeadersPath.appending(moduleMapPath) - } else if publicHeadersFolderContent.count == 1, - let nestedHeadersPath = publicHeadersFolderContent.first, - FileHandler.shared.isFolder(nestedHeadersPath), - FileHandler.shared.exists(nestedHeadersPath.appending(moduleMapPath)) - { - return nestedHeadersPath.appending(moduleMapPath) - } else { - return nil - } - } -} diff --git a/Sources/TuistDependenciesTesting/Carthage/MockCarthageInteractor.swift b/Sources/TuistDependenciesTesting/Carthage/MockCarthageInteractor.swift deleted file mode 100644 index 1e5e93d2db0..00000000000 --- a/Sources/TuistDependenciesTesting/Carthage/MockCarthageInteractor.swift +++ /dev/null @@ -1,35 +0,0 @@ -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistGraphTesting - -@testable import TuistDependencies - -public final class MockCarthageInteractor: CarthageInteracting { - public init() {} - - var invokedInstall = false - var installStub: ( - (AbsolutePath, TuistGraph.CarthageDependencies, Set, Bool) throws -> TuistCore - .DependenciesGraph - )? - - public func install( - dependenciesDirectory: AbsolutePath, - dependencies: TuistGraph.CarthageDependencies, - platforms: Set, - shouldUpdate: Bool - ) throws -> TuistCore.DependenciesGraph { - invokedInstall = true - return try installStub?(dependenciesDirectory, dependencies, platforms, shouldUpdate) ?? .test() - } - - var invokedClean = false - var cleanStub: ((AbsolutePath) throws -> Void)? - - public func clean(dependenciesDirectory: AbsolutePath) throws { - invokedClean = true - try cleanStub?(dependenciesDirectory) - } -} diff --git a/Sources/TuistDependenciesTesting/Carthage/Models/CarthageVersionFile+TestData.swift b/Sources/TuistDependenciesTesting/Carthage/Models/CarthageVersionFile+TestData.swift deleted file mode 100644 index d92c938c6b2..00000000000 --- a/Sources/TuistDependenciesTesting/Carthage/Models/CarthageVersionFile+TestData.swift +++ /dev/null @@ -1,655 +0,0 @@ -import Foundation -import TSCBasic - -@testable import TuistDependencies - -extension CarthageVersionFile { - static func test( - iOS: [Product] = [], - macOS: [Product] = [], - watchOS: [Product] = [], - tvOS: [Product] = [], - visionOS: [Product] = [] - ) -> Self { - .init( - iOS: iOS, - macOS: macOS, - watchOS: watchOS, - tvOS: tvOS, - visionOS: visionOS - ) - } - - static var testAlamofire: Self { - .init( - iOS: [ - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - ], - macOS: [ - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - ], - watchOS: [ - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - ], - tvOS: [ - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - .init( - name: "Alamofire", - container: "Alamofire.xcframework" - ), - ], - visionOS: nil - ) - } - - static var testRxSwift: Self { - .init( - iOS: [ - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - .init( - name: "RxTest", - container: "RxTest.xcframework" - ), - .init( - name: "RxTest", - container: "RxTest.xcframework" - ), - ], - macOS: [ - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - .init( - name: "RxTest", - container: "RxTest.xcframework" - ), - ], - watchOS: [ - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - ], - tvOS: [ - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxBlocking", - container: "RxBlocking.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxCocoa", - container: "RxCocoa.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxRelay", - container: "RxRelay.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - .init( - name: "RxSwift", - container: "RxSwift.xcframework" - ), - .init( - name: "RxTest", - container: "RxTest.xcframework" - ), - ], - visionOS: nil - ) - } - - static var testRealmCocoa: Self { - .init( - iOS: [ - .init( - name: "Realm", - container: "Realm.xcframework" - ), - .init( - name: "Realm", - container: "Realm.xcframework" - ), - .init( - name: "RealmSwift", - container: "RealmSwift.xcframework" - ), - .init( - name: "RealmSwift", - container: "RealmSwift.xcframework" - ), - ], - macOS: nil, - watchOS: nil, - tvOS: nil, - visionOS: nil - ) - } - - static var testAhoyRTC: Self { - .init( - iOS: [ - .init( - name: "AhoyKit", - container: nil - ), - .init( - name: "WebRTC", - container: nil - ), - ], - macOS: [], - watchOS: [], - tvOS: [], - visionOS: [] - ) - } -} - -extension CarthageVersionFile { - /// A snapshot of `.Alamofire.version` file - /// that was generated by `Carthage` in` `0.37.0` version - /// using `carthage bootstrap --platform iOS,macOS,tvOS,watchOS --use-xcframeworks --no-use-binaries --use-netrc - /// --cache-builds --new-resolver` command - static var testAlamofireJson: String { - """ - { - "Mac" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "57f5c800334d5f7a1c46285e1e00fd9e26abaf836dbcec92578b69403dd69596", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "macos-arm64_x86_64" - } - ], - "watchOS" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "ce13aaa785ffa2c3c16ba88b8ab54e97bac5ba0a41a5ac22d9552a84100b07dc", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "watchos-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "54293baccc33dc9f91018a4ec9253f3b17faa0c62fe3eef973835a76bc1357c9", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "watchos-arm64_32_armv7k" - } - ], - "tvOS" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "bf2734287d14a558d4b739727ebe5f9f9a1f6ed2aeb0c5781b633b8bcac37d70", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "tvos-arm64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "0494fd475a6c62575d810bf50c8c3d09a5b3e5cc192d6f88005e45ff718bf503", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "tvos-arm64_x86_64-simulator" - } - ], - "commitish" : "5.4.3", - "iOS" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "5fbbffeccfee11c3d48840b59111c9483f985e01a53109e920cf60a79df743cb", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "ios-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "615afccd6819b4d613bf80375d08a39b15beea9e00698b8f3a83d35fb4e7be1c", - "name" : "Alamofire", - "container" : "Alamofire.xcframework", - "identifier" : "ios-arm64_armv7" - } - ] - } - """ - } - - /// A snapshot of `.RxSwift.version` file - /// that was generated by `Carthage` in` `0.37.0` version - /// using `carthage bootstrap --platform iOS,macOS,tvOS,watchOS --use-xcframeworks --no-use-binaries --use-netrc - /// --cache-builds --new-resolver` command - static var testRxSwiftJson: String { - """ - { - "Mac" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "86b1f3a3476db7b35180336876c2731a49b546899d647ab99d52909a6635c883", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "macos-arm64_x86_64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "f7b73b8a44fd2992b330e6c9d044be3873aa9195cb98990dd041abc86622b359", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "macos-arm64_x86_64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "55ce7c5d0f4fe9df7a609d23174dd4ae62a66333f30051d880285f58967ef415", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "macos-arm64_x86_64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "a9ff6e3d6213ea912c3136173af678bd4bb1840057ce88c0b451b30962ccb0bd", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "macos-arm64_x86_64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "3a81b7ea565c01a2663cb3d970bc8411c13f43c092bdb515432d49fc12ea3c72", - "name" : "RxTest", - "container" : "RxTest.xcframework", - "identifier" : "macos-arm64_x86_64" - } - ], - "watchOS" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "fa387e94a430ae1a2185b00d2e71d7c837adf11646ba036fa0800b08ed3db154", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "watchos-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "c6c6e1483df5fa04a8192c1bd4a3eb9f8d2db46d4b77659aa05e0102548c072d", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "watchos-arm64_32_armv7k" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "e614a6a4c2cfb6547753381d3638302e8955ad21893cb5d2f6e07b46946dbe36", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "watchos-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "69ffb2f2502a30e7b4bcd5258fd39b97fdc9c81a2e9e49d8770703dd4c07e0ee", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "watchos-arm64_32_armv7k" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "79091465303a53417f13caa34c6f5c16713d1888f2c7620a2790574d772bc6c2", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "watchos-arm64_32_armv7k" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "d9c5f13754933b994beaded1c5ff0edde45efb86ca13aebc08bae7e567727c18", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "watchos-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "2cb6d5c3c02b778610b0ce1d8af2ff5f69fca80cf2c6382da4f14fb936735689", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "watchos-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "f65a096e38c6940c1b1fdbc5c24f11e8d8af9cafb219723b1e4e2041da6c81c0", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "watchos-arm64_32_armv7k" - } - ], - "tvOS" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "acc2a71bb7bc9c27a0ec95385f048a0040d88bab44deddcb9a1a3b61320c4e6f", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "tvos-arm64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "12c36aea3712976f34d25e0337ba8f6393b76ecfc3e1351a2ab3023c99018b33", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "tvos-arm64_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "4420a6279e55e2c5c3f4b6aa7567eda56aa81a162315b028d6ac7b5689266ef3", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "tvos-arm64_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "a4f7428701da909fb88bdcda602f1a9d1526de20914702dc1519565aa41135eb", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "tvos-arm64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "c312b7732b52838109e93404db33a8693a5f86384c6de054d7878bd98f64f780", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "tvos-arm64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "b73a59d4a73bfdffb3c382588ee48fc74a3d0c5f4fe87242a0588a597ac289d0", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "tvos-arm64_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "e4d95e41cdc6a5e624f1dfc649935a93e6f9eb6a979ace621a8afbd4e9ea6389", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "tvos-arm64_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "9665ba98bde33d0fb8e77d0f70a2950421104b555942375d15515d6c63585eac", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "tvos-arm64" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "6e281b30953f1c8db38d9f1652926e3466188a633cd16bf74339715d931759ec", - "name" : "RxTest", - "container" : "RxTest.xcframework", - "identifier" : "tvos-arm64_x86_64-simulator" - } - ], - "commitish" : "6.2.0", - "iOS" : [ - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "5cb314834422a56915d9404d12e072600665eeba5815b89ca547032eaa7b372e", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "ios-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "4ed2e9c1871c5338a481fa0b73cf7a71e92ded5f7477292e116742c543431101", - "name" : "RxBlocking", - "container" : "RxBlocking.xcframework", - "identifier" : "ios-arm64_armv7" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "5c1719d1c61658eddba8809440b809fb23ab64e24f196db24797627683fd5485", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "ios-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "c3492a3348d7a20396c185dbee479fa19f601b77f0f627608ff67cc029e06e3c", - "name" : "RxCocoa", - "container" : "RxCocoa.xcframework", - "identifier" : "ios-arm64_armv7" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "3725a226c968c7331363377e4e4e2d8b218ae27391ea815263c840e5a66da76a", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "ios-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "4dbc43707ed1bde34abec38f4cf1e903604a20dcd4937130e27922ad6f98caac", - "name" : "RxRelay", - "container" : "RxRelay.xcframework", - "identifier" : "ios-arm64_armv7" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "d2e8ba83ea1e99dc8a22fcc94650e6d8f915293fd811ef1d9a34a3ccb84d4d93", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "ios-arm64_i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "0c2f64086afd5835576d820cd9671b42659a7612afb42d44149757b93d39119c", - "name" : "RxSwift", - "container" : "RxSwift.xcframework", - "identifier" : "ios-arm64_armv7" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "8a2f2e875b174a68c1330791d12e65119f912ad660fd08c14c831a9e6ecd7cfb", - "name" : "RxTest", - "container" : "RxTest.xcframework", - "identifier" : "ios-arm64_armv7" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "6b863d8e43e0831195b03fc0bcb8f84d7d652b0565966f9a890839c45365bf61", - "name" : "RxTest", - "container" : "RxTest.xcframework", - "identifier" : "ios-arm64_i386_x86_64-simulator" - } - ] - } - """ - } - - /// A snapshot of `.realm-cocoa.version` file - /// that was generated by `Carthage` in` `0.37.0` version - /// using `carthage bootstrap --platform iOS --use-xcframeworks --no-use-binaries --use-netrc --cache-builds --new-resolver` - /// command - static var testRealmCocoaJson: String { - """ - { - "commitish" : "v10.7.6", - "iOS" : [ - { - "hash" : "acf910bcb59a82ea4d5c5ecd8358ed4c8438ec4052374e36421bf2c7863a7c51", - "name" : "Realm", - "container" : "Realm.xcframework", - "identifier" : "ios-i386_x86_64-simulator" - }, - { - "hash" : "eca5d0e7fd94e459b73c2d80d35dbbb198cdc460bb656d5912a34e04c4dad45d", - "name" : "Realm", - "container" : "Realm.xcframework", - "identifier" : "ios-arm64_armv7" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "31c09fb27b44ed77915be0cab1e9364570d806f0cf2b7f962c94488c51f20d29", - "name" : "RealmSwift", - "container" : "RealmSwift.xcframework", - "identifier" : "ios-i386_x86_64-simulator" - }, - { - "swiftToolchainVersion" : "5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)", - "hash" : "8b749ef640129c1c56aac21d71c1358ddfc2e27d465e500cce048c1d19131425", - "name" : "RealmSwift", - "container" : "RealmSwift.xcframework", - "identifier" : "ios-arm64_armv7" - } - ] - } - """ - } - - /// A snapshot of `.CarthageAhoyRTC-bitcode.version` file - /// that was generated by `Carthage` in` `0.37.0` version - /// using `carthage bootstrap --platform iOS --use-xcframeworks --no-use-binaries --use-netrc --cache-builds --new-resolver` - /// command - static var testAhoyRTCJson: String { - """ - { - "Mac" : [ - - ], - "watchOS" : [ - - ], - "tvOS" : [ - - ], - "commitish" : "2.1", - "iOS" : [ - { - "name" : "AhoyKit", - "hash" : "c963ec94999f3fe64f75880ba394338d5c694a5cec8f756bc35481f3b8c8b4d2", - "linking" : "dynamic" - }, - { - "name" : "WebRTC", - "hash" : "3a9ced64f6f8ccca46dc0038bdbf3efd8cf98f73cbc29ee1b00d98757b7fab33", - "linking" : "dynamic" - } - ], - "visionOS": [ - - ] - } - """ - } -} - -extension CarthageVersionFile.Product { - static func test( - name: String = "", - container: String? = "" - ) -> Self { - .init( - name: name, - container: container - ) - } -} diff --git a/Sources/TuistDependenciesTesting/Carthage/Utils/MockCarthageController.swift b/Sources/TuistDependenciesTesting/Carthage/Utils/MockCarthageController.swift deleted file mode 100644 index 9c8c03831d5..00000000000 --- a/Sources/TuistDependenciesTesting/Carthage/Utils/MockCarthageController.swift +++ /dev/null @@ -1,57 +0,0 @@ -import TSCBasic -import TSCUtility -import TuistGraph - -@testable import TuistDependencies - -public final class MockCarthageController: CarthageControlling { - public init() {} - - var invokedCanUseSystemCarthage = false - var canUseSystemCarthageStub: (() -> Bool)? - - public func canUseSystemCarthage() -> Bool { - invokedCanUseSystemCarthage = true - return canUseSystemCarthageStub?() ?? false - } - - var invokedCarthageVersion = false - var carthageVersionStub: (() throws -> Version)? - - public func carthageVersion() throws -> Version { - invokedCarthageVersion = true - return try carthageVersionStub?() ?? Version(0, 0, 0) - } - - var invokedIsXCFrameworksProductionSupported = false - var isXCFrameworksProductionSupportedStub: (() -> Bool)? - - public func isXCFrameworksProductionSupported() throws -> Bool { - invokedIsXCFrameworksProductionSupported = true - return isXCFrameworksProductionSupportedStub?() ?? false - } - - var invokedBootstrap = false - var bootstrapStub: ((AbsolutePath, Set, Bool) throws -> Void)? - - public func bootstrap( - at path: AbsolutePath, - platforms: Set, - printOutput: Bool - ) throws { - invokedBootstrap = true - try bootstrapStub?(path, platforms, printOutput) - } - - var invokedUpdate = false - var updateStub: ((AbsolutePath, Set, Bool) throws -> Void)? - - public func update( - at path: AbsolutePath, - platforms: Set, - printOutput: Bool - ) throws { - invokedUpdate = true - try updateStub?(path, platforms, printOutput) - } -} diff --git a/Sources/TuistDependenciesTesting/Carthage/Utils/MockCarthageGraphGenerator.swift b/Sources/TuistDependenciesTesting/Carthage/Utils/MockCarthageGraphGenerator.swift deleted file mode 100644 index ca3d4ca0454..00000000000 --- a/Sources/TuistDependenciesTesting/Carthage/Utils/MockCarthageGraphGenerator.swift +++ /dev/null @@ -1,18 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore - -@testable import TuistDependencies - -public final class MockCarthageGraphGenerator: CarthageGraphGenerating { - public init() {} - - var invokedGenerate = false - var generateStub: ((AbsolutePath) throws -> TuistCore.DependenciesGraph)? - - public func generate(at path: AbsolutePath) throws -> TuistCore.DependenciesGraph { - invokedGenerate = true - return try generateStub?(path) ?? .test() - } -} diff --git a/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift b/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift deleted file mode 100644 index 12f57f38e36..00000000000 --- a/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift +++ /dev/null @@ -1,745 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistDependencies -import TuistSupport -import TuistSupportTesting - -extension TuistCore.DependenciesGraph { - /// A snapshot of `graph.json` file. - public static var testJson: String { - """ - { - "externalDependencies" : { - "RxSwift" : [ - { - "kind" : "xcframework", - "path" : "/Tuist/Dependencies/Carthage/RxSwift.xcframework" - } - ] - }, - "externalProjects": [] - } - """ - } - - public static func test( - externalDependencies: [String: [TargetDependency]] = [:], - externalProjects: [Path: Project] = [:] - ) -> Self { - .init(externalDependencies: externalDependencies, externalProjects: externalProjects) - } - - public static func testXCFramework( - name: String = "Test", - // swiftlint:disable:next force_try - path: Path = Path(AbsolutePath.root.appending(try! RelativePath(validating: "Test.xcframework")).pathString) - ) -> Self { - let externalDependencies = [name: [TargetDependency.xcframework(path: path)]] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - // swiftlint:disable:next function_body_length - public static func test( - spmFolder: Path, - packageFolder: Path, - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign], - fileHandler: FileHandler - ) throws -> Self { - try fileHandler.createFolder(try AbsolutePath(validating: "\(packageFolder.pathString)/customPath/resources")) - - let externalDependencies: [String: [TargetDependency]] = [ - "Tuist": [ - .project( - target: "Tuist", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "Tuist", - destinations: destinations, - product: .staticFramework, - productName: "Tuist", - bundleId: "Tuist", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - .glob( - "\(packageFolder.pathString)/customPath/customSources/**", - excluding: "\(packageFolder.pathString)/customPath/excluded/sources/**" - ), - ], - resources: [ - .folderReference(path: "\(packageFolder.pathString)/customPath/resources", tags: []), - ], - dependencies: [ - .target(name: "TuistKit"), - .project( - target: "ALibrary", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "ADependency"), - condition: .when([.ios]) - ), - .project( - target: "ALibraryUtils", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "ADependency"), - condition: .when([.ios]) - ), - .sdk(name: "WatchKit", type: .framework, status: .required, condition: .when([.watchos])), - ], - settings: Self.spmSettings(with: [ - "HEADER_SEARCH_PATHS": [ - "$(SRCROOT)/customPath/cSearchPath", - "$(SRCROOT)/customPath/cxxSearchPath", - ], - "OTHER_CFLAGS": ["CUSTOM_C_FLAG"], - "OTHER_CPLUSPLUSFLAGS": ["CUSTOM_CXX_FLAG"], - "OTHER_SWIFT_FLAGS": ["CUSTOM_SWIFT_FLAG1", "CUSTOM_SWIFT_FLAG2"], - "GCC_PREPROCESSOR_DEFINITIONS": ["CXX_DEFINE=CXX_VALUE", "C_DEFINE=C_VALUE"], - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "SWIFT_DEFINE", - ]) - ), - .init( - name: "TuistKit", - destinations: destinations, - product: .staticFramework, - productName: "TuistKit", - bundleId: "TuistKit", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/TuistKit/**", - ], - dependencies: [ - .project( - target: "AnotherLibrary", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "another-dependency") - ), - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "test", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings( - base: [ - "GCC_C_LANGUAGE_STANDARD": "c99", - ], - configurations: [ - .debug(name: .debug), - .release(name: .release), - ] - ), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } - - // swiftlint:disable:next function_body_length - public static func aDependency( - spmFolder: Path, - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - ) -> Self { - let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "ADependency") - - let externalDependencies: [String: [TargetDependency]] = [ - "ALibrary": [ - .project( - target: "ALibrary", - path: packageFolder - ), - .project( - target: "ALibraryUtils", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "ALibrary", - destinations: destinations, - product: .staticFramework, - productName: "ALibrary", - bundleId: "ALibrary", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/ALibrary/**", - ], - dependencies: [ - .target( - name: "ALibraryUtils" - ), - ], - settings: Self.spmSettings() - ), - .init( - name: "ALibraryUtils", - destinations: destinations, - product: .staticFramework, - productName: "ALibraryUtils", - bundleId: "ALibraryUtils", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/ALibraryUtils/**", - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "a-dependency", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings( - configurations: [ - .debug(name: .debug), - .release(name: .release), - ] - ), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } - - public static func anotherDependency( - spmFolder: Path, - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - ) -> Self { - let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "another-dependency") - - let externalDependencies: [String: [TargetDependency]] = [ - "AnotherLibrary": [ - .project( - target: "AnotherLibrary", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "AnotherLibrary", - destinations: destinations, - product: .staticFramework, - productName: "AnotherLibrary", - bundleId: "AnotherLibrary", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/AnotherLibrary/**", - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "another-dependency", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings( - configurations: [ - .debug(name: .debug), - .release(name: .release), - ] - ), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } - - public static func alamofire( - spmFolder: Path, - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - ) -> Self { - let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "Alamofire") - - let externalDependencies: [String: [TargetDependency]] = [ - "Alamofire": [ - .project( - target: "Alamofire", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "Alamofire", - destinations: destinations, - product: .staticFramework, - productName: "Alamofire", - bundleId: "Alamofire", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Source/**", - ], - dependencies: [ - .sdk( - name: "CFNetwork", - type: .framework, - status: .required, - condition: .when([.ios, .macos, .tvos, .watchos]) - ), - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "Alamofire", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings(base: ["SWIFT_VERSION": "5.0.0"]), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } - - // swiftlint:disable:next function_body_length - public static func googleAppMeasurement( - spmFolder: Path, - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - ) -> Self { - let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleAppMeasurement") - let artifactsFolder = Self.artifactsFolder(spmFolder: spmFolder, packageName: "GoogleAppMeasurement") - - let externalDependencies = [ - "GoogleAppMeasurement": [ - TargetDependency.project( - target: "GoogleAppMeasurementTarget", - path: packageFolder - ), - ], - "GoogleAppMeasurementWithoutAdIdSupport": [ - TargetDependency.project( - target: "GoogleAppMeasurementWithoutAdIdSupportTarget", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "GoogleAppMeasurementTarget", - destinations: destinations, - product: .staticFramework, - productName: "GoogleAppMeasurementTarget", - bundleId: "GoogleAppMeasurementTarget", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/GoogleAppMeasurementWrapper/**", - ], - dependencies: [ - .xcframework(path: "\(artifactsFolder.pathString)/GoogleAppMeasurement.xcframework"), - .project( - target: "GULAppDelegateSwizzler", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "GULMethodSwizzler", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "GULNSData", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "GULNetwork", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "nanopb", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "nanopb") - ), - .sdk(name: "sqlite3", type: .library, status: .required), - .sdk(name: "c++", type: .library, status: .required), - .sdk(name: "z", type: .library, status: .required), - .sdk(name: "StoreKit", type: .framework, status: .required), - ], - settings: Self.spmSettings() - ), - .init( - name: "GoogleAppMeasurementWithoutAdIdSupportTarget", - destinations: destinations, - product: .staticFramework, - productName: "GoogleAppMeasurementWithoutAdIdSupportTarget", - bundleId: "GoogleAppMeasurementWithoutAdIdSupportTarget", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/GoogleAppMeasurementWithoutAdIdSupportWrapper/**", - ], - dependencies: [ - .xcframework( - path: "\(artifactsFolder.pathString)/GoogleAppMeasurementWithoutAdIdSupport.xcframework" - ), - .project( - target: "GULAppDelegateSwizzler", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "GULMethodSwizzler", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "GULNSData", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "GULNetwork", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - ), - .project( - target: "nanopb", - path: Self.packageFolder(spmFolder: spmFolder, packageName: "nanopb") - ), - .sdk(name: "sqlite3", type: .library, status: .required), - .sdk(name: "c++", type: .library, status: .required), - .sdk(name: "z", type: .library, status: .required), - .sdk(name: "StoreKit", type: .framework, status: .required), - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "GoogleAppMeasurement", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings( - base: [ - "GCC_C_LANGUAGE_STANDARD": "c99", - "CLANG_CXX_LANGUAGE_STANDARD": "gnu++14", - ], - configurations: [ - .debug(name: .debug), - .release(name: .release), - ] - ), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } - - // swiftlint:disable:next function_body_length - public static func googleUtilities( - spmFolder: Path, - customProductTypes: [String: Product] = [:], - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - ) -> Self { - let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "GoogleUtilities") - - let externalDependencies: [String: [TargetDependency]] = [ - "GULAppDelegateSwizzler": [ - .project( - target: "GULAppDelegateSwizzler", - path: packageFolder - ), - ], - "GULMethodSwizzler": [ - .project( - target: "GULMethodSwizzler", - path: packageFolder - ), - ], - "GULNSData": [ - .project( - target: "GULNSData", - path: packageFolder - ), - ], - "GULNetwork": [ - .project( - target: "GULNetwork", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "GULAppDelegateSwizzler", - destinations: destinations, - product: customProductTypes["GULAppDelegateSwizzler"] ?? .staticFramework, - productName: "GULAppDelegateSwizzler", - bundleId: "GULAppDelegateSwizzler", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/GULAppDelegateSwizzler/**", - ], - settings: Self.spmSettings() - ), - .init( - name: "GULMethodSwizzler", - destinations: destinations, - product: customProductTypes["GULMethodSwizzler"] ?? .staticFramework, - productName: "GULMethodSwizzler", - bundleId: "GULMethodSwizzler", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/GULMethodSwizzler/**", - ], - settings: Self.spmSettings() - ), - - .init( - name: "GULNSData", - destinations: destinations, - product: customProductTypes["GULNSData"] ?? .staticFramework, - productName: "GULNSData", - bundleId: "GULNSData", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/GULNSData/**", - ], - settings: Self.spmSettings() - ), - .init( - name: "GULNetwork", - destinations: destinations, - product: customProductTypes["GULNetwork"] ?? .staticFramework, - productName: "GULNetwork", - bundleId: "GULNetwork", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/GULNetwork/**", - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "GoogleUtilities", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings( - configurations: [ - .debug(name: .debug), - .release(name: .release), - ] - ), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } - - public static func nanopb( - spmFolder: Path, - destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - ) -> Self { - let packageFolder = Self.packageFolder(spmFolder: spmFolder, packageName: "nanopb") - - let externalDependencies = [ - "nanopb": [ - TargetDependency.project( - target: "nanopb", - path: packageFolder - ), - ], - ] - - let targets: [Target] = [ - .init( - name: "nanopb", - destinations: destinations, - product: .staticFramework, - productName: "nanopb", - bundleId: "nanopb", - deploymentTargets: resolveDeploymentTargets(for: destinations), - infoPlist: .default, - sources: [ - "\(packageFolder.pathString)/Sources/nanopb/**", - ], - settings: Self.spmSettings() - ), - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [ - packageFolder: .init( - name: "nanopb", - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: .settings( - configurations: [ - .debug(name: .debug), - .release(name: .release), - ] - ), - targets: targets, - resourceSynthesizers: .default - ), - ] - ) - } -} - -extension DependenciesGraph { - fileprivate static func artifactsFolder(spmFolder: Path, packageName: String) -> Path { - Path("\(spmFolder.pathString)/artifacts/\(packageName)") - } - - fileprivate static func packageFolder(spmFolder: Path, packageName: String) -> Path { - Path("\(spmFolder.pathString)/checkouts/\(packageName)") - } - - static func spmSettings( - baseSettings: Settings = .settings(), - with customSettings: SettingsDictionary = [:], - moduleMap: String? = nil - ) -> Settings { - let defaultSpmSettings: SettingsDictionary = [ - "ALWAYS_SEARCH_USER_PATHS": "YES", - "GCC_WARN_INHIBIT_ALL_WARNINGS": "YES", - "SWIFT_SUPPRESS_WARNINGS": "YES", - "CLANG_ENABLE_OBJC_WEAK": "NO", - "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER": "NO", - "ENABLE_STRICT_OBJC_MSGSEND": "NO", - "FRAMEWORK_SEARCH_PATHS": ["$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks"], - "GCC_NO_COMMON_BLOCKS": "NO", - "USE_HEADERMAP": "NO", - ] - var settingsDictionary = customSettings.merging(defaultSpmSettings, uniquingKeysWith: { custom, _ in custom }) - - if let moduleMap { - settingsDictionary["MODULEMAP_FILE"] = .string(moduleMap) - } - - if case let .array(headerSearchPaths) = settingsDictionary["HEADER_SEARCH_PATHS"] { - settingsDictionary["HEADER_SEARCH_PATHS"] = .array(["$(inherited)"] + headerSearchPaths) - } - - if case let .array(cDefinitions) = settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] { - settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] = .array( - ["$(inherited)"] + (cDefinitions + ["SWIFT_PACKAGE=1"]) - .sorted() - ) - } else { - settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] = .array(["$(inherited)", "SWIFT_PACKAGE=1"]) - } - - if case let .string(swiftDefinitions) = settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] { - settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = - .string("$(inherited) SWIFT_PACKAGE \(swiftDefinitions)") - } else { - settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = .string("$(inherited) SWIFT_PACKAGE") - } - - if case let .array(cFlags) = settingsDictionary["OTHER_CFLAGS"] { - settingsDictionary["OTHER_CFLAGS"] = .array(["$(inherited)"] + cFlags) - } - - if case let .array(cxxFlags) = settingsDictionary["OTHER_CPLUSPLUSFLAGS"] { - settingsDictionary["OTHER_CPLUSPLUSFLAGS"] = .array(["$(inherited)"] + cxxFlags) - } - - if case let .array(swiftFlags) = settingsDictionary["OTHER_SWIFT_FLAGS"] { - settingsDictionary["OTHER_SWIFT_FLAGS"] = .array(["$(inherited)"] + swiftFlags) - } - - if case let .array(linkerFlags) = settingsDictionary["OTHER_LDFLAGS"] { - settingsDictionary["OTHER_LDFLAGS"] = .array(["$(inherited)"] + linkerFlags) - } - - return .settings( - base: baseSettings.base.merging(settingsDictionary, uniquingKeysWith: { $1 }), - configurations: baseSettings.configurations, - defaultSettings: baseSettings.defaultSettings - ) - } -} - -// MARK: - Helpers - -extension DependenciesGraph { - fileprivate static func resolveDeploymentTargets(for destinations: Destinations) -> DeploymentTargets { - let platforms = destinations.platforms - let applicableVersions = PLATFORM_TEST_VERSION.filter { platforms.contains($0.key) } - - return DeploymentTargets( - iOS: applicableVersions[Platform.iOS], - macOS: applicableVersions[Platform.macOS], - watchOS: applicableVersions[Platform.watchOS], - tvOS: applicableVersions[Platform.tvOS], - visionOS: applicableVersions[Platform.visionOS] - ) - } -} diff --git a/Sources/TuistDependenciesTesting/DependenciesGraph/MockDependenciesGraphController.swift b/Sources/TuistDependenciesTesting/DependenciesGraph/MockDependenciesGraphController.swift deleted file mode 100644 index 4159203a876..00000000000 --- a/Sources/TuistDependenciesTesting/DependenciesGraph/MockDependenciesGraphController.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph - -@testable import TuistDependencies - -public final class MockDependenciesGraphController: DependenciesGraphControlling { - public init() {} - - var invokedSave = false - var saveStub: ((TuistGraph.DependenciesGraph, AbsolutePath) throws -> Void)? - - public func save(_ dependenciesGraph: TuistGraph.DependenciesGraph, to path: AbsolutePath) throws { - invokedSave = true - try saveStub?(dependenciesGraph, path) - } - - var invokedLoad = false - var loadStub: ((AbsolutePath) throws -> TuistGraph.DependenciesGraph)? - - public func load(at path: AbsolutePath) throws -> TuistGraph.DependenciesGraph { - invokedLoad = true - return try loadStub?(path) ?? .test() - } - - var invokedClean = false - var cleanStub: ((AbsolutePath) throws -> Void)? - - public func clean(at path: AbsolutePath) throws { - invokedClean = true - try cleanStub?(path) - } -} diff --git a/Sources/TuistDependenciesTesting/MockDependenciesController.swift b/Sources/TuistDependenciesTesting/MockDependenciesController.swift deleted file mode 100644 index 78b2f7bfb77..00000000000 --- a/Sources/TuistDependenciesTesting/MockDependenciesController.swift +++ /dev/null @@ -1,73 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph - -@testable import TuistDependencies - -public final class MockDependenciesController: DependenciesControlling { - public init() {} - - public var invokedFetch = false - public var legacyFetchStub: ( - (AbsolutePath, TuistGraph.Dependencies, TSCUtility.Version?) throws -> TuistCore - .DependenciesGraph - )? - - public func fetch( - at path: AbsolutePath, - dependencies: TuistGraph.Dependencies, - swiftVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - invokedFetch = true - 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 legacyUpdateStub: ( - (AbsolutePath, TuistGraph.Dependencies, TSCUtility.Version?) throws -> TuistCore - .DependenciesGraph - )? - - public func update( - at path: AbsolutePath, - dependencies: TuistGraph.Dependencies, - swiftVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - invokedUpdate = true - 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 - public var saveStub: ((TuistGraph.DependenciesGraph, AbsolutePath) throws -> Void)? - - public func save(dependenciesGraph: TuistGraph.DependenciesGraph, to path: AbsolutePath) throws { - invokedSave = true - try saveStub?(dependenciesGraph, path) - } -} diff --git a/Sources/TuistDependenciesTesting/SwiftPackageManager/MockSwiftPackageManagerInteractor.swift b/Sources/TuistDependenciesTesting/SwiftPackageManager/MockSwiftPackageManagerInteractor.swift deleted file mode 100644 index 2a287e5e95b..00000000000 --- a/Sources/TuistDependenciesTesting/SwiftPackageManager/MockSwiftPackageManagerInteractor.swift +++ /dev/null @@ -1,41 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph - -@testable import TuistDependencies - -public final class MockSwiftPackageManagerInteractor: SwiftPackageManagerInteracting { - public init() {} - - var invokedInstall = false - var installStub: ( - ( - AbsolutePath, - TuistGraph.SwiftPackageManagerDependencies, - Set, - Bool, - TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph - )? - - public func install( - dependenciesDirectory: AbsolutePath, - dependencies: TuistGraph.SwiftPackageManagerDependencies, - platforms: Set, - shouldUpdate: Bool, - swiftToolsVersion: TSCUtility.Version? - ) throws -> TuistCore.DependenciesGraph { - invokedInstall = true - return try installStub?(dependenciesDirectory, dependencies, platforms, shouldUpdate, swiftToolsVersion) ?? .none - } - - var invokedClean = false - var cleanStub: ((AbsolutePath) throws -> Void)? - - public func clean(dependenciesDirectory: AbsolutePath) throws { - invokedClean = true - try cleanStub?(dependenciesDirectory) - } -} diff --git a/Sources/TuistDependenciesTesting/SwiftPackageManager/Utils/MockSwiftPackageManagerGraphGenerator.swift b/Sources/TuistDependenciesTesting/SwiftPackageManager/Utils/MockSwiftPackageManagerGraphGenerator.swift deleted file mode 100644 index 015d8972b35..00000000000 --- a/Sources/TuistDependenciesTesting/SwiftPackageManager/Utils/MockSwiftPackageManagerGraphGenerator.swift +++ /dev/null @@ -1,45 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph - -@testable import TuistDependencies - -public final class MockSwiftPackageManagerGraphGenerator: SwiftPackageManagerGraphGenerating { - public init() {} - - var invokedGenerate = false - var generateStub: ( - ( - AbsolutePath, - [String: TuistGraph.Product], - Set, - TuistGraph.Settings, - [String: TuistGraph.SettingsDictionary], - TSCUtility.Version?, - [String: TuistGraph.Project.Options] - ) throws -> TuistCore.DependenciesGraph - )? - - public func generate( - at path: AbsolutePath, - productTypes: [String: TuistGraph.Product], - platforms: Set, - baseSettings: TuistGraph.Settings, - targetSettings: [String: TuistGraph.SettingsDictionary], - swiftToolsVersion: TSCUtility.Version?, - projectOptions: [String: TuistGraph.Project.Options] - ) throws -> TuistCore.DependenciesGraph { - invokedGenerate = true - return try generateStub?( - path, - productTypes, - platforms, - baseSettings, - targetSettings, - swiftToolsVersion, - projectOptions - ) ?? .test() - } -} diff --git a/Sources/TuistEnvKit/Commands/BundleCommand.swift b/Sources/TuistEnvKit/Commands/BundleCommand.swift deleted file mode 100644 index 07a683912c9..00000000000 --- a/Sources/TuistEnvKit/Commands/BundleCommand.swift +++ /dev/null @@ -1,16 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic - -struct BundleCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "bundle", - abstract: "Bundles the version specified in the .tuist-version file into the .tuist-bin directory" - ) - } - - func run() throws { - try BundleService().run() - } -} diff --git a/Sources/TuistEnvKit/Commands/CommandRunner.swift b/Sources/TuistEnvKit/Commands/CommandRunner.swift deleted file mode 100644 index e02dd86dedc..00000000000 --- a/Sources/TuistEnvKit/Commands/CommandRunner.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility -import TuistSupport - -protocol CommandRunning: AnyObject { - func run() throws -} - -enum CommandRunnerError: FatalError { - case versionNotFound - - var type: ErrorType { - switch self { - case .versionNotFound: - return .abort - } - } - - var description: String { - switch self { - case .versionNotFound: - return "No valid version has been found locally" - } - } -} - -class CommandRunner: CommandRunning { - // MARK: - Attributes - - let versionResolver: VersionResolving - let environment: Environmenting - let updater: Updating - let versionsController: VersionsControlling - let installer: Installing - let arguments: () -> [String] - let exiter: (Int) -> Void - - // MARK: - Init - - init( - versionResolver: VersionResolving = VersionResolver(), - environment: Environmenting = Environment.shared, - updater: Updating = Updater(), - installer: Installing = Installer(), - versionsController: VersionsControlling = VersionsController(), - arguments: @escaping () -> [String] = CommandRunner.arguments, - exiter: @escaping (Int) -> Void = { exit(Int32($0)) } - ) { - self.versionResolver = versionResolver - self.environment = environment - self.versionsController = versionsController - self.arguments = arguments - self.updater = updater - self.installer = installer - self.exiter = exiter - } - - // MARK: - CommandRunning - - func run() throws { - let currentPath = FileHandler.shared.currentPath - - // Version resolving - let resolvedVersion = try versionResolver.resolve(path: currentPath) - - switch resolvedVersion { - case let .bin(path): - logger.debug("Using bundled version at path \(path.pathString)") - case let .versionFile(path, value): - logger.debug("Using version \(value) defined at \(path.pathString)") - default: - break - } - - if case let ResolvedVersion.bin(path) = resolvedVersion { - try runAtPath(path) - } else if case let ResolvedVersion.versionFile(_, version) = resolvedVersion { - try runVersion(version) - } else { - try runHighestLocalVersion() - } - } - - // MARK: - Fileprivate - - func runHighestLocalVersion() throws { - var version: String! - - if let highgestVersion = versionsController.semverVersions().last?.description { - version = highgestVersion - } else { - try updater.update() - guard let highgestVersion = versionsController.semverVersions().last?.description else { - throw CommandRunnerError.versionNotFound - } - version = highgestVersion - } - - let path = try versionsController.path(version: version) - try runAtPath(path) - } - - func runVersion(_ version: String) throws { - guard Version(version) != nil else { - logger.error("\(version) is not a valid version") - exiter(1) - return - } - - if !versionsController.versions().contains(where: { $0.description == version }) { - logger.notice("Version \(version) not found locally. Installing...") - try installer.install(version: version) - } - - let path = try versionsController.path(version: version) - try runAtPath(path) - } - - func runAtPath(_ path: AbsolutePath) throws { - var args: [String] = [] - - args.append(path.appending(component: Constants.binName).pathString) - args.append(contentsOf: Array(arguments().dropFirst())) - - var environment = ProcessInfo.processInfo.environment - if CommandLine.arguments.contains("--verbose") { - environment[Constants.EnvironmentVariables.verbose] = "true" - } - - do { - try System.shared.runAndPrint(args, verbose: false, environment: environment) - } catch { - exiter(1) - } - } - - // MARK: - Static - - static func arguments() -> [String] { - Array(ProcessInfo.processInfo.arguments).filter { $0 != "--verbose" } - } -} diff --git a/Sources/TuistEnvKit/Commands/InstallCommand.swift b/Sources/TuistEnvKit/Commands/InstallCommand.swift deleted file mode 100644 index 6c1db8f5b14..00000000000 --- a/Sources/TuistEnvKit/Commands/InstallCommand.swift +++ /dev/null @@ -1,21 +0,0 @@ -import ArgumentParser -import Foundation - -/// Command that installs new versions of Tuist in the system. -struct InstallCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "install", - abstract: "Installs a version of tuist" - ) - } - - @Argument( - help: "The version of tuist to be installed" - ) - var version: String - - func run() throws { - try InstallService().run(version: version) - } -} diff --git a/Sources/TuistEnvKit/Commands/LocalCommand.swift b/Sources/TuistEnvKit/Commands/LocalCommand.swift deleted file mode 100644 index e3afcb47f4c..00000000000 --- a/Sources/TuistEnvKit/Commands/LocalCommand.swift +++ /dev/null @@ -1,22 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic -import TuistSupport - -struct LocalCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "local", - abstract: "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions" - ) - } - - @Argument( - help: "The version that you would like to pin your current directory to" - ) - var version: String? - - func run() throws { - try LocalService().run(version: version) - } -} diff --git a/Sources/TuistEnvKit/Commands/TuistCommand.swift b/Sources/TuistEnvKit/Commands/TuistCommand.swift deleted file mode 100644 index 8a05a6d0541..00000000000 --- a/Sources/TuistEnvKit/Commands/TuistCommand.swift +++ /dev/null @@ -1,93 +0,0 @@ -import ArgumentParser -import Foundation -import TuistSupport - -public struct TuistCommand: ParsableCommand { - public init() {} - - public static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "tuist", - abstract: "Manage the environment tuist versions", - subcommands: [ - LocalCommand.self, - BundleCommand.self, - UpdateCommand.self, - InstallCommand.self, - UninstallCommand.self, - VersionCommand.self, - ] - ) - } - - public static func main(_: [String]? = nil) { - let errorHandler = ErrorHandler() - let processedArguments = processArguments() - - // Help env - if processedArguments.dropFirst().first == "--help-env" { - let error = CleanExit.helpRequest(self) - exit(withError: error) - } - - // Parse the command - var command: ParsableCommand? - do { - if let parsedArguments = try parse() { - command = try parseAsRoot(parsedArguments) - } - } catch { - let exitCode = exitCode(for: error).rawValue - if exitCode == 0 { - logger.info("\(fullMessage(for: error))") - } else { - logger.error("\(fullMessage(for: error))") - } - _exit(exitCode) - } - - // Run the command - do { - if var command { - try command.run() - } 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) - } else { - errorHandler.fatal(error: UnhandledError(error: error)) - _exit(exitCode(for: error).rawValue) - } - } - } - - // MARK: - Helpers - - private static func parse() throws -> [String]? { - let arguments = Array(processArguments().dropFirst()) - guard let firstArgument = arguments.first else { return nil } - // swiftformat:disable preferKeyPath - let containsCommand = configuration.subcommands.map { $0.configuration.commandName }.contains(firstArgument) - // swiftformat:enable preferKeyPath - if containsCommand { - return arguments - } - return nil - } - - // MARK: - Static - - static func processArguments() -> [String] { - CommandRunner.arguments() - } -} diff --git a/Sources/TuistEnvKit/Commands/UninstallCommand.swift b/Sources/TuistEnvKit/Commands/UninstallCommand.swift deleted file mode 100644 index 5b295625095..00000000000 --- a/Sources/TuistEnvKit/Commands/UninstallCommand.swift +++ /dev/null @@ -1,21 +0,0 @@ -import ArgumentParser -import Foundation -import TuistSupport - -struct UninstallCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "uninstall", - abstract: "Uninstalls a version of tuist" - ) - } - - @Argument( - help: "The version of tuist to be uninstalled" - ) - var version: String - - func run() throws { - try UninstallService().run(version: version) - } -} diff --git a/Sources/TuistEnvKit/Commands/UpdateCommand.swift b/Sources/TuistEnvKit/Commands/UpdateCommand.swift deleted file mode 100644 index 1ab855b8745..00000000000 --- a/Sources/TuistEnvKit/Commands/UpdateCommand.swift +++ /dev/null @@ -1,17 +0,0 @@ -import ArgumentParser -import Foundation -import TuistSupport - -/// Command that updates the version of Tuist in the environment. -struct UpdateCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "update", - abstract: "Installs the latest version if it's not already installed" - ) - } - - func run() throws { - try UpdateService().run() - } -} diff --git a/Sources/TuistEnvKit/Commands/VersionCommand.swift b/Sources/TuistEnvKit/Commands/VersionCommand.swift deleted file mode 100644 index 4f16408ce53..00000000000 --- a/Sources/TuistEnvKit/Commands/VersionCommand.swift +++ /dev/null @@ -1,17 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic -import TuistSupport - -struct VersionCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "envversion", - abstract: "Outputs the current version of tuist env." - ) - } - - func run() throws { - try VersionService().run() - } -} diff --git a/Sources/TuistEnvKit/HTTP/HTTPClient.swift b/Sources/TuistEnvKit/HTTP/HTTPClient.swift deleted file mode 100644 index 794ded14712..00000000000 --- a/Sources/TuistEnvKit/HTTP/HTTPClient.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -enum HTTPClientError: FatalError { - case clientError(URL, Error) - case noData(URL) - case copyFileError(AbsolutePath, Error) - case missingResource(URL) - - /// Error type - var type: ErrorType { - switch self { - case .clientError: - return .abort - case .noData: - return .abort - case .copyFileError: - return .abort - case .missingResource: - return .abort - } - } - - /// Error description. - var description: String { - switch self { - case let .clientError(url, error): - return "The request to \(url.absoluteString) errored with: \(error.localizedDescription)" - case let .noData(url): - return "The request to \(url.absoluteString) returned no data" - case let .copyFileError(path, error): - return "The file could not be copied into \(path.pathString): \(error.localizedDescription)" - case let .missingResource(url): - return "Couldn't locate resource downloaded from \(url.absoluteString)" - } - } -} - -protocol HTTPClienting { - /// Fetches the content from the given URL and returns it as a data. - /// - /// - Parameter url: URL to download the resource from. - /// - Returns: Response body as a data. - /// - Throws: An error if the request fails. - func read(url: URL) throws -> Data - - /// Downloads the resource from the given URL into the file at the given path. - /// - /// - Parameters: - /// - url: URL to download the resource from. - /// - to: Path where the file should be downloaded. - /// - Throws: An error if the dowload fails. - func download(url: URL, to: AbsolutePath) throws -} - -final class HTTPClient: HTTPClienting { - // MARK: - Attributes - - /// URL session. - fileprivate let session: URLSession = .shared - - // MARK: - HTTPClienting - - /// Fetches the content from the given URL and returns it as a data. - /// - /// - Parameter url: URL to download the resource from. - /// - Returns: Response body as a data. - /// - Throws: An error if the request fails. - func read(url: URL) throws -> Data { - var data: Data? - var error: Error? - - let semaphore = DispatchSemaphore(value: 0) - session.dataTask(with: url) { responseData, _, responseError in - data = responseData - error = responseError - semaphore.signal() - }.resume() - semaphore.wait() - - if let error { - throw HTTPClientError.clientError(url, error) - } - guard let resultData = data else { - throw HTTPClientError.noData(url) - } - return resultData - } - - /// Downloads the resource from the given URL into the file at the given path. - /// - /// - Parameters: - /// - url: URL to download the resource from. - /// - to: Path where the file should be downloaded. - /// - Throws: An error if the dowload fails. - func download(url: URL, to: AbsolutePath) throws { - let semaphore = DispatchSemaphore(value: 0) - var clientError: HTTPClientError? - - session.downloadTask(with: url) { downloadURL, _, error in - defer { semaphore.signal() } - if let error { - clientError = HTTPClientError.clientError(url, error) - } else if let downloadURL { - let from = try! AbsolutePath(validating: downloadURL.path) // swiftlint:disable:this force_try - do { - try FileHandler.shared.copy(from: from, to: to) - } catch { - clientError = HTTPClientError.copyFileError(to, error) - } - } else { - clientError = .missingResource(url) - } - }.resume() - semaphore.wait() - if let clientError { - throw clientError - } - } -} diff --git a/Sources/TuistEnvKit/Installer/BuildCopier.swift b/Sources/TuistEnvKit/Installer/BuildCopier.swift deleted file mode 100644 index fb4cc7151ab..00000000000 --- a/Sources/TuistEnvKit/Installer/BuildCopier.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -protocol BuildCopying: AnyObject { - func copy(from: AbsolutePath, to: AbsolutePath) throws -} - -class BuildCopier: BuildCopying { - // MARK: - Static - - /// Files that should be copied (if they exist). - static let files: [String] = [ - "tuist", - Constants.templatesDirectoryName, - Constants.vendorDirectoryName, - // Project description - "ProjectDescription.swiftmodule", - "ProjectDescription.swiftdoc", - "ProjectDescription.swiftinterface", - "libProjectDescription.dylib", - ] - - func copy(from: AbsolutePath, to: AbsolutePath) throws { - try BuildCopier.files.forEach { file in - let filePath = from.appending(component: file) - let toPath = to.appending(component: file) - if !FileHandler.shared.exists(filePath) { return } - try System.shared.run(["/bin/cp", "-rf", filePath.pathString, toPath.pathString]) - if file == "tuist" { - try System.shared.run(["/bin/chmod", "+x", toPath.pathString]) - } - } - } -} diff --git a/Sources/TuistEnvKit/Installer/EnvInstaller.swift b/Sources/TuistEnvKit/Installer/EnvInstaller.swift deleted file mode 100644 index d8413b2135d..00000000000 --- a/Sources/TuistEnvKit/Installer/EnvInstaller.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -/// Protocol that defines the interface of an instance that can install versions of TuistEnv. -protocol EnvInstalling: AnyObject { - /// It installs a version of Tuist in the local environment. - /// - /// - Parameters: - /// - version: Version to be installed. - /// - Throws: An error if the installation fails. - func install(version: String) throws -} - -/// Error thrown by the installer. -/// -/// - versionNotFound: When the specified version cannot be found. -/// - incompatibleSwiftVersion: When the environment Swift version is incompatible with the Swift version Tuist has been compiled -/// with. -enum EnvInstallerError: FatalError, Equatable { - case versionNotFound(String) - case incompatibleSwiftVersion(local: String, expected: String) - - var type: ErrorType { - switch self { - case .versionNotFound: return .abort - case .incompatibleSwiftVersion: return .abort - } - } - - var description: String { - switch self { - case let .versionNotFound(version): - return "Version \(version) not found" - case let .incompatibleSwiftVersion(local, expected): - return "Found \(local) Swift version but expected \(expected)" - } - } -} - -/// Class that manages the installation of Tuist versions. -final class EnvInstaller: EnvInstalling { - // MARK: - Attributes - - let buildCopier: BuildCopying - let versionsController: VersionsControlling - - // MARK: - Init - - init( - buildCopier: BuildCopying = BuildCopier(), - versionsController: VersionsControlling = VersionsController() - ) { - self.buildCopier = buildCopier - self.versionsController = versionsController - } - - // MARK: - Installing - - func install(version: String) throws { - try withTemporaryDirectory { temporaryDirectory in - try install(version: version, temporaryDirectory: temporaryDirectory) - } - } - - func install(version: String, temporaryDirectory: AbsolutePath) throws { - try installFromBundle( - bundleURL: URL(string: "https://github.com/tuist/tuist/releases/download/\(version)/tuistenv.zip")!, - version: version, - temporaryDirectory: temporaryDirectory - ) - } - - func installFromBundle( - bundleURL: URL, - version: String, - temporaryDirectory: AbsolutePath - ) throws { - let installationPath = try System.shared.which("tuist") - - // Download bundle - logger.notice("Downloading TuistEnv version \(version)") - let downloadPath = temporaryDirectory.appending(component: Constants.envBundleName) - try System.shared.run(["/usr/bin/curl", "-LSs", "--output", downloadPath.pathString, bundleURL.absoluteString]) - - // Unzip - logger.notice("Installing…") - try System.shared.run(["/usr/bin/unzip", "-q", downloadPath.pathString, "tuistenv", "-d", temporaryDirectory.pathString]) - - // Remove old version - let rmArgs = ["rm", installationPath] - do { - try System.shared.run(rmArgs) - } catch { - try System.shared.run(["sudo"] + rmArgs) - } - - // Move - let mvArgs = ["mv", temporaryDirectory.appending(component: "tuistenv").pathString, installationPath] - do { - try System.shared.run(mvArgs) - } catch { - try System.shared.run(["sudo"] + mvArgs) - } - - logger.notice("TuistEnv Version \(version) installed") - } -} diff --git a/Sources/TuistEnvKit/Installer/Installer.swift b/Sources/TuistEnvKit/Installer/Installer.swift deleted file mode 100644 index 42cfc499df1..00000000000 --- a/Sources/TuistEnvKit/Installer/Installer.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -/// Protocol that defines the interface of an instance that can install versions of Tuist. -protocol Installing: AnyObject { - /// It installs a version of Tuist in the local environment. - /// - /// - Parameters: - /// - version: Version to be installed. - /// - Throws: An error if the installation fails. - func install(version: String) throws -} - -/// Error thrown by the installer. -/// -/// - versionNotFound: When the specified version cannot be found. -/// - incompatibleSwiftVersion: When the environment Swift version is incompatible with the Swift version Tuist has been compiled -/// with. -enum InstallerError: FatalError, Equatable { - case versionNotFound(String) - case incompatibleSwiftVersion(local: String, expected: String) - - var type: ErrorType { - switch self { - case .versionNotFound: return .abort - case .incompatibleSwiftVersion: return .abort - } - } - - var description: String { - switch self { - case let .versionNotFound(version): - return "Version \(version) not found" - case let .incompatibleSwiftVersion(local, expected): - return "Found \(local) Swift version but expected \(expected)" - } - } -} - -/// Class that manages the installation of Tuist versions. -final class Installer: Installing { - // MARK: - Attributes - - let buildCopier: BuildCopying - let versionsController: VersionsControlling - - // MARK: - Init - - init( - buildCopier: BuildCopying = BuildCopier(), - versionsController: VersionsControlling = VersionsController() - ) { - self.buildCopier = buildCopier - self.versionsController = versionsController - } - - // MARK: - Installing - - func install(version: String) throws { - try withTemporaryDirectory { temporaryDirectory in - try install(version: version, temporaryDirectory: temporaryDirectory) - } - } - - func install(version: String, temporaryDirectory: AbsolutePath) throws { - try installFromBundle( - bundleURL: URL(string: "https://github.com/tuist/tuist/releases/download/\(version)/tuist.zip")!, - version: version, - temporaryDirectory: temporaryDirectory - ) - } - - func installFromBundle( - bundleURL: URL, - version: String, - temporaryDirectory: AbsolutePath - ) throws { - try versionsController.install(version: version, installation: { installationDirectory in - - // Download bundle - logger.notice("Downloading version \(version)") - - let downloadPath = temporaryDirectory.appending(component: Constants.bundleName) - try System.shared.run(["/usr/bin/curl", "-LSs", "--output", downloadPath.pathString, bundleURL.absoluteString]) - - // Unzip - logger.notice("Installing…") - try System.shared.run(["/usr/bin/unzip", "-q", downloadPath.pathString, "-d", installationDirectory.pathString]) - - logger.notice("Version \(version) installed") - }) - } -} diff --git a/Sources/TuistEnvKit/Installer/SwiftVersion.swift b/Sources/TuistEnvKit/Installer/SwiftVersion.swift deleted file mode 100644 index 66bfd3acda0..00000000000 --- a/Sources/TuistEnvKit/Installer/SwiftVersion.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -struct SwiftVersion: Comparable, Equatable, CustomStringConvertible { - let major: Int - let minor: Int - let patch: Int - - init(_ value: String) { - let components = value.split(separator: ".") - major = Int(String(components[0]))! - - if components.count == 3 { - minor = Int(String(components[1]))! - patch = Int(String(components[2]))! - } else if components.count == 2 { - minor = Int(String(components[1]))! - patch = 0 - } else { - minor = 0 - patch = 0 - } - } - - var description: String { - "\(major).\(minor).\(patch)" - } - - // MARK: - Comparable - - static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool { - lhs.major < rhs.major || lhs.minor < rhs.minor || lhs.patch < lhs.patch - } -} diff --git a/Sources/TuistEnvKit/Services/BundleService.swift b/Sources/TuistEnvKit/Services/BundleService.swift deleted file mode 100644 index c759129f1ba..00000000000 --- a/Sources/TuistEnvKit/Services/BundleService.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -enum BundleServiceError: FatalError, Equatable { - case missingVersionFile(AbsolutePath) - - var type: ErrorType { - switch self { - case .missingVersionFile: - return .abort - } - } - - var description: String { - switch self { - case let .missingVersionFile(path): - return "Couldn't find a .tuist-version file in the directory \(path.pathString)" - } - } -} - -final class BundleService { - private let versionsController: VersionsControlling - private let installer: Installing - - init( - versionsController: VersionsControlling = VersionsController(), - installer: Installing = Installer() - ) { - self.versionsController = versionsController - self.installer = installer - } - - func run() throws { - let versionFilePath = FileHandler.shared.currentPath.appending(component: Constants.versionFileName) - let binFolderPath = FileHandler.shared.currentPath.appending(component: Constants.binFolderName) - - if !FileHandler.shared.exists(versionFilePath) { - throw BundleServiceError.missingVersionFile(FileHandler.shared.currentPath) - } - - let version = try String(contentsOf: versionFilePath.url).trimmingCharacters(in: .whitespacesAndNewlines) - logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)", metadata: .section) - - let versionPath = try versionsController.path(version: version) - - // Installing - if !FileHandler.shared.exists(versionPath) { - logger.notice("Version \(version) not available locally. Installing...") - try installer.install(version: version) - } - - // Copying - if FileHandler.shared.exists(binFolderPath) { - try FileHandler.shared.delete(binFolderPath) - } - try FileHandler.shared.copy(from: versionPath, to: binFolderPath) - - logger.notice("tuist bundled successfully at \(binFolderPath.pathString)", metadata: .success) - } -} diff --git a/Sources/TuistEnvKit/Services/InstallService.swift b/Sources/TuistEnvKit/Services/InstallService.swift deleted file mode 100644 index c4477f1044a..00000000000 --- a/Sources/TuistEnvKit/Services/InstallService.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import TSCUtility -import TuistSupport - -final class InstallService { - /// Controller to manage system versions. - private let versionsController: VersionsControlling - - /// Installer instance to run the installation. - private let installer: Installing - - init( - versionsController: VersionsControlling = VersionsController(), - installer: Installing = Installer() - ) { - self.versionsController = versionsController - self.installer = installer - } - - func run(version: String) throws { - let parsedVersion = try Version(versionString: version, usesLenientParsing: true) - let versions = versionsController.versions().map(\.description) - if versions.contains(parsedVersion.description) { - logger.warning("Version \(parsedVersion) already installed, skipping") - return - } - try installer.install(version: parsedVersion.description) - } -} diff --git a/Sources/TuistEnvKit/Services/LocalService.swift b/Sources/TuistEnvKit/Services/LocalService.swift deleted file mode 100644 index 8cbf9ad806f..00000000000 --- a/Sources/TuistEnvKit/Services/LocalService.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -final class LocalService { - private let versionController: VersionsControlling - - init(versionController: VersionsControlling = VersionsController()) { - self.versionController = versionController - } - - func run(version: String?) throws { - if let version { - try createVersionFile(version: version) - } else { - try printLocalVersions() - } - } - - // MARK: - Helpers - - private func printLocalVersions() throws { - logger.notice("The following versions are available in the local environment:", metadata: .section) - let versions = versionController.semverVersions() - let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n") - logger.notice("\(output)") - } - - private func createVersionFile(version: String) throws { - let currentPath = FileHandler.shared.currentPath - logger.notice("Generating \(Constants.versionFileName) file with version \(version)", metadata: .section) - let tuistVersionPath = currentPath.appending(component: Constants.versionFileName) - try "\(version)".write( - to: URL(fileURLWithPath: tuistVersionPath.pathString), - atomically: true, - encoding: .utf8 - ) - logger.notice("File generated at path \(tuistVersionPath.pathString)", metadata: .success) - } -} diff --git a/Sources/TuistEnvKit/Services/UninstallService.swift b/Sources/TuistEnvKit/Services/UninstallService.swift deleted file mode 100644 index 30d946d391c..00000000000 --- a/Sources/TuistEnvKit/Services/UninstallService.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import TuistSupport - -final class UninstallService { - /// Controller to manage system versions. - private let versionsController: VersionsControlling - - /// Installer instance to run the installation. - private let installer: Installing - - init( - versionsController: VersionsControlling = VersionsController(), - installer: Installing = Installer() - ) { - self.versionsController = versionsController - self.installer = installer - } - - func run(version: String) throws { - let versions = versionsController.versions().map(\.description) - if versions.contains(version) { - try versionsController.uninstall(version: version) - logger.notice("Version \(version) uninstalled", metadata: .success) - } else { - logger.warning("Version \(version) cannot be uninstalled because it's not installed") - } - } -} diff --git a/Sources/TuistEnvKit/Services/UpdateService.swift b/Sources/TuistEnvKit/Services/UpdateService.swift deleted file mode 100644 index 72bc4d564b7..00000000000 --- a/Sources/TuistEnvKit/Services/UpdateService.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import TuistSupport - -final class UpdateService { - /// Updater instance that runs the update. - private let updater: Updating - - init(updater: Updating = Updater()) { - self.updater = updater - } - - func run() throws { - logger.notice("Checking for updates...", metadata: .section) - try updater.update() - } -} diff --git a/Sources/TuistEnvKit/Services/VersionService.swift b/Sources/TuistEnvKit/Services/VersionService.swift deleted file mode 100644 index 12179c35811..00000000000 --- a/Sources/TuistEnvKit/Services/VersionService.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -final class VersionService { - func run() throws { - logger.notice("\(Constants.version)") - } -} diff --git a/Sources/TuistEnvKit/Settings/Settings.swift b/Sources/TuistEnvKit/Settings/Settings.swift deleted file mode 100644 index d22a69e8b14..00000000000 --- a/Sources/TuistEnvKit/Settings/Settings.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -/// Settings -class Settings: Codable, Equatable { - // MARK: - Init - - /// Initializes the settings instance. - init() {} - - // MARK: - Equatable - - /// Compares two instances of Settings. - /// - /// - Parameters: - /// - lhs: first instance to be compared. - /// - rhs: second instance to be compared. - /// - Returns: true if the two instances are equal. - static func == (_: Settings, _: Settings) -> Bool { - true - } -} diff --git a/Sources/TuistEnvKit/Settings/SettingsController.swift b/Sources/TuistEnvKit/Settings/SettingsController.swift deleted file mode 100644 index fddcd32417f..00000000000 --- a/Sources/TuistEnvKit/Settings/SettingsController.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import TuistSupport - -/// The class that conforms this protocol exposes an interface for interacting with the user settings. -protocol SettingsControlling: AnyObject { - /// It fetches the current settings. - /// - /// - Returns: settings. - /// - Throws: an error if the settings cannot be fetched. - func settings() throws -> Settings - - /// Stores the settings. - /// - /// - Parameter settings: settings to be stored. - /// - Throws: an error if the saving fails. - func set(settings: Settings) throws -} - -/// Controller to manage user settings. -class SettingsController: SettingsControlling { - /// It fetches the current settings. - /// - /// - Returns: settings. - /// - Throws: an error if the settings cannot be fetched. - func settings() throws -> Settings { - let path = Environment.shared.settingsPath - if !FileHandler.shared.exists(path) { return Settings() } - let data = try Data(contentsOf: URL(fileURLWithPath: path.pathString)) - let decoder = JSONDecoder() - return try decoder.decode(Settings.self, from: data) - } - - /// Stores the settings. - /// - /// - Parameter settings: settings to be stored. - /// - Throws: an error if the saving fails. - func set(settings: Settings) throws { - let encoder = JSONEncoder() - let data = try encoder.encode(settings) - let path = Environment.shared.settingsPath - if FileHandler.shared.exists(path) { try FileHandler.shared.delete(path) } - let url = URL(fileURLWithPath: path.pathString) - try data.write(to: url, options: Data.WritingOptions.atomic) - } -} diff --git a/Sources/TuistEnvKit/Settings/VersionProvider.swift b/Sources/TuistEnvKit/Settings/VersionProvider.swift deleted file mode 100644 index d7a1fe6fd21..00000000000 --- a/Sources/TuistEnvKit/Settings/VersionProvider.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Combine -import Foundation -import TSCBasic -import TSCUtility -import TuistSupport - -protocol VersionProviding { - /// Returns the list of versions available on GitHub by parsing the release tags. - /// - Returns: An array of the versions. - func versions() throws -> [Version] - - /// Returns the latest available version - /// - Returns: The latest available version, or `nil` if no versions were found. - func latestVersion() throws -> Version? -} - -class VersionProvider: VersionProviding { - let gitHandler: GitHandling - - init(gitHandler: GitHandling = GitHandler()) { - self.gitHandler = gitHandler - } - - func versions() throws -> [Version] { - try gitHandler.remoteTaggedVersions(url: Constants.gitRepositoryURL) - } - - func latestVersion() throws -> Version? { - try versions().last - } -} diff --git a/Sources/TuistEnvKit/Updater/Updater.swift b/Sources/TuistEnvKit/Updater/Updater.swift deleted file mode 100644 index d0071349a2a..00000000000 --- a/Sources/TuistEnvKit/Updater/Updater.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import TuistSupport - -protocol Updating: AnyObject { - func update() throws -} - -final class Updater: Updating { - // MARK: - Attributes - - let versionsController: VersionsControlling - let installer: Installing - let envInstaller: EnvInstalling - let versionProvider: VersionProviding - - // MARK: - Init - - init( - versionsController: VersionsControlling = VersionsController(), - installer: Installing = Installer(), - envInstaller: EnvInstalling = EnvInstaller(), - versionProvider: VersionProviding = VersionProvider() - ) { - self.versionsController = versionsController - self.installer = installer - self.envInstaller = envInstaller - self.versionProvider = versionProvider - } - - // MARK: - Internal - - func update() throws { - guard let highestRemoteVersion = try versionProvider.latestVersion() else { - logger.warning("No remote versions found") - return - } - - if let highestLocalVersion = versionsController.semverVersions().sorted().last { - guard highestRemoteVersion > highestLocalVersion else { - logger.notice("There are no updates available") - return - } - logger.notice("Installing new version available \(highestRemoteVersion)") - } else { - logger.notice("No local versions available. Installing the latest version \(highestRemoteVersion)") - } - - try installer.install(version: highestRemoteVersion.description) - logger.info("Updating tuistenv", metadata: .section) - try envInstaller.install(version: highestRemoteVersion.description) - } -} diff --git a/Sources/TuistEnvKit/Versions/VersionResolver.swift b/Sources/TuistEnvKit/Versions/VersionResolver.swift deleted file mode 100644 index a37d78316b8..00000000000 --- a/Sources/TuistEnvKit/Versions/VersionResolver.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -enum ResolvedVersion: Equatable { - case bin(AbsolutePath) - case versionFile(AbsolutePath, String) - case undefined -} - -protocol VersionResolving: AnyObject { - func resolve(path: AbsolutePath) throws -> ResolvedVersion -} - -enum VersionResolverError: FatalError, Equatable { - case readError(path: AbsolutePath) - - var type: ErrorType { - switch self { - case .readError: return .abort - } - } - - var description: String { - switch self { - case let .readError(path): - return "Cannot read the version file at path \(path.pathString)." - } - } -} - -class VersionResolver: VersionResolving { - // MARK: - Attributes - - private let settingsController: SettingsControlling - private let fileManager: FileManager = .default - - // MARK: - Init - - init(settingsController: SettingsControlling = SettingsController()) { - self.settingsController = settingsController - } - - // MARK: - VersionResolving - - func resolve(path: AbsolutePath) throws -> ResolvedVersion { - try resolveTraversing(from: path) - } - - // MARK: - Fileprivate - - private func resolveTraversing(from path: AbsolutePath) throws -> ResolvedVersion { - let versionPath = path.appending(component: Constants.versionFileName) - let binPath = path.appending(component: Constants.binFolderName) - if fileManager.fileExists(atPath: binPath.appending(component: Constants.binName).pathString) { - return .bin(binPath) - } else if fileManager.fileExists(atPath: versionPath.pathString) { - return try resolveVersionFile(path: versionPath) - } - if path.components.count > 1 { - return try resolveTraversing(from: path.parentDirectory) - } - return .undefined - } - - private func resolveVersionFile(path: AbsolutePath) throws -> ResolvedVersion { - var value: String! - do { - value = try String(contentsOf: URL(fileURLWithPath: path.pathString)).trimmingCharacters(in: .whitespacesAndNewlines) - } catch { - throw VersionResolverError.readError(path: path) - } - return ResolvedVersion.versionFile(path, value) - } -} diff --git a/Sources/TuistEnvKit/Versions/VersionsController.swift b/Sources/TuistEnvKit/Versions/VersionsController.swift deleted file mode 100644 index 22849a528f2..00000000000 --- a/Sources/TuistEnvKit/Versions/VersionsController.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version -import TuistSupport - -protocol VersionsControlling: AnyObject { - typealias Installation = (AbsolutePath) throws -> Void - - func install(version: String, installation: Installation) throws - func uninstall(version: String) throws - func path(version: String) throws -> AbsolutePath - func versions() -> [InstalledVersion] - func semverVersions() -> [Version] -} - -enum InstalledVersion: CustomStringConvertible, Equatable { - case semver(Version) - case reference(String) - - var description: String { - switch self { - case let .reference(value): return value - case let .semver(value): return value.description - } - } -} - -class VersionsController: VersionsControlling { - // MARK: - VersionsControlling - - func install(version: String, installation: Installation) throws { - try withTemporaryDirectory { tmpDir in - try installation(tmpDir) - - // Copy only if there's file in the folder - if !tmpDir.glob("*").isEmpty { - let dstPath = path(version: version) - if FileHandler.shared.exists(dstPath) { - try FileHandler.shared.delete(dstPath) - } - try FileHandler.shared.copy(from: tmpDir, to: dstPath) - } - } - } - - func uninstall(version: String) throws { - let path = path(version: version) - if FileHandler.shared.exists(path) { - try FileHandler.shared.delete(path) - } - } - - func path(version: String) -> AbsolutePath { - Environment.shared.versionsDirectory.appending(component: version) - } - - func versions() -> [InstalledVersion] { - Environment.shared.versionsDirectory.glob("*").map { path in - let versionStringValue = path.components.last! - if let version = Version(versionStringValue) { - return InstalledVersion.semver(version) - } else { - return InstalledVersion.reference(versionStringValue) - } - } - } - - func semverVersions() -> [Version] { - versions().compactMap { version in - if case let InstalledVersion.semver(semver) = version { - return semver - } - return nil - }.sorted() - } -} diff --git a/Sources/TuistGenerator/Descriptors/ProjectDescriptor.swift b/Sources/TuistGenerator/Descriptors/ProjectDescriptor.swift index a5e12017740..2985a2f7e95 100644 --- a/Sources/TuistGenerator/Descriptors/ProjectDescriptor.swift +++ b/Sources/TuistGenerator/Descriptors/ProjectDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import XcodeProj diff --git a/Sources/TuistGenerator/Descriptors/SideEffectDescriptorExecutor.swift b/Sources/TuistGenerator/Descriptors/SideEffectDescriptorExecutor.swift index e0278ccc6c3..6adb1261821 100644 --- a/Sources/TuistGenerator/Descriptors/SideEffectDescriptorExecutor.swift +++ b/Sources/TuistGenerator/Descriptors/SideEffectDescriptorExecutor.swift @@ -1,3 +1,4 @@ +import FileSystem import Foundation import TuistCore import TuistSupport @@ -6,31 +7,35 @@ import TuistSupport public protocol SideEffectDescriptorExecuting: AnyObject { /// Executes the given side effects sequentially. /// - Parameter sideEffects: Side effects to be executed. - func execute(sideEffects: [SideEffectDescriptor]) throws + func execute(sideEffects: [SideEffectDescriptor]) async throws } public final class SideEffectDescriptorExecutor: SideEffectDescriptorExecuting { - public init() {} + private let fileSystem: FileSystem + + public init(fileSystem: FileSystem = FileSystem()) { + self.fileSystem = fileSystem + } // MARK: - SideEffectDescriptorExecuting - public func execute(sideEffects: [SideEffectDescriptor]) throws { + public func execute(sideEffects: [SideEffectDescriptor]) async throws { for sideEffect in sideEffects { logger.debug("Side effect: \(sideEffect)") switch sideEffect { case let .command(commandDescriptor): try perform(command: commandDescriptor) case let .file(fileDescriptor): - try process(file: fileDescriptor) + try await process(file: fileDescriptor) case let .directory(directoryDescriptor): - try process(directory: directoryDescriptor) + try await process(directory: directoryDescriptor) } } } // MARK: - Fileprivate - private func process(file: FileDescriptor) throws { + private func process(file: FileDescriptor) async throws { switch file.state { case .present: try FileHandler.shared.createFolder(file.path.parentDirectory) @@ -40,11 +45,11 @@ public final class SideEffectDescriptorExecutor: SideEffectDescriptorExecuting { try FileHandler.shared.touch(file.path) } case .absent: - try FileHandler.shared.delete(file.path) + try await fileSystem.remove(file.path) } } - private func process(directory: DirectoryDescriptor) throws { + private func process(directory: DirectoryDescriptor) async throws { switch directory.state { case .present: if !FileHandler.shared.exists(directory.path) { @@ -52,7 +57,7 @@ public final class SideEffectDescriptorExecutor: SideEffectDescriptorExecuting { } case .absent: if FileHandler.shared.exists(directory.path) { - try FileHandler.shared.delete(directory.path) + try await fileSystem.remove(directory.path) } } } diff --git a/Sources/TuistGenerator/Descriptors/WorkspaceDescriptor.swift b/Sources/TuistGenerator/Descriptors/WorkspaceDescriptor.swift index 8a50a5a4ff0..8cfd11320c1 100644 --- a/Sources/TuistGenerator/Descriptors/WorkspaceDescriptor.swift +++ b/Sources/TuistGenerator/Descriptors/WorkspaceDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import XcodeProj diff --git a/Sources/TuistGenerator/Descriptors/WorkspaceSettingsDescriptor.swift b/Sources/TuistGenerator/Descriptors/WorkspaceSettingsDescriptor.swift index 92d8de7b6cd..3c95feaa9e8 100644 --- a/Sources/TuistGenerator/Descriptors/WorkspaceSettingsDescriptor.swift +++ b/Sources/TuistGenerator/Descriptors/WorkspaceSettingsDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XcodeProj /// Workspace Settings Descriptor diff --git a/Sources/TuistGenerator/Extensions/AbsolutePath+Extras.swift b/Sources/TuistGenerator/Extensions/AbsolutePath+Extras.swift index 377caa36b2f..e55da711ab7 100644 --- a/Sources/TuistGenerator/Extensions/AbsolutePath+Extras.swift +++ b/Sources/TuistGenerator/Extensions/AbsolutePath+Extras.swift @@ -1,6 +1,6 @@ import Foundation +import Path import PathKit -import TSCBasic extension AbsolutePath { var path: Path { diff --git a/Sources/TuistGenerator/Extensions/CopyFilesAction+Extras.swift b/Sources/TuistGenerator/Extensions/CopyFilesAction+Extras.swift index 3a65175c20a..ff9998ea6ac 100644 --- a/Sources/TuistGenerator/Extensions/CopyFilesAction+Extras.swift +++ b/Sources/TuistGenerator/Extensions/CopyFilesAction+Extras.swift @@ -1,5 +1,5 @@ import TuistCore -import TuistGraph +import XcodeGraph import XcodeProj extension CopyFilesAction.Destination { diff --git a/Sources/TuistGenerator/Extensions/Graph+Extras.swift b/Sources/TuistGenerator/Extensions/Graph+Extras.swift index 7b15a196424..235179e560a 100644 --- a/Sources/TuistGenerator/Extensions/Graph+Extras.swift +++ b/Sources/TuistGenerator/Extensions/Graph+Extras.swift @@ -1,9 +1,10 @@ import Foundation +import Path import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.Graph { +extension XcodeGraph.Graph { /// Filters the project graph /// - Parameters: /// - Returns: Filtered graph targets and dependencies @@ -42,8 +43,12 @@ extension TuistGraph.Graph { return filteredTargetsAndDependencies.reduce(into: [GraphTarget: Set]()) { result, target in if skipExternalDependencies, target.project.isExternal { return } - guard let targetDependencies = graphTraverser.dependencies[.target(name: target.target.name, path: target.path)] - else { return } + guard let targetDependencies = graphTraverser + .dependencies[.target(name: target.target.name, path: target.path)] + else { + result[target] = Set() + return + } result[target] = targetDependencies .filter { dependency in @@ -55,7 +60,7 @@ extension TuistGraph.Graph { } extension GraphDependency { - fileprivate func isExternal(_ projects: [AbsolutePath: TuistGraph.Project]) -> Bool { + fileprivate func isExternal(_ projects: [Path.AbsolutePath: XcodeGraph.Project]) -> Bool { switch self { case let .target(_, path): return projects[path]?.isExternal ?? false diff --git a/Sources/TuistGenerator/Extensions/Xcodeproj+Extras.swift b/Sources/TuistGenerator/Extensions/Xcodeproj+Extras.swift index 67be89721f8..38448d9e812 100644 --- a/Sources/TuistGenerator/Extensions/Xcodeproj+Extras.swift +++ b/Sources/TuistGenerator/Extensions/Xcodeproj+Extras.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph import XcodeProj extension PBXFileElement { diff --git a/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift b/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift index 4a4f86045c1..195b9f4c3a3 100644 --- a/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift +++ b/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj enum BuildPhaseGenerationError: FatalError, Equatable { @@ -71,6 +71,21 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { ) } + /** + Targets that depend on a Swift Macro have the following dependency graph: + - Target -> MyMacro (Static framework) -> MyMacro (Executable) + - or, in some cases, they directly depend on the executable: Target -> 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 $(BUILD_DIR)/$(CONFIGURATION)/ExecutableName\#ExecutableName` build setting. + */ + let directSwiftMacroExecutables = graphTraverser.directSwiftMacroExecutables(path: path, name: target.name).sorted() + try generateCopySwiftMacroExecutableScriptBuildPhase( + directSwiftMacroExecutables: directSwiftMacroExecutables, + pbxTarget: pbxTarget, + pbxproj: pbxproj + ) + if target.supportsSources { try generateSourcesBuildPhase( files: target.sources, @@ -334,7 +349,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxBuildFiles.append(contentsOf: try generateResourcesBuildFile( target: target, - files: target.resources, + files: target.resources.resources, fileElements: fileElements )) @@ -386,16 +401,30 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxTarget.buildPhases.append(copyFilesPhase) var buildFilesCache = Set() - let filePaths = action.files.map(\.path).sorted() + let files = action.files.sorted(using: KeyPathComparator(\.path)) var pbxBuildFiles = [PBXBuildFile]() - for filePath in filePaths { + for file in files { + let filePath = file.path guard let fileReference = fileElements.file(path: filePath) else { throw BuildPhaseGenerationError.missingFileReference(filePath) } + var settings: [String: Any]? + + /// File ATTRIBUTES + /// example: `settings = {ATTRIBUTES = (Codesign, )`} + if file.codeSignOnCopy { + var settingsCopy = settings ?? [:] + var attributes = settingsCopy["ATTRIBUTES"] as? [String] ?? [] + attributes.append("CodeSignOnCopy") + settingsCopy["ATTRIBUTES"] = attributes + settings = settingsCopy + } + if buildFilesCache.contains(filePath) == false { - let pbxBuildFile = PBXBuildFile(file: fileReference) + let pbxBuildFile = PBXBuildFile(file: fileReference, settings: settings) + pbxBuildFile.applyPlatformFilters(file.condition?.platformFilters) pbxBuildFiles.append(pbxBuildFile) buildFilesCache.insert(filePath) } @@ -405,6 +434,49 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { } } + private func generateCopySwiftMacroExecutableScriptBuildPhase( + directSwiftMacroExecutables: [GraphDependencyReference], + pbxTarget: PBXTarget, + pbxproj: PBXProj + ) throws { + if directSwiftMacroExecutables.isEmpty { return } + + let copySwiftMacrosBuildPhase = PBXShellScriptBuildPhase(name: "Copy Swift Macro executable into $BUILT_PRODUCT_DIR") + + let executableNames = directSwiftMacroExecutables.compactMap { + switch $0 { + case let .product(_, productName, _): + return productName + default: + return nil + } + } + + 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 = executableNames.map { executable in + "$BUILD_DIR/Debug-$EFFECTIVE_PLATFORM_NAME/\(executable)" + } + + pbxproj.add(object: copySwiftMacrosBuildPhase) + pbxTarget.buildPhases.append(copySwiftMacrosBuildPhase) + } + private func generateResourcesBuildFile( target: Target, files: [ResourceFileElement], diff --git a/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift b/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift index 6fb40f61fbb..1792a0430ff 100644 --- a/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift +++ b/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift @@ -1,5 +1,5 @@ import Foundation -import TuistGraph +import XcodeGraph import XcodeProj protocol BuildRulesGenerating: AnyObject { diff --git a/Sources/TuistGenerator/Generator/ConfigGenerator.swift b/Sources/TuistGenerator/Generator/ConfigGenerator.swift index 0dcf2abfff7..3ef7c072672 100644 --- a/Sources/TuistGenerator/Generator/ConfigGenerator.swift +++ b/Sources/TuistGenerator/Generator/ConfigGenerator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj protocol ConfigGenerating: AnyObject { @@ -28,16 +28,13 @@ protocol ConfigGenerating: AnyObject { final class ConfigGenerator: ConfigGenerating { // MARK: - Attributes - private let fileGenerator: FileGenerating private let defaultSettingsProvider: DefaultSettingsProviding // MARK: - Init init( - fileGenerator: FileGenerating = FileGenerator(), defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider() ) { - self.fileGenerator = fileGenerator self.defaultSettingsProvider = defaultSettingsProvider } @@ -185,6 +182,16 @@ final class ConfigGenerator: ConfigGenerating { settingsHelper.extend(buildSettings: &settings, with: target.settings?.baseDebug ?? [:]) } settingsHelper.extend(buildSettings: &settings, with: configuration?.settings ?? [:]) + settingsHelper + .extend( + buildSettings: &settings, + with: swiftMacrosDerivedSettings( + target: target, + graphTraverser: graphTraverser, + projectPath: project.path + ), + inherit: true + ) let variantBuildConfiguration = XCBuildConfiguration( name: buildConfiguration.xcodeValue, @@ -223,10 +230,6 @@ final class ConfigGenerator: ConfigGenerating { settings.merge(deploymentTargetDerivedSettings(target: target)) { $1 } settings .merge(watchTargetDerivedSettings(target: target, graphTraverser: graphTraverser, projectPath: project.path)) { $1 } - settings - .merge(swiftMacrosDerivedSettings(target: target, graphTraverser: graphTraverser, projectPath: project.path)) { - $1 - } } private func generalTargetDerivedSettings( @@ -248,12 +251,16 @@ final class ConfigGenerator: ConfigGenerating { } // Entitlements - if let entitlements = target.entitlements, let path = entitlements.path { - let relativePath = path.relative(to: sourceRootPath).pathString - if project.xcodeProjPath.parentDirectory == sourceRootPath { - settings["CODE_SIGN_ENTITLEMENTS"] = .string(relativePath) - } else { - settings["CODE_SIGN_ENTITLEMENTS"] = .string("$(SRCROOT)/\(relativePath)") + if let entitlements = target.entitlements { + if let path = entitlements.path { + let relativePath = path.relative(to: sourceRootPath).pathString + if project.xcodeProjPath.parentDirectory == sourceRootPath { + settings["CODE_SIGN_ENTITLEMENTS"] = .string(relativePath) + } else { + settings["CODE_SIGN_ENTITLEMENTS"] = .string("$(SRCROOT)/\(relativePath)") + } + } else if case let .variable(configName) = entitlements { + settings["CODE_SIGN_ENTITLEMENTS"] = .string(configName) } } @@ -278,6 +285,20 @@ final class ConfigGenerator: ConfigGenerating { settings["PRODUCT_NAME"] = .string(target.productName) + if target.mergeable { + settings["MERGEABLE_LIBRARY"] = .string("YES") + } + + switch target.mergedBinaryType { + case .disabled: + // When `MERGED_BINARY_TYPE` is disabled, `MERGED_BINARY_TYPE` value should be left empty + break + case .automatic: + settings["MERGED_BINARY_TYPE"] = .string("automatic") + case .manual: + settings["MERGED_BINARY_TYPE"] = .string("manual") + } + return settings } @@ -358,6 +379,22 @@ final class ConfigGenerator: ConfigGenerating { } } + if let initialInstallTags = target.onDemandResourcesTags?.initialInstall, !initialInstallTags.isEmpty { + settings["ON_DEMAND_RESOURCES_INITIAL_INSTALL_TAGS"] = .string( + initialInstallTags.sorted().map { + $0.replacingOccurrences(of: " ", with: "\\ ") + }.joined(separator: " ") + ) + } + + if let prefetchOrder = target.onDemandResourcesTags?.prefetchOrder, !prefetchOrder.isEmpty { + settings["ON_DEMAND_RESOURCES_PREFETCH_ORDER"] = .string( + prefetchOrder.map { + $0.replacingOccurrences(of: " ", with: "\\ ") + }.joined(separator: " ") + ) + } + return settings } diff --git a/Sources/TuistGenerator/Generator/DescriptorGenerator.swift b/Sources/TuistGenerator/Generator/DescriptorGenerator.swift index ae5d873f6ed..9fbffcde43e 100644 --- a/Sources/TuistGenerator/Generator/DescriptorGenerator.swift +++ b/Sources/TuistGenerator/Generator/DescriptorGenerator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// Descriptor Generator /// diff --git a/Sources/TuistGenerator/Generator/FileGenerator.swift b/Sources/TuistGenerator/Generator/FileGenerator.swift deleted file mode 100644 index 9d62c1b5e95..00000000000 --- a/Sources/TuistGenerator/Generator/FileGenerator.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import TSCBasic -import XcodeProj - -protocol FileGenerating: AnyObject { - func generateFile( - path: AbsolutePath, - in group: PBXGroup, - sourceRootPath: AbsolutePath - ) throws -> PBXFileReference -} - -final class FileGenerator: FileGenerating { - func generateFile( - path: AbsolutePath, - in group: PBXGroup, - sourceRootPath: AbsolutePath - ) throws -> PBXFileReference { - try group.addFile(at: path.path, sourceTree: .group, sourceRoot: sourceRootPath.path) - } -} diff --git a/Sources/TuistGenerator/Generator/GeneratedProject.swift b/Sources/TuistGenerator/Generator/GeneratedProject.swift index f08cd316663..dd11386808f 100644 --- a/Sources/TuistGenerator/Generator/GeneratedProject.swift +++ b/Sources/TuistGenerator/Generator/GeneratedProject.swift @@ -1,6 +1,6 @@ import Foundation +import Path import PathKit -import TSCBasic import XcodeProj final class GeneratedProject { diff --git a/Sources/TuistGenerator/Generator/InfoPlistContentProvider.swift b/Sources/TuistGenerator/Generator/InfoPlistContentProvider.swift index c1fd7dd9275..795ecf933a7 100644 --- a/Sources/TuistGenerator/Generator/InfoPlistContentProvider.swift +++ b/Sources/TuistGenerator/Generator/InfoPlistContentProvider.swift @@ -1,6 +1,6 @@ import Foundation import TuistCore -import TuistGraph +import XcodeGraph /// Defines the interface to obtain the content to generate derived Info.plist files for the targets. protocol InfoPlistContentProviding { @@ -242,7 +242,7 @@ final class InfoPlistContentProvider: InfoPlistContentProviding { } private func hostTarget(for target: Target, in project: Project) -> Target? { - project.targets.first { + project.targets.values.first { $0.dependencies.contains(where: { dependency in if case let .target(name, _) = dependency, name == target.name { return true diff --git a/Sources/TuistGenerator/Generator/KnownAssetTagsFetcher.swift b/Sources/TuistGenerator/Generator/KnownAssetTagsFetcher.swift new file mode 100644 index 00000000000..4ba7b00ebb4 --- /dev/null +++ b/Sources/TuistGenerator/Generator/KnownAssetTagsFetcher.swift @@ -0,0 +1,62 @@ +import Foundation +import PathKit +import XcodeGraph + +private struct ContentJson: Decodable { + struct ContentProperties: Decodable { + enum CodingKeys: String, CodingKey { + case onDemandResourceTags = "on-demand-resource-tags" + } + + let onDemandResourceTags: [String] + } + + let properties: ContentProperties +} + +protocol KnownAssetTagsFetching: AnyObject { + func fetch(project: Project) throws -> [String] +} + +final class KnownAssetTagsFetcher: KnownAssetTagsFetching { + func fetch(project: Project) throws -> [String] { + var tags = project.targets.values.map { $0.resources.resources.map(\.tags).flatMap { $0 } }.flatMap { $0 } + + let initialInstallTags = project.targets.values.compactMap { + $0.onDemandResourcesTags?.initialInstall?.compactMap { $0 } + }.flatMap { $0 } + + let prefetchOrderTags = project.targets.values.compactMap { + $0.onDemandResourcesTags?.prefetchOrder?.compactMap { $0 } + }.flatMap { $0 } + + tags.append(contentsOf: initialInstallTags) + tags.append(contentsOf: prefetchOrderTags) + + var assetContentsPaths: Set = [] + let decoder = JSONDecoder() + for target in project.targets.values { + let assetCatalogs = target.resources.resources.filter { $0.path.extension == "xcassets" } + for assetCatalog in assetCatalogs { + guard let children = try? assetCatalog.path.path.recursiveChildren() else { continue } + let contents = children.filter { $0.lastComponent == "Contents.json" } + for content in contents { + assetContentsPaths.insert(content) + } + } + } + + var assetsTags: [String] = [] + for path in assetContentsPaths { + guard let data = try? Data(contentsOf: path.url) else { continue } + guard let attributes = try? decoder.decode(ContentJson.self, from: data) else { continue } + assetsTags.append(contentsOf: attributes.properties.onDemandResourceTags) + } + + tags.append(contentsOf: assetsTags) + + let uniqueTags = Set(tags).sorted() + + return uniqueTags + } +} diff --git a/Sources/TuistGenerator/Generator/LinkGenerator.swift b/Sources/TuistGenerator/Generator/LinkGenerator.swift index ec3749ed557..548768f3882 100644 --- a/Sources/TuistGenerator/Generator/LinkGenerator.swift +++ b/Sources/TuistGenerator/Generator/LinkGenerator.swift @@ -1,9 +1,9 @@ import Foundation +import Path import PathKit -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj enum LinkGeneratorError: FatalError, Equatable { @@ -116,23 +116,6 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ fileElements: fileElements ) - /** - Targets that depend on a Swift Macro have the following dependency graph: - - 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 $(BUILD_DIR)/$(CONFIGURATION)/ExecutableName\#ExecutableName` build setting. - */ - let directSwiftMacroExecutables = graphTraverser.directSwiftMacroExecutables(path: path, name: target.name).sorted() - try generateCopySwiftMacroExecutableScriptBuildPhase( - directSwiftMacroExecutables: directSwiftMacroExecutables, - target: target, - pbxTarget: pbxTarget, - pbxproj: pbxproj, - fileElements: fileElements - ) - try generatePackages( target: target, pbxTarget: pbxTarget, @@ -430,7 +413,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ for dependency in linkableDependencies { switch dependency { - case let .framework(path, _, _, _, _, _, _, _, status, condition): + case let .framework(path, _, _, _, _, _, _, status, condition): try addBuildFile(path, condition: condition, status: status) case let .library(path, _, _, _, condition): try addBuildFile(path, condition: condition) @@ -513,61 +496,6 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ ) } - func generateCopySwiftMacroExecutableScriptBuildPhase( - directSwiftMacroExecutables: [GraphDependencyReference], - target: Target, - pbxTarget: PBXTarget, - pbxproj: PBXProj, - fileElements _: ProjectFileElements - ) throws { - if directSwiftMacroExecutables.isEmpty { return } - - let copySwiftMacrosBuildPhase = PBXShellScriptBuildPhase(name: "Copy Swift Macro executable into $BUILT_PRODUCT_DIR") - - let executableNames = directSwiftMacroExecutables.compactMap { - switch $0 { - case let .product(_, productName, _): - return productName - default: - return nil - } - } - - 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) - } - private func generateDependenciesBuildPhase( dependencies: [GraphDependencyReference], target: Target, @@ -588,7 +516,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ buildFile.applyCondition(condition, applicableTo: target) pbxproj.add(object: buildFile) files.append(buildFile) - case let .framework(path: path, _, _, _, _, _, _, _, _, condition), + case let .framework(path: path, _, _, _, _, _, _, _, condition), let .library(path: path, _, _, _, condition): guard let fileRef = fileElements.file(path: path) else { throw LinkGeneratorError.missingReference(path: path) diff --git a/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift b/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift index 69068b2b662..fc8ed18f60e 100644 --- a/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift +++ b/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj protocol ProjectDescriptorGenerating: AnyObject { @@ -54,6 +54,9 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { /// Generator for the project schemes. let schemeDescriptorsGenerator: SchemeDescriptorsGenerating + /// Fetcher for the project known asset tags associated with on-demand resources. + let knownAssetTagsFetcher: KnownAssetTagsFetching + // MARK: - Init /// Initializes the project generator with its attributes. @@ -62,14 +65,17 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { /// - targetGenerator: Generator for the project targets. /// - configGenerator: Generator for the project configuration. /// - schemeDescriptorsGenerator: Generator for the project schemes. + /// - knownAssetTagsFetcher: Fetcher for the project known asset tags associated with on-demand resources. init( targetGenerator: TargetGenerating = TargetGenerator(), configGenerator: ConfigGenerating = ConfigGenerator(), - schemeDescriptorsGenerator: SchemeDescriptorsGenerating = SchemeDescriptorsGenerator() + schemeDescriptorsGenerator: SchemeDescriptorsGenerating = SchemeDescriptorsGenerator(), + knownAssetTagsFetcher: KnownAssetTagsFetching = KnownAssetTagsFetcher() ) { self.targetGenerator = targetGenerator self.configGenerator = configGenerator self.schemeDescriptorsGenerator = schemeDescriptorsGenerator + self.knownAssetTagsFetcher = knownAssetTagsFetcher } // MARK: - ProjectGenerating @@ -145,6 +151,8 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { graphTraverser: graphTraverser ) + groups.removeEmptyAuxiliaryGroups() + let xcodeProj = XcodeProj(workspace: workspace, pbxproj: pbxproj) return ProjectDescriptor( path: project.path, @@ -196,7 +204,7 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { graphTraverser: GraphTraversing ) throws -> [String: PBXNativeTarget] { var nativeTargets: [String: PBXNativeTarget] = [:] - for target in project.targets { + for target in project.targets.values.sorted() { let nativeTarget = try targetGenerator.generateTarget( target: target, project: project, @@ -213,7 +221,7 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { /// Target dependencies try targetGenerator.generateTargetDependencies( path: project.path, - targets: project.targets, + targets: Array(project.targets.values), nativeTargets: nativeTargets, graphTraverser: graphTraverser ) @@ -269,7 +277,13 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { ) pbxproj.add(object: reference) - try pbxproj.rootGroup()?.children.append(reference) + + if let existingPackageGroup = try pbxproj.rootGroup()?.group(named: "Packages") { + existingPackageGroup.children.append(reference) + } else { + let packageGroup = try pbxproj.rootGroup()?.addGroup(named: "Packages", options: .withoutFolder) + packageGroup?.first?.children.append(reference) + } case let .remote(url: url, requirement: requirement): let packageReference = XCRemoteSwiftPackageReference( @@ -281,18 +295,15 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { } } - pbxProject.packages = packageReferences.sorted { $0.key < $1.key }.map { $1 } + pbxProject.remotePackages = packageReferences.sorted { $0.key < $1.key }.map { $1 } } private func generateAttributes(project: Project) -> [String: Any] { var attributes: [String: Any] = [:] - /// ODR tags - let tags = project.targets.map { $0.resources.map(\.tags).flatMap { $0 } }.flatMap { $0 } - let uniqueTags = Set(tags).sorted() - - if !uniqueTags.isEmpty { - attributes["KnownAssetTags"] = uniqueTags + // On Demand Resources tags + if let knownAssetTags = try? knownAssetTagsFetcher.fetch(project: project), !knownAssetTags.isEmpty { + attributes["KnownAssetTags"] = knownAssetTags } // BuildIndependentTargetsInParallel @@ -303,6 +314,11 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { attributes["ORGANIZATIONNAME"] = organizationName } + /// Class prefix + if let classPrefix = project.classPrefix { + attributes["CLASSPREFIX"] = classPrefix + } + /// Last upgrade check if let lastUpgradeCheck = project.lastUpgradeCheck { attributes["LastUpgradeCheck"] = lastUpgradeCheck.xcodeStringValue diff --git a/Sources/TuistGenerator/Generator/ProjectFileElements.swift b/Sources/TuistGenerator/Generator/ProjectFileElements.swift index 4c3dcb345dc..a73075522e5 100644 --- a/Sources/TuistGenerator/Generator/ProjectFileElements.swift +++ b/Sources/TuistGenerator/Generator/ProjectFileElements.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj public struct GroupFileElement: Hashable { @@ -40,11 +40,16 @@ class ProjectFileElements { var products: [String: PBXFileReference] = [:] var compiled: [AbsolutePath: PBXFileReference] = [:] var knownRegions: Set = Set([]) + private let cacheDirectoriesProvider: CacheDirectoriesProviding // MARK: - Init - init(_ elements: [AbsolutePath: PBXFileElement] = [:]) { + init( + _ elements: [AbsolutePath: PBXFileElement] = [:], + cacheDirectoriesProvider: CacheDirectoriesProviding = CacheDirectoriesProvider() + ) { self.elements = elements + self.cacheDirectoriesProvider = cacheDirectoriesProvider } func generateProjectFiles( @@ -55,7 +60,7 @@ class ProjectFileElements { ) throws { var files = Set() - for target in project.targets { + for target in project.targets.values.sorted() { try files.formUnion(targetFiles(target: target)) } let projectFileElements = projectFiles(project: project) @@ -74,7 +79,7 @@ class ProjectFileElements { ) // Products - let directProducts = project.targets.map { + let directProducts = project.targets.values.map { GraphDependencyReference.product(target: $0.name, productName: $0.productNameWithExtension, condition: nil) } @@ -122,15 +127,11 @@ class ProjectFileElements { // Add the .gpx files if needed. GPS Exchange files must be added to the // project/workspace so that the scheme can correctly reference them. // In case the configuration already contains such file, we should avoid adding it twice - let gpxFiles = project.schemes.compactMap { scheme -> GroupFileElement? in - guard case let .gpxFile(path) = scheme.runAction?.options.simulatedLocation else { - return nil - } - - return GroupFileElement(path: path, group: project.filesGroup) - } + let runActionGPXFiles = gpxFilesForRunAction(in: project.schemes, filesGroup: project.filesGroup) + fileElements.formUnion(runActionGPXFiles) - fileElements.formUnion(gpxFiles) + let testActionGPXFiles = gpxFilesForTestAction(in: project.schemes, filesGroup: project.filesGroup) + fileElements.formUnion(testActionGPXFiles) return fileElements } @@ -168,7 +169,7 @@ class ProjectFileElements { // Elements var elements = Set() elements.formUnion(files.map { GroupFileElement(path: $0, group: target.filesGroup) }) - elements.formUnion(target.resources.map { + elements.formUnion(target.resources.resources.map { GroupFileElement( path: $0.path, group: target.filesGroup, @@ -200,6 +201,7 @@ class ProjectFileElements { } } + // swiftlint:disable:next function_body_length func generate( dependencyReferences: Set, groups: ProjectGroups, @@ -225,7 +227,7 @@ class ProjectFileElements { group: filesGroup, sourceRootPath: sourceRootPath ) - case let .framework(path, _, _, _, _, _, _, _, _, _): + case let .framework(path, _, _, _, _, _, _, _, _): try generatePrecompiledDependency( path, groups: groups, @@ -274,7 +276,8 @@ class ProjectFileElements { sourceRootPath: AbsolutePath ) throws { // Pre-compiled artifact from the cache - if path.pathString.contains(CacheCategory.builds.directoryName) { + let cacheDirectory = cacheDirectoriesProvider.cacheDirectory() + if path.pathString.contains(cacheDirectory.pathString) { guard compiled[path] == nil else { return } @@ -282,7 +285,7 @@ class ProjectFileElements { from: sourceRootPath, fileAbsolutePath: path, name: path.basename, - toGroup: groups.frameworks, + toGroup: groups.cachedFrameworks, pbxproj: pbxproj ) compiled[path] = fileElement @@ -732,4 +735,37 @@ class ProjectFileElements { return nil } } + + /// Finds and returns the gpx files used by the Run Action for all schemes. + func gpxFilesForRunAction(in schemes: [Scheme], filesGroup: ProjectGroup) -> [GroupFileElement] { + let gpxFiles = schemes.compactMap { scheme -> GroupFileElement? in + guard case let .gpxFile(path) = scheme.runAction?.options.simulatedLocation else { + return nil + } + + return GroupFileElement(path: path, group: filesGroup) + } + + return gpxFiles + } + + /// Finds and returns the gpx files used by the Test Action for all schemes. + func gpxFilesForTestAction(in schemes: [Scheme], filesGroup: ProjectGroup) -> [GroupFileElement] { + let gpxFiles = schemes.compactMap { scheme -> [GroupFileElement] in + guard let testAction = scheme.testAction else { return [] } + + let elements = testAction.targets.compactMap { target -> GroupFileElement? in + guard case let .gpxFile(path) = target.simulatedLocation else { + return nil + } + + return GroupFileElement(path: path, group: filesGroup) + } + + return elements + } + .flatMap { $0 } + + return gpxFiles + } } diff --git a/Sources/TuistGenerator/Generator/ProjectGroups.swift b/Sources/TuistGenerator/Generator/ProjectGroups.swift index cc020268a8a..df4f7a337e8 100644 --- a/Sources/TuistGenerator/Generator/ProjectGroups.swift +++ b/Sources/TuistGenerator/Generator/ProjectGroups.swift @@ -1,8 +1,9 @@ import Foundation +import Path import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj enum ProjectGroupsError: FatalError, Equatable { @@ -29,10 +30,22 @@ class ProjectGroups { @SortedPBXGroup var sortedMain: PBXGroup let products: PBXGroup let frameworks: PBXGroup + let cachedFrameworks: PBXGroup private let pbxproj: PBXProj private let projectGroups: [String: PBXGroup] + private var auxiliaryGroups: [PBXGroup] { + // List of groups Xcode doesn't have by default if empty + // + // Note: The`Products` group is always included in the pbxproj but Xcode + // hides it in the UI if its empty. + [ + frameworks, + cachedFrameworks, + ] + } + // MARK: - Init private init( @@ -40,12 +53,14 @@ class ProjectGroups { projectGroups: [(name: String, group: PBXGroup)], products: PBXGroup, frameworks: PBXGroup, + cachedFrameworks: PBXGroup, pbxproj: PBXProj ) { sortedMain = main self.projectGroups = Dictionary(uniqueKeysWithValues: projectGroups) self.products = products self.frameworks = frameworks + self.cachedFrameworks = cachedFrameworks self.pbxproj = pbxproj } @@ -64,6 +79,13 @@ class ProjectGroups { return group } + func removeEmptyAuxiliaryGroups() { + for emptyGroup in auxiliaryGroups.filter(\.children.isEmpty) { + sortedMain.children.removeAll(where: { $0 == emptyGroup }) + pbxproj.delete(object: emptyGroup) + } + } + static func generate( project: Project, pbxproj: PBXProj @@ -93,27 +115,36 @@ class ProjectGroups { projectGroups.append((item, projectGroup)) } + /// Products + /// If the products group is the last non-empty group, it will not appear. + /// This appears to be an Xcode bug that is still there as of Xcode 15.3 + /// https://developer.apple.com/forums/thread/77406 + let productsGroup = PBXGroup(children: [], sourceTree: .group, name: "Products") + pbxproj.add(object: productsGroup) + mainGroup.children.append(productsGroup) + /// 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") - pbxproj.add(object: productsGroup) - mainGroup.children.append(productsGroup) + /// Cached frameworks + let cacheGroup = PBXGroup(children: [], sourceTree: .group, name: "Cache") + pbxproj.add(object: cacheGroup) + mainGroup.children.append(cacheGroup) return ProjectGroups( main: mainGroup, projectGroups: projectGroups, products: productsGroup, frameworks: frameworksGroup, + cachedFrameworks: cacheGroup, pbxproj: pbxproj ) } private static func extractProjectGroupNames(from project: Project) -> [String] { - let groups = [project.filesGroup] + project.targets.map(\.filesGroup) + let groups = [project.filesGroup] + project.targets.values.map(\.filesGroup) let groupNames: [String] = groups.compactMap { switch $0 { case let .group(name: groupName): diff --git a/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift b/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift index 9e8817842c2..497b89b2e8b 100644 --- a/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift +++ b/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj /// Protocol that defines the interface of the schemes generation. @@ -107,20 +107,6 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { } } - /// Wipes shared and user schemes at a workspace or project path. This is needed - /// currently to support the workspace scheme generation case where a workspace that - /// already exists on disk is being regenerated. Wiping the schemes directory prevents - /// older custom schemes from persisting after regeneration. - /// - /// - Parameter at: Path to the workspace or project. - func wipeSchemes(at path: AbsolutePath) throws { - let fileHandler = FileHandler.shared - let userPath = try schemeDirectory(path: path, shared: false) - let sharedPath = try schemeDirectory(path: path, shared: true) - if fileHandler.exists(userPath) { try fileHandler.delete(userPath) } - if fileHandler.exists(sharedPath) { try fileHandler.delete(sharedPath) } - } - // swiftlint:disable function_body_length /// Generate schemes for a project or workspace. /// @@ -135,7 +121,7 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { path: AbsolutePath, graphTraverser: GraphTraversing, generatedProjects: [AbsolutePath: GeneratedProject], - lastUpgradeCheck: Version? + lastUpgradeCheck: XcodeGraph.Version? ) throws -> SchemeDescriptor { let generatedBuildAction = try schemeBuildAction( scheme: scheme, @@ -312,11 +298,29 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { else { continue } + + var locationScenarioReference: XCScheme.LocationScenarioReference? + + if let locationScenario = testableTarget.simulatedLocation { + var identifier = locationScenario.identifier + + if case let .gpxFile(gpxPath) = locationScenario { + let fileRelativePath = gpxPath.relative(to: graphTraverser.workspace.xcWorkspacePath) + identifier = fileRelativePath.pathString + } + + locationScenarioReference = .init( + identifier: identifier, + referenceType: locationScenario.referenceType + ) + } + let testable = XCScheme.TestableReference( skipped: testableTarget.isSkipped, parallelizable: testableTarget.isParallelizable, randomExecutionOrdering: testableTarget.isRandomExecutionOrdering, buildableReference: reference, + locationScenarioReference: locationScenarioReference, skippedTests: skippedTests ) testables.append(testable) @@ -373,13 +377,13 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { let onlyGenerateCoverageForSpecifiedTargets = codeCoverageTargets.count > 0 ? true : nil - let enableAddressSanitizer = testAction.diagnosticsOptions.contains(.enableAddressSanitizer) + let enableAddressSanitizer = testAction.diagnosticsOptions.addressSanitizerEnabled var enableASanStackUseAfterReturn = false if enableAddressSanitizer { - enableASanStackUseAfterReturn = testAction.diagnosticsOptions.contains(.enableASanStackUseAfterReturn) + enableASanStackUseAfterReturn = testAction.diagnosticsOptions.detectStackUseAfterReturnEnabled } - let enableThreadSanitizer = testAction.diagnosticsOptions.contains(.enableThreadSanitizer) - let disableMainThreadChecker = !testAction.diagnosticsOptions.contains(.mainThreadChecker) + let enableThreadSanitizer = testAction.diagnosticsOptions.threadSanitizerEnabled + let disableMainThreadChecker = !testAction.diagnosticsOptions.mainThreadCheckerEnabled let shouldUseLaunchSchemeArgsEnv: Bool = args == nil && environments == nil let language = testAction.language let region = testAction.region @@ -506,15 +510,15 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { } let buildConfiguration = scheme.runAction?.configurationName ?? defaultBuildConfiguration - let enableAddressSanitizer = scheme.runAction?.diagnosticsOptions.contains(.enableAddressSanitizer) ?? false + let enableAddressSanitizer = scheme.runAction?.diagnosticsOptions.addressSanitizerEnabled ?? false var enableASanStackUseAfterReturn = false if enableAddressSanitizer == true { - enableASanStackUseAfterReturn = scheme.runAction?.diagnosticsOptions.contains(.enableASanStackUseAfterReturn) ?? false + enableASanStackUseAfterReturn = scheme.runAction?.diagnosticsOptions.detectStackUseAfterReturnEnabled ?? false } - let enableThreadSanitizer = scheme.runAction?.diagnosticsOptions.contains(.enableThreadSanitizer) ?? false - let disableMainThreadChecker = scheme.runAction?.diagnosticsOptions.contains(.mainThreadChecker) == false + let enableThreadSanitizer = scheme.runAction?.diagnosticsOptions.threadSanitizerEnabled ?? false + let disableMainThreadChecker = scheme.runAction?.diagnosticsOptions.mainThreadCheckerEnabled == false let disablePerformanceAntipatternChecker = scheme.runAction?.diagnosticsOptions - .contains(.performanceAntipatternChecker) == false + .performanceAntipatternCheckerEnabled == false let launchActionConstants: Constants.LaunchAction let launcherIdentifier: String @@ -748,10 +752,12 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { rootPath: AbsolutePath, generatedProjects: [AbsolutePath: GeneratedProject] ) throws -> XCScheme.ArchiveAction? { - guard let target = defaultTargetReference(scheme: scheme), - let graphTarget = graphTraverser.target(path: target.projectPath, name: target.name) else { return nil } - guard let archiveAction = scheme.archiveAction else { + guard let target = defaultTargetReference(scheme: scheme), + let graphTarget = graphTraverser.target(path: target.projectPath, name: target.name) + else { + return nil + } return defaultSchemeArchiveAction(for: graphTarget.project) } @@ -879,31 +885,6 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { ) } - /// Creates the directory where the schemes are stored inside the project. - /// If the directory exists it does not re-create it. - /// - /// - Parameters: - /// - path: Path to the Xcode workspace or project. - /// - shared: Scheme should be shared or not - /// - Returns: Path to the schemes directory. - /// - Throws: A FatalError if the creation of the directory fails. - private func createSchemesDirectory(path: AbsolutePath, shared: Bool = true) throws -> AbsolutePath { - let schemePath = try schemeDirectory(path: path, shared: shared) - if !FileHandler.shared.exists(schemePath) { - try FileHandler.shared.createFolder(schemePath) - } - return schemePath - } - - private func schemeDirectory(path: AbsolutePath, shared: Bool = true) throws -> AbsolutePath { - if shared { - return path.appending(try RelativePath(validating: "xcshareddata/xcschemes")) - } else { - let username = NSUserName() - return path.appending(try RelativePath(validating: "xcuserdata/\(username).xcuserdatad/xcschemes")) - } - } - /// Returns the scheme commandline argument passed on launch /// /// - Parameters: @@ -997,7 +978,7 @@ extension TestAction { expandVariableFromTarget: nil, preActions: [], postActions: [], - diagnosticsOptions: [], + diagnosticsOptions: SchemeDiagnosticsOptions(), language: nil, region: nil, preferredScreenCaptureFormat: nil, diff --git a/Sources/TuistGenerator/Generator/TargetGenerator.swift b/Sources/TuistGenerator/Generator/TargetGenerator.swift index 2d3fe4c160c..159074d9495 100644 --- a/Sources/TuistGenerator/Generator/TargetGenerator.swift +++ b/Sources/TuistGenerator/Generator/TargetGenerator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj protocol TargetGenerating: AnyObject { @@ -31,20 +31,17 @@ final class TargetGenerator: TargetGenerating { let configGenerator: ConfigGenerating let buildPhaseGenerator: BuildPhaseGenerating let linkGenerator: LinkGenerating - let fileGenerator: FileGenerating let buildRulesGenerator: BuildRulesGenerating // MARK: - Init init( configGenerator: ConfigGenerating = ConfigGenerator(), - fileGenerator: FileGenerating = FileGenerator(), buildPhaseGenerator: BuildPhaseGenerating = BuildPhaseGenerator(), linkGenerator: LinkGenerating = LinkGenerator(), buildRulesGenerator: BuildRulesGenerating = BuildRulesGenerator() ) { self.configGenerator = configGenerator - self.fileGenerator = fileGenerator self.buildPhaseGenerator = buildPhaseGenerator self.linkGenerator = linkGenerator self.buildRulesGenerator = buildRulesGenerator diff --git a/Sources/TuistGenerator/Generator/WorkspaceDescriptorGenerator.swift b/Sources/TuistGenerator/Generator/WorkspaceDescriptorGenerator.swift index 350ef92222a..771e4e9e61d 100644 --- a/Sources/TuistGenerator/Generator/WorkspaceDescriptorGenerator.swift +++ b/Sources/TuistGenerator/Generator/WorkspaceDescriptorGenerator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XcodeProj @@ -238,6 +238,7 @@ final class WorkspaceDescriptorGenerator: WorkspaceDescriptorGenerating { ) return .group(groupReference) + case let .virtualGroup(name, contents): return .group(.init(location: .container(""), name: name, children: try contents.map { try recursiveChildElement( @@ -246,6 +247,7 @@ final class WorkspaceDescriptorGenerator: WorkspaceDescriptorGenerating { path: path ) }.sorted(by: workspaceDataElementSort))) + case let .project(path: projectPath): guard generatedProjects[projectPath] != nil else { throw WorkspaceDescriptorGeneratorError.projectNotFound(path: projectPath) diff --git a/Sources/TuistGenerator/Generator/WorkspaceSettingsDescriptorGenerator.swift b/Sources/TuistGenerator/Generator/WorkspaceSettingsDescriptorGenerator.swift index c61f6b57066..21c2040ec3e 100644 --- a/Sources/TuistGenerator/Generator/WorkspaceSettingsDescriptorGenerator.swift +++ b/Sources/TuistGenerator/Generator/WorkspaceSettingsDescriptorGenerator.swift @@ -1,7 +1,7 @@ import Foundation import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj /// Protocol that defines the interface of the workspace settings generation. diff --git a/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift b/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift index e146d6661be..512c2fccb6d 100644 --- a/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift +++ b/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph struct WorkspaceStructure { enum Element: Equatable { @@ -130,8 +130,13 @@ private class DirectoryStructure { private func isDependencyProject(_ node: Node) -> Bool { switch node { - case let .project(path): return path.pathString.contains(".build/checkouts") - case .directory, .file, .folderReference, .virtualGroup: return false + case let .project(path): + return path.pathString + .contains( + "\(Constants.SwiftPackageManager.packageBuildDirectoryName)/\(Constants.DerivedDirectory.dependenciesDerivedDirectory)" + ) + case .directory, .file, .folderReference, .virtualGroup: + return false } } diff --git a/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift b/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift index 71568c2e023..1b655ab6499 100644 --- a/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift +++ b/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift @@ -1,8 +1,8 @@ import Foundation import GraphViz import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// Interface that describes a mapper that converts a project graph into a GraphViz graph. public protocol GraphToGraphVizMapping { @@ -13,7 +13,7 @@ public protocol GraphToGraphVizMapping { /// - targetsAndDependencies: Targets to be converted into a GraphViz.Graph. /// - Returns: The GraphViz.Graph representation. func map( - graph: TuistGraph.Graph, + graph: XcodeGraph.Graph, targetsAndDependencies: [GraphTarget: Set] ) -> GraphViz.Graph } @@ -28,7 +28,7 @@ public final class GraphToGraphVizMapper: GraphToGraphVizMapping { /// - targetsAndDependencies: Targets to be converted into a GraphViz.Graph. /// - Returns: The GraphViz.Graph representation public func map( - graph: TuistGraph.Graph, + graph: XcodeGraph.Graph, targetsAndDependencies: [GraphTarget: Set] ) -> GraphViz.Graph { var nodes: [GraphViz.Node] = [] @@ -76,7 +76,6 @@ extension GraphDependency { bcsymbolmapPaths: _, linking: _, architectures: _, - isCarthage: _, status: _ ): return path.basenameWithoutExt diff --git a/Sources/TuistGenerator/GraphViz/NodeStyling.swift b/Sources/TuistGenerator/GraphViz/NodeStyling.swift index a26ad4f2edc..6b25c8266be 100644 --- a/Sources/TuistGenerator/GraphViz/NodeStyling.swift +++ b/Sources/TuistGenerator/GraphViz/NodeStyling.swift @@ -1,7 +1,7 @@ import Foundation import GraphViz import TuistCore -import TuistGraph +import XcodeGraph extension GraphViz.Node { mutating func applyAttributes(attributes: NodeStyleAttributes?) { diff --git a/Sources/TuistGenerator/Linter/EnvironmentLinter.swift b/Sources/TuistGenerator/Linter/EnvironmentLinter.swift index c8ed1fca05a..670d9f99868 100644 --- a/Sources/TuistGenerator/Linter/EnvironmentLinter.swift +++ b/Sources/TuistGenerator/Linter/EnvironmentLinter.swift @@ -1,7 +1,6 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport public protocol EnvironmentLinting { diff --git a/Sources/TuistGenerator/Linter/GraphLinter.swift b/Sources/TuistGenerator/Linter/GraphLinter.swift index 7b19aab3034..dc4a3c37cb0 100644 --- a/Sources/TuistGenerator/Linter/GraphLinter.swift +++ b/Sources/TuistGenerator/Linter/GraphLinter.swift @@ -1,8 +1,8 @@ import Foundation import struct TSCUtility.Version import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public protocol GraphLinting: AnyObject { func lint(graphTraverser: GraphTraversing, config: Config) -> [LintingIssue] @@ -45,11 +45,39 @@ public class GraphLinter: GraphLinting { issues.append(contentsOf: lintMismatchingConfigurations(graphTraverser: graphTraverser)) issues.append(contentsOf: lintWatchBundleIndentifiers(graphTraverser: graphTraverser)) issues.append(contentsOf: lintCodeCoverageMode(graphTraverser: graphTraverser)) + issues.append(contentsOf: lintSchemesUnknownTargets(graphTraverser: graphTraverser)) return issues } // MARK: - Fileprivate + private func lintSchemesUnknownTargets(graphTraverser: GraphTraversing) -> [LintingIssue] { + let targets = graphTraverser.targets() + return graphTraverser.schemes().compactMap { scheme in + let unknownTargets = scheme + .targetDependencies() + .filter { targetReference in + if let target = targets[targetReference.projectPath], + target.keys.contains(targetReference.name) + { + return false + } + return true + } + + guard !unknownTargets.isEmpty else { return nil } + + let targetsDescriptionStrings = unknownTargets.map { target in + target.name + " " + "(\(target.projectPath.relative(to: graphTraverser.path).pathString))" + } + + return LintingIssue( + reason: "Cannot find targets \(targetsDescriptionStrings.joined(separator: ", ")) defined in \(scheme.name)", + severity: .warning + ) + } + } + private func lintCodeCoverageMode(graphTraverser: GraphTraversing) -> [LintingIssue] { switch graphTraverser.workspace.generationOptions.autogeneratedWorkspaceSchemes.codeCoverageMode { case .disabled, .all: return [] @@ -89,6 +117,62 @@ public class GraphLinter: GraphLinting { private func lintDependencies(graphTraverser: GraphTraversing, config: Config) -> [LintingIssue] { var issues: [LintingIssue] = [] + + issues.append(contentsOf: lintDependencyRelationships(graphTraverser: graphTraverser)) + issues.append(contentsOf: lintLinkableDependencies(graphTraverser: graphTraverser)) + issues.append(contentsOf: staticProductsLinter.lint(graphTraverser: graphTraverser, config: config)) + issues.append(contentsOf: lintPrecompiledFrameworkDependencies(graphTraverser: graphTraverser)) + issues.append(contentsOf: lintPackageDependencies(graphTraverser: graphTraverser)) + issues.append(contentsOf: lintAppClip(graphTraverser: graphTraverser)) + + return issues + } + + private func lintLinkableDependencies(graphTraverser: GraphTraversing) -> [LintingIssue] { + let linkableProducts: Set = [ + .framework, + .staticFramework, + .staticLibrary, + .dynamicLibrary, + ] + + let dependencyIssues = graphTraverser.dependencies.flatMap { fromDependency, _ -> [LintingIssue] in + guard case let GraphDependency.target(fromTargetName, fromTargetPath) = fromDependency, + let fromTarget = graphTraverser.target(path: fromTargetPath, name: fromTargetName) else { return [] } + + let fromPlatforms = fromTarget.target.supportedPlatforms + + let dependencies: [LintingIssue] = graphTraverser.directTargetDependencies(path: fromTargetPath, name: fromTargetName) + .flatMap { dependentTarget in + guard linkableProducts.contains(dependentTarget.target.product) else { return [LintingIssue]() } + + var requiredPlatforms = fromPlatforms + + if let condition = dependentTarget.condition { + requiredPlatforms.formIntersection(Set(condition.platformFilters.compactMap(\.platform))) + } + + let platformsSupportedByDependency = dependentTarget.target.supportedPlatforms + let unaccountedPlatforms = requiredPlatforms.subtracting(platformsSupportedByDependency) + + if !unaccountedPlatforms.isEmpty { + let missingPlatforms = unaccountedPlatforms.map(\.rawValue).joined(separator: ", ") + return [LintingIssue( + reason: "Target \(fromTargetName) which depends on \(dependentTarget.target.name) does not support the required platforms: \(missingPlatforms). The dependency on \(dependentTarget.target.name) must have a dependency condition constraining to at most: \(platformsSupportedByDependency.map(\.rawValue).joined(separator: ", ")).", + severity: .error + )] + } else { + return [LintingIssue]() + } + } + + return dependencies + } + + return dependencyIssues + } + + private func lintDependencyRelationships(graphTraverser: GraphTraversing) -> [LintingIssue] { let dependencyIssues = graphTraverser.dependencies.flatMap { fromDependency, toDependencies -> [LintingIssue] in toDependencies.flatMap { toDependency -> [LintingIssue] in guard case let GraphDependency.target(fromTargetName, fromTargetPath) = fromDependency else { return [] } @@ -98,14 +182,7 @@ public class GraphLinter: GraphLinting { return lintDependency(from: fromTarget, to: toTarget) } } - - issues.append(contentsOf: dependencyIssues) - issues.append(contentsOf: staticProductsLinter.lint(graphTraverser: graphTraverser, config: config)) - issues.append(contentsOf: lintPrecompiledFrameworkDependencies(graphTraverser: graphTraverser)) - issues.append(contentsOf: lintPackageDependencies(graphTraverser: graphTraverser)) - issues.append(contentsOf: lintAppClip(graphTraverser: graphTraverser)) - - return issues + return dependencyIssues } private func lintDependency(from: GraphTarget, to: GraphTarget) -> [LintingIssue] { @@ -321,7 +398,7 @@ public class GraphLinter: GraphLinting { } struct LintableTarget: Equatable, Hashable { - let platform: TuistGraph.Platform + let platform: XcodeGraph.Platform let product: Product } @@ -378,6 +455,7 @@ public class GraphLinter: GraphLinting { LintableTarget(platform: .iOS, product: .bundle), LintableTarget(platform: .iOS, product: .appClip), LintableTarget(platform: .macOS, product: .macro), + LintableTarget(platform: .iOS, product: .appExtension), ], LintableTarget(platform: .iOS, product: .uiTests): [ LintableTarget(platform: .iOS, product: .app), @@ -394,6 +472,7 @@ public class GraphLinter: GraphLinting { LintableTarget(platform: .iOS, product: .dynamicLibrary), LintableTarget(platform: .iOS, product: .staticFramework), LintableTarget(platform: .iOS, product: .framework), + LintableTarget(platform: .iOS, product: .bundle), LintableTarget(platform: .macOS, product: .macro), ], LintableTarget(platform: .iOS, product: .appClip): [ @@ -401,6 +480,7 @@ public class GraphLinter: GraphLinting { LintableTarget(platform: .iOS, product: .dynamicLibrary), LintableTarget(platform: .iOS, product: .framework), LintableTarget(platform: .iOS, product: .staticFramework), + LintableTarget(platform: .iOS, product: .appExtension), LintableTarget(platform: .macOS, product: .macro), ], LintableTarget(platform: .iOS, product: .extensionKitExtension): [ diff --git a/Sources/TuistGenerator/Linter/PackageLinter.swift b/Sources/TuistGenerator/Linter/PackageLinter.swift index 81c2ab06409..b00999ce227 100644 --- a/Sources/TuistGenerator/Linter/PackageLinter.swift +++ b/Sources/TuistGenerator/Linter/PackageLinter.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph protocol PackageLinting: AnyObject { func lint(_ package: Package) -> [LintingIssue] diff --git a/Sources/TuistGenerator/Linter/ProjectLinter.swift b/Sources/TuistGenerator/Linter/ProjectLinter.swift index ad3cca9f9df..234c5887164 100644 --- a/Sources/TuistGenerator/Linter/ProjectLinter.swift +++ b/Sources/TuistGenerator/Linter/ProjectLinter.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph protocol ProjectLinting: AnyObject { func lint(_ project: Project) -> [LintingIssue] @@ -48,25 +48,8 @@ class ProjectLinter: ProjectLinting { } private func lintTargets(project: Project) -> [LintingIssue] { - var issues: [LintingIssue] = [] - issues.append(contentsOf: project.targets.flatMap(targetLinter.lint)) - issues.append(contentsOf: lintNotDuplicatedTargets(project: project)) - return issues - } - - private func lintNotDuplicatedTargets(project: Project) -> [LintingIssue] { - var issues: [LintingIssue] = [] - let duplicatedTargets = project.targets.map(\.name) - .reduce(into: [String: Int]()) { $0[$1] = ($0[$1] ?? 0) + 1 } - .filter { $0.value > 1 } - .keys - if !duplicatedTargets.isEmpty { - let issue = LintingIssue( - reason: "Targets \(duplicatedTargets.joined(separator: ", ")) from project at \(project.path.pathString) have duplicates.", - severity: .error - ) - issues.append(issue) + return project.targets.values.flatMap { target in + targetLinter.lint(target: target, options: project.options) } - return issues } } diff --git a/Sources/TuistGenerator/Linter/SchemeLinter.swift b/Sources/TuistGenerator/Linter/SchemeLinter.swift index a16d2412c84..a5290dcc91e 100644 --- a/Sources/TuistGenerator/Linter/SchemeLinter.swift +++ b/Sources/TuistGenerator/Linter/SchemeLinter.swift @@ -1,7 +1,7 @@ import Foundation import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph protocol SchemeLinting { func lint(project: Project) -> [LintingIssue] @@ -11,8 +11,8 @@ class SchemeLinter: SchemeLinting { func lint(project: Project) -> [LintingIssue] { var issues = [LintingIssue]() issues.append(contentsOf: lintReferencedBuildConfigurations(schemes: project.schemes, settings: project.settings)) - issues.append(contentsOf: lintCodeCoverageTargets(schemes: project.schemes, targets: project.targets)) - issues.append(contentsOf: lintExpandVariableTarget(schemes: project.schemes, targets: project.targets)) + issues.append(contentsOf: lintCodeCoverageTargets(schemes: project.schemes, targets: Array(project.targets.values))) + issues.append(contentsOf: lintExpandVariableTarget(schemes: project.schemes, targets: Array(project.targets.values))) issues.append(contentsOf: projectSchemeCantReferenceRemoteTargets(schemes: project.schemes, project: project)) return issues } @@ -149,13 +149,11 @@ extension SchemeLinter { private func projectSchemeCantReferenceRemoteTargets(scheme: Scheme, project: Project) -> [LintingIssue] { var issues: [LintingIssue] = [] - for targetDependency in scheme.targetDependencies() { - if targetDependency.projectPath != project.path { - issues.append(.init( - 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 - )) - } + for targetDependency in scheme.targetDependencies() where targetDependency.projectPath != project.path { + issues.append(.init( + 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 + )) } return issues diff --git a/Sources/TuistGenerator/Linter/SettingsLinter.swift b/Sources/TuistGenerator/Linter/SettingsLinter.swift index 2f1bb234094..7a80359dfe8 100644 --- a/Sources/TuistGenerator/Linter/SettingsLinter.swift +++ b/Sources/TuistGenerator/Linter/SettingsLinter.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph protocol SettingsLinting: AnyObject { func lint(project: Project) -> [LintingIssue] diff --git a/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift b/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift index 421460d3e37..572d7c15f4f 100644 --- a/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift +++ b/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph /// Static Products Graph Linter /// @@ -91,6 +91,10 @@ class StaticProductsGraphLinter: StaticProductsGraphLinting { let dependencyTarget = graphTraverser.target(path: targetPath, name: targetName), dependencyTarget.target.canLinkStaticProducts() else { + cache.cache( + results: results, + for: dependency + ) return results } @@ -160,7 +164,7 @@ class StaticProductsGraphLinter: StaticProductsGraphLinting { switch dependency { case let .xcframework(xcframework): return xcframework.linking == .static - case let .framework(_, _, _, _, linking, _, _, _): + case let .framework(_, _, _, _, linking, _, _): return linking == .static case let .library(_, _, linking, _, _): return linking == .static diff --git a/Sources/TuistGenerator/Linter/TargetLinter.swift b/Sources/TuistGenerator/Linter/TargetLinter.swift index f1011b96415..ab4d6cf30de 100644 --- a/Sources/TuistGenerator/Linter/TargetLinter.swift +++ b/Sources/TuistGenerator/Linter/TargetLinter.swift @@ -1,10 +1,10 @@ import Foundation import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph protocol TargetLinting: AnyObject { - func lint(target: Target) -> [LintingIssue] + func lint(target: Target, options: Project.Options) -> [LintingIssue] } class TargetLinter: TargetLinting { @@ -25,14 +25,15 @@ class TargetLinter: TargetLinting { // MARK: - TargetLinting - func lint(target: Target) -> [LintingIssue] { + func lint(target: Target, options: Project.Options) -> [LintingIssue] { var issues: [LintingIssue] = [] issues.append(contentsOf: lintProductName(target: target)) + issues.append(contentsOf: lintProductNameBuildSettings(target: target)) issues.append(contentsOf: lintValidPlatformProductCombinations(target: target)) issues.append(contentsOf: lintBundleIdentifier(target: target)) issues.append(contentsOf: lintHasSourceFiles(target: target)) issues.append(contentsOf: lintCopiedFiles(target: target)) - issues.append(contentsOf: lintLibraryHasNoResources(target: target)) + issues.append(contentsOf: lintLibraryHasNoResources(target: target, options: options)) issues.append(contentsOf: lintDeploymentTarget(target: target)) issues.append(contentsOf: settingsLinter.lint(target: target)) issues.append(contentsOf: lintDuplicateDependency(target: target)) @@ -40,6 +41,7 @@ class TargetLinter: TargetLinting { issues.append(contentsOf: validateCoreDataModelsExist(target: target)) issues.append(contentsOf: validateCoreDataModelVersionsExist(target: target)) issues.append(contentsOf: lintMergeableLibrariesOnlyAppliesToDynamicTargets(target: target)) + issues.append(contentsOf: lintOnDemandResourcesTags(target: target)) for script in target.scripts { issues.append(contentsOf: targetScriptLinter.lint(script)) } @@ -60,7 +62,7 @@ class TargetLinter: TargetLinting { bundleIdentifier = bundleIdentifier.replacingOccurrences(of: "\\$\\(.+\\)", with: "", options: .regularExpression) var allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - allowed.formUnion(CharacterSet(charactersIn: "-.")) + allowed.formUnion(CharacterSet(charactersIn: "-./")) if !bundleIdentifier.unicodeScalars.allSatisfy({ allowed.contains($0) }) { let reason = @@ -74,14 +76,14 @@ class TargetLinter: TargetLinting { private func lintProductName(target: Target) -> [LintingIssue] { var allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") - let allowsDot = target.product == .app || target.product == .commandLineTool - if allowsDot { - allowed.formUnion(CharacterSet(charactersIn: ".")) + let allowsExtraCharacters = target.product != .framework && target.product != .staticFramework + if allowsExtraCharacters { + allowed.formUnion(CharacterSet(charactersIn: ".-")) } if target.productName.unicodeScalars.allSatisfy(allowed.contains) == false { let reason = - "Invalid product name '\(target.productName)'. This string must contain only alphanumeric (A-Z,a-z,0-9)\(allowsDot ? ", period (.)" : ""), and underscore (_) characters." + "Invalid product name '\(target.productName)'. This string must contain only alphanumeric (A-Z,a-z,0-9)\(allowsExtraCharacters ? ", period (.), hyphen (-)" : ""), and underscore (_) characters." return [LintingIssue(reason: reason, severity: .warning)] } @@ -89,6 +91,36 @@ class TargetLinter: TargetLinting { return [] } + private func lintProductNameBuildSettings(target: Target) -> [LintingIssue] { + var settingsProductNames: Set = Set() + var issues: [LintingIssue] = [] + + if let value = target.settings?.base["PRODUCT_NAME"], case let SettingValue.string(baseProductName) = value { + settingsProductNames.insert(baseProductName) + } + target.settings?.configurations.values.forEach { configuration in + if let value = configuration?.settings["PRODUCT_NAME"], + case let SettingValue.string(configurationProductName) = value + { + settingsProductNames.insert(configurationProductName) + } + } + if settingsProductNames.count > 1 { + issues.append(.init( + reason: "The target '\(target.name)' has a PRODUCT_NAME build setting that is different across configurations and might cause unpredictable behaviours.", + severity: .warning + )) + } + if settingsProductNames.contains(where: { $0.contains("$") }) { + issues.append(.init( + reason: "The target '\(target.name)' has a PRODUCT_NAME build setting containing variables that are resolved at build time, and might cause unpredictable behaviours.", + severity: .warning + )) + } + + return issues + } + private func lintHasSourceFiles(target: Target) -> [LintingIssue] { let supportsSources = target.supportsSources let sources = target.sources @@ -117,7 +149,7 @@ class TargetLinter: TargetLinting { private func lintCopiedFiles(target: Target) -> [LintingIssue] { var issues: [LintingIssue] = [] - let files = target.resources.map(\.path) + let files = target.resources.resources.map(\.path) let entitlements = files.filter { $0.pathString.contains(".entitlements") } if let targetInfoPlistPath = target.infoPlist?.path, files.contains(targetInfoPlistPath) { @@ -188,15 +220,15 @@ class TargetLinter: TargetLinting { return issues } - private func lintLibraryHasNoResources(target: Target) -> [LintingIssue] { + private func lintLibraryHasNoResources(target: Target, options: Project.Options) -> [LintingIssue] { if target.supportsResources { return [] } - if target.resources.isEmpty == false { + if target.resources.resources.isEmpty == false, options.disableBundleAccessors { return [ LintingIssue( - reason: "Target \(target.name) cannot contain resources. \(target.product) targets do not support resources", + reason: "Target \(target.name) cannot contain resources. For \(target.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", severity: .error ), ] @@ -212,7 +244,7 @@ class TargetLinter: TargetLinting { let osVersionRegex = "\\b[0-9]+\\.[0-9]+(?:\\.[0-9]+)?\\b" if !version.matches(pattern: osVersionRegex) { return [versionFormatIssue] } - let destinations = target.destinations + let destinations = target.destinations.sorted(by: { $0.rawValue < $1.rawValue }) let inconsistentPlatformIssue = LintingIssue( reason: "Found an inconsistency between target destinations `\(destinations)` and deployment target `\(platform.caseValue)`", severity: .error @@ -223,7 +255,10 @@ class TargetLinter: TargetLinting { case .macOS: if !target.supports(.macOS) { return [inconsistentPlatformIssue] } case .watchOS: if !target.supports(.watchOS) { return [inconsistentPlatformIssue] } case .tvOS: if !target.supports(.tvOS) { return [inconsistentPlatformIssue] } - case .visionOS: if !target.supports(.visionOS) { return [inconsistentPlatformIssue] } + case .visionOS: + if !target.supports(.visionOS), !target.destinations.contains(.appleVisionWithiPadDesign) { + return [inconsistentPlatformIssue] + } } return [] } @@ -289,6 +324,19 @@ class TargetLinter: TargetLinting { } return [] } + + private func lintOnDemandResourcesTags(target: Target) -> [LintingIssue] { + guard let onDemandResourcesTags = target.onDemandResourcesTags else { return [] } + guard let initialInstall = onDemandResourcesTags.initialInstall else { return [] } + guard let prefetchOrder = onDemandResourcesTags.prefetchOrder else { return [] } + let intersection = Set(initialInstall).intersection(Set(prefetchOrder)) + return intersection.map { tag in + LintingIssue( + reason: "Prefetched Order Tag \"\(tag)\" is already assigned to Initial Install Tags category for the target \(target.name) and will be ignored by Xcode", + severity: .warning + ) + } + } } extension TargetDependency { diff --git a/Sources/TuistGenerator/Linter/TargetScriptLinter.swift b/Sources/TuistGenerator/Linter/TargetScriptLinter.swift index 6ffab86f6e4..988fc3392d4 100644 --- a/Sources/TuistGenerator/Linter/TargetScriptLinter.swift +++ b/Sources/TuistGenerator/Linter/TargetScriptLinter.swift @@ -1,7 +1,7 @@ import Foundation import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// Protocol that defines the interface of a linter for target scripts. protocol TargetScriptLinting { diff --git a/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift b/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift index d724a62b39f..9f16040a950 100644 --- a/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph /// A project mapper that auto-generates schemes for each of the targets of the `Project` /// if the user hasn't already defined schemes for those. @@ -14,13 +14,15 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl // swiftlint:disable:next function_body_length public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { + logger.debug("Transforming project \(project.name): Auto-generating project schemes") + let userDefinedSchemes = project.schemes let userDefinedSchemeNames = Set(project.schemes.map(\.name)) var buildTargets: Set = [] var testTargets: Set = [] var runTargets: Set = [] - for target in project.targets { + for target in project.targets.values.sorted() { switch target.product { case .app, .appClip, .commandLineTool, .watch2App, .xpc: runTargets.insert(target) @@ -106,7 +108,9 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl } let filteredAutogeneratedSchemes = autogeneratedSchemes.filter { !userDefinedSchemeNames.contains($0.name) } - return (project.with(schemes: (userDefinedSchemes + filteredAutogeneratedSchemes).sorted { $0.name < $1.name }), []) + var project = project + project.schemes = (userDefinedSchemes + filteredAutogeneratedSchemes).sorted { $0.name < $1.name } + return (project, []) } // MARK: - Private @@ -152,7 +156,10 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl language: project.options.runLanguage, region: project.options.runRegion ), - diagnosticsOptions: [.mainThreadChecker, .performanceAntipatternChecker] + diagnosticsOptions: SchemeDiagnosticsOptions( + mainThreadCheckerEnabled: true, + performanceAntipatternCheckerEnabled: true + ) ) } @@ -201,7 +208,7 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl expandVariableFromTarget: nil, preActions: [], postActions: [], - diagnosticsOptions: [.mainThreadChecker], + diagnosticsOptions: SchemeDiagnosticsOptions(mainThreadCheckerEnabled: true), language: project.options.testLanguage, region: project.options.testRegion, preferredScreenCaptureFormat: project.options.testScreenCaptureFormat @@ -312,6 +319,7 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl private func hostAppTargetReference(for target: Target, project: Project) -> Target? { project.targets + .values .filter { $0.product.canHostTests() && $0.dependencies.contains(where: { dependency in if case let .target(name, _) = dependency, name == target.name { return true @@ -325,6 +333,7 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl private func hostWatchAppTargetReference(for target: Target, project: Project) -> Target? { project.targets + .values .filter { $0.product == .watch2App && $0.dependencies.contains(where: { dependency in if case let .target(name, _) = dependency, name == target.name { return true diff --git a/Sources/TuistGenerator/Mappers/AutogeneratedWorkspaceSchemeWorkspaceMapper.swift b/Sources/TuistGenerator/Mappers/AutogeneratedWorkspaceSchemeWorkspaceMapper.swift index 99f9bd3f067..647eb16212b 100644 --- a/Sources/TuistGenerator/Mappers/AutogeneratedWorkspaceSchemeWorkspaceMapper.swift +++ b/Sources/TuistGenerator/Mappers/AutogeneratedWorkspaceSchemeWorkspaceMapper.swift @@ -1,6 +1,6 @@ import Foundation import TuistCore -import TuistGraph +import XcodeGraph /// Mapper that generates a new scheme `ProjectName-Workspace` that includes all targets from a given workspace public final class AutogeneratedWorkspaceSchemeWorkspaceMapper: WorkspaceMapping { // swiftlint:disable:this type_name @@ -18,35 +18,25 @@ public final class AutogeneratedWorkspaceSchemeWorkspaceMapper: WorkspaceMapping else { return (workspace, []) } + logger.debug("Transforming workspace \(workspace.workspace.name): Auto-generating workspace scheme") let platforms = Set( workspace.projects .flatMap { - $0.targets.flatMap(\.destinations.platforms) + $0.targets.values.flatMap(\.supportedPlatforms) } ) let schemes: [Scheme] - if platforms.count == 1, let platform = platforms.first { - schemes = [ - scheme( - name: "\(workspace.workspace.name)-Workspace", - platform: platform, - project: project, - workspace: workspace - ), - ] - } else { - schemes = platforms.map { platform in - scheme( - name: "\(workspace.workspace.name)-Workspace-\(platform.caseValue)", - platform: platform, - project: project, - workspace: workspace - ) - } - } + schemes = [ + scheme( + name: "\(workspace.workspace.name)-Workspace", + platforms: platforms, + project: project, + workspace: workspace + ), + ] var workspace = workspace workspace.workspace.schemes.append(contentsOf: schemes) @@ -57,7 +47,7 @@ public final class AutogeneratedWorkspaceSchemeWorkspaceMapper: WorkspaceMapping private func scheme( name: String, - platform: Platform, + platforms: Set, project: Project, workspace: WorkspaceWithProjects ) -> Scheme { @@ -65,10 +55,12 @@ public final class AutogeneratedWorkspaceSchemeWorkspaceMapper: WorkspaceMapping var (targets, testableTargets): ([TargetReference], [TestableTarget]) = workspace.projects .reduce(([], [])) { result, project in let targets = project.targets - .filter { $0.supports(platform) } + .values + .filter { !$0.supportedPlatforms.isDisjoint(with: platforms) } .map { TargetReference(projectPath: project.path, name: $0.name) } let testableTargets = project.targets - .filter { $0.supports(platform) } + .values + .filter { !$0.supportedPlatforms.isDisjoint(with: platforms) } .filter(\.product.testsBundle) .map { TargetReference(projectPath: project.path, name: $0.name) } .map { @@ -101,7 +93,10 @@ public final class AutogeneratedWorkspaceSchemeWorkspaceMapper: WorkspaceMapping expandVariableFromTarget: nil, preActions: [], postActions: [], - diagnosticsOptions: [.mainThreadChecker, .performanceAntipatternChecker], + diagnosticsOptions: SchemeDiagnosticsOptions( + mainThreadCheckerEnabled: true, + performanceAntipatternCheckerEnabled: true + ), language: workspace.workspace.generationOptions.autogeneratedWorkspaceSchemes.testLanguage, region: workspace.workspace.generationOptions.autogeneratedWorkspaceSchemes.testRegion, preferredScreenCaptureFormat: workspace.workspace.generationOptions.autogeneratedWorkspaceSchemes diff --git a/Sources/TuistGenerator/Mappers/DeleteDerivedDirectoryProjectMapper.swift b/Sources/TuistGenerator/Mappers/DeleteDerivedDirectoryProjectMapper.swift index ea8cbfd8d6e..806cd7d4b67 100644 --- a/Sources/TuistGenerator/Mappers/DeleteDerivedDirectoryProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/DeleteDerivedDirectoryProjectMapper.swift @@ -1,25 +1,42 @@ import Foundation import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// A project mapper that returns side effects to delete the derived directory. public final class DeleteDerivedDirectoryProjectMapper: ProjectMapping { private let derivedDirectoryName: String + private let fileHandler: FileHandling - public init(derivedDirectoryName: String = Constants.DerivedDirectory.name) { + public init( + derivedDirectoryName: String = Constants.DerivedDirectory.name, + fileHandler: FileHandling = FileHandler.shared + ) { self.derivedDirectoryName = derivedDirectoryName + self.fileHandler = fileHandler } // MARK: - ProjectMapping public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { - logger.debug("Determining the /Derived directories that should be deleted within \(project.path)") + logger.debug("Transforming project \(project.name): Deleting /Derived directory") + let derivedDirectoryPath = project.path.appending(component: derivedDirectoryName) - let directoryDescriptor = DirectoryDescriptor(path: derivedDirectoryPath, state: .absent) - return (project, [ - .directory(directoryDescriptor), - ]) + if !fileHandler.exists(derivedDirectoryPath) { + return (project, []) + } + + let sideEffects: [SideEffectDescriptor] = try fileHandler.contentsOfDirectory(derivedDirectoryPath) + .filter { $0.extension != "modulemap" } + .map { + if fileHandler.isFolder($0) { + return .directory(DirectoryDescriptor(path: $0, state: .absent)) + } else { + return .file(FileDescriptor(path: $0, state: .absent)) + } + } + + return (project, sideEffects) } } diff --git a/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift b/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift index 0b5d1d4eaf4..62317fd6c61 100644 --- a/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift +++ b/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift @@ -1,20 +1,25 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -/// A target mapper that enforces explicit dependneices by adding custom build directories +/// A target mapper that enforces explicit dependencies by adding custom build directories public struct ExplicitDependencyGraphMapper: GraphMapping { public init() {} - public func map(graph: Graph) async throws -> (Graph, [SideEffectDescriptor]) { + public func map( + graph: Graph, + environment: MapperEnvironment + ) async throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { if !graph.packages.isEmpty { return ( graph, - [] + [], + environment ) } + logger.debug("Transforming graph \(graph.name): Enforcing explicit dependencies") let graphTraverser = GraphTraverser(graph: graph) @@ -22,7 +27,7 @@ public struct ExplicitDependencyGraphMapper: GraphMapping { graph.projects = Dictionary(uniqueKeysWithValues: graph.projects.map { projectPath, project in var project = project - project.targets = project.targets.map { target in + project.targets = project.targets.mapValues { target in let graphTarget = GraphTarget(path: projectPath, target: target, project: project) let projectDebugConfigurations = project.settings.configurations.keys .filter { $0.variant == .debug } @@ -40,9 +45,10 @@ public struct ExplicitDependencyGraphMapper: GraphMapping { return (projectPath, project) }) - return (graph, []) + return (graph, [], environment) } + // swiftlint:disable:next function_body_length private func map(_ graphTarget: GraphTarget, graphTraverser: GraphTraversing, debugConfigurations: [String]) -> Target { let allTargetDependencies = graphTraverser.allTargetDependencies( path: graphTarget.path, @@ -71,11 +77,17 @@ public struct ExplicitDependencyGraphMapper: GraphMapping { additionalSettings["FRAMEWORK_SEARCH_PATHS"] = .array(frameworkSearchPaths) } + // If any dependency of this target has "ENABLE_TESTING_SEARCH_PATHS" set to "YES", it needs to be propagated + // on the upstream target as well + if graphTraverser.needsEnableTestingSearchPaths(path: graphTarget.path, name: graphTarget.target.name) { + additionalSettings["ENABLE_TESTING_SEARCH_PATHS"] = .string("YES") + } + var target = graphTarget.target target.settings = Settings( base: target.settings?.base ?? [:], baseDebug: additionalSettings, - configurations: [:] + configurations: target.settings?.configurations ?? [:] ) let copyBuiltProductsScript: String @@ -146,7 +158,6 @@ public struct ExplicitDependencyGraphMapper: GraphMapping { ), ] ) - return target } @@ -154,7 +165,7 @@ public struct ExplicitDependencyGraphMapper: GraphMapping { debugConfigurations: [String], extensionName: String, prefix: String = "" - ) -> (String, [String], [String]) { + ) -> (String, [String], [String]) { // swiftlint:disable:this large_tuple let script = debugConfigurations.map { copyScript(for: $0, extensionName: extensionName, prefix: prefix) } diff --git a/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift b/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift index c925a3436f6..070bdf7ae76 100644 --- a/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj /// A project mapper that generates derived entitlements files for targets that define it as a dictonary. @@ -21,14 +21,18 @@ public final class GenerateEntitlementsProjectMapper: ProjectMapping { // MARK: - ProjectMapping public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { - let results = try project.targets - .reduce(into: (targets: [Target](), sideEffects: [SideEffectDescriptor]())) { results, target in + logger.debug("Transforming project \(project.name): Synthesizing entitlement files'") + + let results = try project.targets.values + .reduce(into: (targets: [String: Target](), sideEffects: [SideEffectDescriptor]())) { results, target in let (updatedTarget, sideEffects) = try map(target: target, project: project) - results.targets.append(updatedTarget) + results.targets[updatedTarget.name] = updatedTarget results.sideEffects.append(contentsOf: sideEffects) } + var project = project + project.targets = results.targets - return (project.with(targets: results.targets), results.sideEffects) + return (project, results.sideEffects) } // MARK: - Private diff --git a/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift b/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift index 1aaaa5b826a..fa305ca867d 100644 --- a/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj /// A project mapper that generates derived Info.plist files for targets that define it as a dictonary. @@ -35,14 +35,18 @@ public final class GenerateInfoPlistProjectMapper: ProjectMapping { // MARK: - ProjectMapping public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { - let results = try project.targets - .reduce(into: (targets: [Target](), sideEffects: [SideEffectDescriptor]())) { results, target in + logger.debug("Transforming project \(project.name): Synthesizing Info.plist") + + let results = try project.targets.values + .reduce(into: (targets: [String: Target](), sideEffects: [SideEffectDescriptor]())) { results, target in let (updatedTarget, sideEffects) = try map(target: target, project: project) - results.targets.append(updatedTarget) + results.targets[updatedTarget.name] = updatedTarget results.sideEffects.append(contentsOf: sideEffects) } + var project = project + project.targets = results.targets - return (project.with(targets: results.targets), results.sideEffects) + return (project, results.sideEffects) } // MARK: - Private @@ -68,8 +72,7 @@ public final class GenerateInfoPlistProjectMapper: ProjectMapping { options: 0 ) - let infoPlistPath = project.path - .appending(component: derivedDirectoryName) + let infoPlistPath = project.derivedDirectoryPath(for: target) .appending(component: infoPlistsDirectoryName) .appending(component: "\(target.name)-Info.plist") let sideEffect = SideEffectDescriptor.file(FileDescriptor(path: infoPlistPath, contents: data)) diff --git a/Sources/TuistGenerator/Mappers/GeneratePrivacyManifestProjectMapper.swift b/Sources/TuistGenerator/Mappers/GeneratePrivacyManifestProjectMapper.swift new file mode 100644 index 00000000000..19c3f0a4ef6 --- /dev/null +++ b/Sources/TuistGenerator/Mappers/GeneratePrivacyManifestProjectMapper.swift @@ -0,0 +1,64 @@ +import Foundation +import Path +import TuistCore +import TuistSupport +import XcodeGraph +import XcodeProj + +/// A project mapper that generates derived privacyManifest files for targets that define it as a dictonary. +public final class GeneratePrivacyManifestProjectMapper: ProjectMapping { + public init() {} + + // MARK: - ProjectMapping + + public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { + logger.debug("Transforming project \(project.name): Synthesizing privacy manifest files'") + + let results = try project.targets.values + .reduce(into: (targets: [String: Target](), sideEffects: [SideEffectDescriptor]())) { results, target in + let (updatedTarget, sideEffects) = try map(target: target, project: project) + results.targets[updatedTarget.name] = updatedTarget + results.sideEffects.append(contentsOf: sideEffects) + } + var project = project + project.targets = results.targets + + return (project, results.sideEffects) + } + + // MARK: - Private + + private func map(target: Target, project: Project) throws -> (Target, [SideEffectDescriptor]) { + guard let privacyManifest = target.resources.privacyManifest else { + return (target, []) + } + + let dictionary: [String: Any] = [ + "NSPrivacyTracking": privacyManifest.tracking, + "NSPrivacyTrackingDomains": privacyManifest.trackingDomains, + "NSPrivacyCollectedDataTypes": privacyManifest.collectedDataTypes.map { $0.mapValues { $0.value } }, + "NSPrivacyAccessedAPITypes": privacyManifest.accessedApiTypes.map { $0.mapValues { $0.value } }, + ] + + let data = try PropertyListSerialization.data( + fromPropertyList: dictionary, + format: .xml, + options: 0 + ) + + let privacyManifestPath = project.path + .appending(component: Constants.DerivedDirectory.name) + .appending(component: Constants.DerivedDirectory.privacyManifest) + .appending(component: target.name) + .appending(component: "PrivacyInfo.xcprivacy") + let sideEffect = SideEffectDescriptor.file(FileDescriptor(path: privacyManifestPath, contents: data)) + + var resources = target.resources + resources.resources.append(.init(path: privacyManifestPath)) + + var newTarget = target + newTarget.resources = resources + + return (newTarget, [sideEffect]) + } +} diff --git a/Sources/TuistGenerator/Mappers/IDETemplateMacrosMapper.swift b/Sources/TuistGenerator/Mappers/IDETemplateMacrosMapper.swift index 322b39ae5db..c2ca8499aa6 100644 --- a/Sources/TuistGenerator/Mappers/IDETemplateMacrosMapper.swift +++ b/Sources/TuistGenerator/Mappers/IDETemplateMacrosMapper.swift @@ -1,17 +1,19 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph public final class IDETemplateMacrosMapper: ProjectMapping, WorkspaceMapping { public init() {} public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { - (project, try sideEffects(for: project.ideTemplateMacros, to: project.xcodeProjPath)) + logger.debug("Transforming project \(project.name): Generating xcshareddata/IDETemplateMacros.plist") + return (project, try sideEffects(for: project.ideTemplateMacros, to: project.xcodeProjPath)) } public func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) { - (workspace, try sideEffects(for: workspace.workspace.ideTemplateMacros, to: workspace.workspace.xcWorkspacePath)) + logger.debug("Transforming workspace \(workspace.workspace.name): Generating xcshareddata/IDETemplateMacros.plist") + return (workspace, try sideEffects(for: workspace.workspace.ideTemplateMacros, to: workspace.workspace.xcWorkspacePath)) } private func sideEffects( diff --git a/Sources/TuistGenerator/Mappers/LastUpgradeVersionWorkspaceMapper.swift b/Sources/TuistGenerator/Mappers/LastUpgradeVersionWorkspaceMapper.swift index 481bd1d9f06..429cbfb2c0f 100644 --- a/Sources/TuistGenerator/Mappers/LastUpgradeVersionWorkspaceMapper.swift +++ b/Sources/TuistGenerator/Mappers/LastUpgradeVersionWorkspaceMapper.swift @@ -11,6 +11,7 @@ public final class LastUpgradeVersionWorkspaceMapper: WorkspaceMapping { guard let lastXcodeUpgradeCheck = workspace.workspace.generationOptions.lastXcodeUpgradeCheck else { return (workspace, []) } + logger.debug("Transforming workspace \(workspace.workspace.name): Updating projects' last upgrade check") var projects = workspace.projects projects.indices.forEach { projects[$0].lastUpgradeCheck = projects[$0].lastUpgradeCheck ?? lastXcodeUpgradeCheck } diff --git a/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift b/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift index bec658bbc95..0f00eefffe2 100644 --- a/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift +++ b/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift @@ -1,89 +1,58 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport - -enum ModuleMapMapperError: FatalError { - case invalidTargetDependency(sourceProject: AbsolutePath, sourceTarget: String, dependentTarget: String) - case invalidProjectTargetDependency( - sourceProject: AbsolutePath, - sourceTarget: String, - dependentProject: AbsolutePath, - dependentTarget: String - ) - - /// Error type. - var type: ErrorType { - switch self { - case .invalidTargetDependency, .invalidProjectTargetDependency: return .abort - } - } - - /// Error description. - var description: String { - switch self { - case let .invalidTargetDependency(sourceProject, sourceTarget, dependentTarget): - return """ - Target '\(sourceTarget)' of the project at path '\(sourceProject.pathString)' \ - depends on a target '\(dependentTarget)' that can't be found. \ - Please make sure your project configuration is correct. - """ - case let .invalidProjectTargetDependency(sourceProject, sourceTarget, dependentProject, dependentTarget): - return """ - Target '\(sourceTarget)' of the project at path '\(sourceProject.pathString)' \ - depends on a target '\(dependentTarget)' of the project at path '\( - dependentProject - .pathString - )' that can't be found. \ - Please make sure your project configuration is correct. - """ - } - } -} +import XcodeGraph /// Mapper that maps the `MODULE_MAP` build setting to the `-fmodule-map-file` compiler flags. /// It is required to avoid embedding the module map into the frameworks during cache operations, which would make the framework -/// not portable, as -/// the modulemap could contain absolute paths. -public final class ModuleMapMapper: WorkspaceMapping { +/// not portable, as the modulemap could contain absolute paths. +public final class ModuleMapMapper: GraphMapping { // swiftlint:disable:this type_body_length private static let modulemapFileSetting = "MODULEMAP_FILE" private static let otherCFlagsSetting = "OTHER_CFLAGS" private static let otherSwiftFlagsSetting = "OTHER_SWIFT_FLAGS" + private static let headerSearchPaths = "HEADER_SEARCH_PATHS" private struct TargetID: Hashable { let projectPath: AbsolutePath let targetName: String } + private struct DependencyMetadata: Hashable { + let moduleMapPath: AbsolutePath? + let headerSearchPaths: [String] + } + public init() {} // swiftlint:disable function_body_length - public func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) { - let (projectsByPath, targetsByName) = Self.makeProjectsByPathWithTargetsByName(workspace: workspace) - var targetToModuleMaps: [TargetID: Set] = [:] - for project in workspace.projects { - for target in project.targets { - try Self.dependenciesModuleMaps( - workspace: workspace, - project: project, - target: target, - targetToModuleMaps: &targetToModuleMaps, - projectsByPath: projectsByPath, - targetsByName: targetsByName - ) - } + public func map(graph: Graph, environment: MapperEnvironment) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { + logger + .debug( + "Transforming graph \(graph.name): Mapping MODULE_MAP build setting to -fmodule-map-file compiler flag" + ) + + var targetToDependenciesMetadata: [TargetID: Set] = [:] + let graphTraverser = GraphTraverser(graph: graph) + for target in graphTraverser.allTargets() { + try Self.dependenciesModuleMaps( + graph: graph, + target: target, + targetToDependenciesMetadata: &targetToDependenciesMetadata + ) } - var mappedWorkspace = workspace - for projectIndex in 0 ..< workspace.projects.count { - var mappedProject = workspace.projects[projectIndex] - for targetIndex in 0 ..< mappedProject.targets.count { - var mappedTarget = mappedProject.targets[targetIndex] - let targetID = TargetID(projectPath: mappedProject.path, targetName: mappedTarget.name) - var mappedSettingsDictionary = mappedTarget.settings?.base ?? [:] + var graph = graph + + graph.projects = Dictionary(uniqueKeysWithValues: graph.projects.map { projectPath, project in + var project = project + project.targets = Dictionary(uniqueKeysWithValues: project.targets.map { targetName, target in + var target = target + let targetID = TargetID(projectPath: project.path, targetName: target.name) + var mappedSettingsDictionary = target.settings?.base ?? [:] let hasModuleMap = mappedSettingsDictionary[Self.modulemapFileSetting] != nil - guard hasModuleMap || !(targetToModuleMaps[targetID]?.isEmpty ?? true) else { continue } + guard hasModuleMap || !(targetToDependenciesMetadata[targetID]?.isEmpty ?? true) + else { return (targetName, target) } if hasModuleMap { mappedSettingsDictionary[Self.modulemapFileSetting] = nil @@ -92,7 +61,7 @@ public final class ModuleMapMapper: WorkspaceMapping { if let updatedOtherSwiftFlags = Self.updatedOtherSwiftFlags( targetID: targetID, oldOtherSwiftFlags: mappedSettingsDictionary[Self.otherSwiftFlagsSetting], - targetToModuleMaps: targetToModuleMaps + targetToDependenciesMetadata: targetToDependenciesMetadata ) { mappedSettingsDictionary[Self.otherSwiftFlagsSetting] = updatedOtherSwiftFlags } @@ -100,22 +69,32 @@ public final class ModuleMapMapper: WorkspaceMapping { if let updatedOtherCFlags = Self.updatedOtherCFlags( targetID: targetID, oldOtherCFlags: mappedSettingsDictionary[Self.otherCFlagsSetting], - targetToModuleMaps: targetToModuleMaps + targetToDependenciesMetadata: targetToDependenciesMetadata ) { mappedSettingsDictionary[Self.otherCFlagsSetting] = updatedOtherCFlags } - let targetSettings = mappedTarget.settings ?? Settings( + if let updatedHeaderSearchPaths = Self.updatedHeaderSearchPaths( + targetID: targetID, + oldHeaderSearchPaths: mappedSettingsDictionary[Self.headerSearchPaths], + targetToDependenciesMetadata: targetToDependenciesMetadata + ) { + mappedSettingsDictionary[Self.headerSearchPaths] = updatedHeaderSearchPaths + } + + let targetSettings = target.settings ?? Settings( base: [:], configurations: [:], - defaultSettings: mappedProject.settings.defaultSettings + defaultSettings: project.settings.defaultSettings ) - mappedTarget.settings = targetSettings.with(base: mappedSettingsDictionary) - mappedProject.targets[targetIndex] = mappedTarget - } - mappedWorkspace.projects[projectIndex] = mappedProject - } - return (mappedWorkspace, []) + target.settings = targetSettings.with(base: mappedSettingsDictionary) + + return (target.name, target) + }) + + return (projectPath, project) + }) + return (graph, [], environment) } // swiftlint:enable function_body_length private static func makeProjectsByPathWithTargetsByName(workspace: WorkspaceWithProjects) @@ -125,99 +104,127 @@ public final class ModuleMapMapper: WorkspaceMapping { var targetsByName = [String: Target]() for project in workspace.projects { projectsByPath[project.path] = project - for target in project.targets { + for target in project.targets.values { targetsByName[target.name] = target } } return (projectsByPath, targetsByName) } - /// Calculates the set of module maps to be linked to a given target and populates the `targetToModuleMaps` dictionary. + /// Calculates the set of module maps to be linked to a given target and populates the `targetToDependenciesMetadata` + /// dictionary. /// Each target must link the module map of its direct and indirect dependencies. - /// The `targetToModuleMaps` is also used as cache to avoid recomputing the set for already computed targets. + /// The `targetToDependenciesMetadata` is also used as cache to avoid recomputing the set for already computed targets. private static func dependenciesModuleMaps( // swiftlint:disable:this function_body_length - workspace: WorkspaceWithProjects, - project: Project, - target: Target, - targetToModuleMaps: inout [TargetID: Set], - projectsByPath: [AbsolutePath: Project], - targetsByName: [String: Target] + graph: Graph, + target: GraphTarget, + targetToDependenciesMetadata: inout [TargetID: Set] ) throws { - let targetID = TargetID(projectPath: project.path, targetName: target.name) - if targetToModuleMaps[targetID] != nil { + let targetID = TargetID(projectPath: target.path, targetName: target.target.name) + if targetToDependenciesMetadata[targetID] != nil { // already computed return } - var dependenciesModuleMaps: Set = [] - for dependency in target.dependencies { - let dependentProject: Project - let dependentTarget: Target - switch dependency { - case let .target(name, _): - guard let dependentTargetFromName = targetsByName[name] else { - throw ModuleMapMapperError.invalidTargetDependency( - sourceProject: project.path, - sourceTarget: target.name, - dependentTarget: name - ) - } - dependentProject = project - dependentTarget = dependentTargetFromName - case let .project(name, path, _): - guard let dependentProjectFromPath = projectsByPath[path], - let dependentTargetFromName = targetsByName[name] - else { - throw ModuleMapMapperError.invalidProjectTargetDependency( - sourceProject: project.path, - sourceTarget: target.name, - dependentProject: path, - dependentTarget: name - ) - } - dependentProject = dependentProjectFromPath - dependentTarget = dependentTargetFromName - case .framework, .xcframework, .library, .package, .sdk, .xctest: - continue - } + let graphTraverser = GraphTraverser(graph: graph) + var dependenciesMetadata: Set = [] + for dependency in graphTraverser.directTargetDependencies(path: target.path, name: target.target.name) { try Self.dependenciesModuleMaps( - workspace: workspace, - project: dependentProject, - target: dependentTarget, - targetToModuleMaps: &targetToModuleMaps, - projectsByPath: projectsByPath, - targetsByName: targetsByName + graph: graph, + target: dependency.graphTarget, + targetToDependenciesMetadata: &targetToDependenciesMetadata ) // direct dependency module map - if case let .string(dependencyModuleMap) = dependentTarget.settings?.base[Self.modulemapFileSetting] { - let pathString = dependentProject.path.pathString - let dependencyModuleMapPath = try AbsolutePath( + let dependencyModuleMapPath: AbsolutePath? + + if case let .string(dependencyModuleMap) = dependency.target.settings?.base[Self.modulemapFileSetting] { + let pathString = dependency.graphTarget.path.pathString + dependencyModuleMapPath = try AbsolutePath( validating: dependencyModuleMap .replacingOccurrences(of: "$(PROJECT_DIR)", with: pathString) .replacingOccurrences(of: "$(SRCROOT)", with: pathString) .replacingOccurrences(of: "$(SOURCE_ROOT)", with: pathString) ) - dependenciesModuleMaps.insert(dependencyModuleMapPath) + } else { + dependencyModuleMapPath = nil + } + + var headerSearchPaths: [String] + switch dependency.target.settings?.base[Self.headerSearchPaths] ?? .array([]) { + case let .array(values): + headerSearchPaths = values + case let .string(value): + headerSearchPaths = [value] + } + + headerSearchPaths = headerSearchPaths.map { + let pathString = dependency.graphTarget.path.pathString + return ( + try? AbsolutePath( + validating: $0 + .replacingOccurrences(of: "$(PROJECT_DIR)", with: pathString) + .replacingOccurrences(of: "$(SRCROOT)", with: pathString) + .replacingOccurrences(of: "$(SOURCE_ROOT)", with: pathString) + ).pathString + ) ?? $0 } // indirect dependency module maps - let dependentTargetID = TargetID(projectPath: dependentProject.path, targetName: dependentTarget.name) - if let indirectDependencyModuleMap = targetToModuleMaps[dependentTargetID] { - dependenciesModuleMaps.formUnion(indirectDependencyModuleMap) + let dependentTargetID = TargetID(projectPath: dependency.graphTarget.path, targetName: dependency.target.name) + if let indirectDependencyMetadata = targetToDependenciesMetadata[dependentTargetID] { + dependenciesMetadata.formUnion(indirectDependencyMetadata) } + + dependenciesMetadata.insert( + DependencyMetadata( + moduleMapPath: dependencyModuleMapPath, + headerSearchPaths: headerSearchPaths + ) + ) + } + + targetToDependenciesMetadata[targetID] = dependenciesMetadata + } + + private static func updatedHeaderSearchPaths( + targetID: TargetID, + oldHeaderSearchPaths: SettingsDictionary.Value?, + targetToDependenciesMetadata: [TargetID: Set] + ) -> SettingsDictionary.Value? { + let dependenciesHeaderSearchPaths = Set(targetToDependenciesMetadata[targetID]?.flatMap(\.headerSearchPaths) ?? []) + guard !dependenciesHeaderSearchPaths.isEmpty + else { return nil } + + var mappedHeaderSearchPaths: [String] + switch oldHeaderSearchPaths ?? .array(["$(inherited)"]) { + case let .array(values): + mappedHeaderSearchPaths = values + case let .string(value): + mappedHeaderSearchPaths = value.split(separator: " ").map(String.init) + } + + for headerSearchPath in dependenciesHeaderSearchPaths.sorted() { + mappedHeaderSearchPaths.append( + ( + try? AbsolutePath(validating: headerSearchPath) + .relative(to: targetID.projectPath).pathString + ).map { "$(SRCROOT)/\($0)" } ?? headerSearchPath + ) } - targetToModuleMaps[targetID] = dependenciesModuleMaps + return .array(mappedHeaderSearchPaths) } private static func updatedOtherSwiftFlags( targetID: TargetID, oldOtherSwiftFlags: SettingsDictionary.Value?, - targetToModuleMaps: [TargetID: Set] + targetToDependenciesMetadata: [TargetID: Set] ) -> SettingsDictionary.Value? { - guard let dependenciesModuleMaps = targetToModuleMaps[targetID], !dependenciesModuleMaps.isEmpty else { return nil } + guard let dependenciesModuleMaps = targetToDependenciesMetadata[targetID]?.compactMap(\.moduleMapPath), + !dependenciesModuleMaps.isEmpty + else { return nil } var mappedOtherSwiftFlags: [String] switch oldOtherSwiftFlags ?? .array(["$(inherited)"]) { @@ -240,9 +247,11 @@ public final class ModuleMapMapper: WorkspaceMapping { private static func updatedOtherCFlags( targetID: TargetID, oldOtherCFlags: SettingsDictionary.Value?, - targetToModuleMaps: [TargetID: Set] + targetToDependenciesMetadata: [TargetID: Set] ) -> SettingsDictionary.Value? { - guard let dependenciesModuleMaps = targetToModuleMaps[targetID], !dependenciesModuleMaps.isEmpty else { return nil } + guard let dependenciesModuleMaps = targetToDependenciesMetadata[targetID]?.compactMap(\.moduleMapPath), + !dependenciesModuleMaps.isEmpty + else { return nil } var mappedOtherCFlags: [String] switch oldOtherCFlags ?? .array(["$(inherited)"]) { diff --git a/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift b/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift index 8c4d5370bb9..17e8d88ae98 100644 --- a/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift @@ -1,11 +1,11 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// A project mapper that adds support for defining resources in targets that don't support it -public class ResourcesProjectMapper: ProjectMapping { +public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this type_body_length private let contentHasher: ContentHashing public init(contentHasher: ContentHashing) { self.contentHasher = contentHasher @@ -15,24 +15,32 @@ public class ResourcesProjectMapper: ProjectMapping { guard !project.options.disableBundleAccessors else { return (project, []) } + logger.debug("Transforming project \(project.name): Generating bundles for libraries'") + var sideEffects: [SideEffectDescriptor] = [] - var targets: [Target] = [] + var targets: [String: Target] = [:] - for target in project.targets { + for target in project.targets.values { let (mappedTargets, targetSideEffects) = try mapTarget(target, project: project) - targets.append(contentsOf: mappedTargets) + mappedTargets.forEach { targets[$0.name] = $0 } sideEffects.append(contentsOf: targetSideEffects) } - return (project.with(targets: targets), sideEffects) + var project = project + project.targets = targets + + return (project, sideEffects) } + // swiftlint:disable:next function_body_length public func mapTarget(_ target: Target, project: Project) throws -> ([Target], [SideEffectDescriptor]) { - if target.resources.isEmpty, target.coreDataModels.isEmpty { return ([target], []) } + if target.resources.resources.isEmpty, target.coreDataModels.isEmpty { return ([target], []) } + var additionalTargets: [Target] = [] var sideEffects: [SideEffectDescriptor] = [] - let bundleName = "\(project.name)_\(target.name)" + let sanitizedTargetName = target.name.sanitizedModuleName + let bundleName = "\(project.name)_\(sanitizedTargetName)" var modifiedTarget = target if !target.supportsResources { @@ -40,7 +48,7 @@ public class ResourcesProjectMapper: ProjectMapping { name: bundleName, destinations: target.destinations, product: .bundle, - productName: nil, + productName: bundleName, bundleId: "\(target.bundleId).resources", deploymentTargets: target.deploymentTargets, infoPlist: .extendingDefault(with: [:]), @@ -55,14 +63,16 @@ public class ResourcesProjectMapper: ProjectMapping { coreDataModels: target.coreDataModels, filesGroup: target.filesGroup ) - modifiedTarget.resources = [] + modifiedTarget.resources.resources = [] modifiedTarget.copyFiles = [] modifiedTarget.dependencies.append(.target(name: bundleName, condition: .when(target.dependencyPlatformFilters))) additionalTargets.append(resourcesTarget) } - if target.supportsSources { - let (filePath, data) = synthesizedFile(bundleName: bundleName, target: target, project: project) + if target.supportsSources, + target.sources.containsSwiftFiles + { + let (filePath, data) = synthesizedSwiftFile(bundleName: bundleName, target: target, project: project) let hash = try data.map(contentHasher.hash) let sourceFile = SourceFile(path: filePath, contentHash: hash) @@ -71,103 +81,243 @@ public class ResourcesProjectMapper: ProjectMapping { sideEffects.append(sideEffect) } + if project.isExternal, + target.supportsSources, + target.sources.containsObjcFiles, + target.resources.containsBundleAccessedResources, + !target.supportsResources + { + let (headerFilePath, headerData) = synthesizedObjcHeaderFile(bundleName: bundleName, target: target, project: project) + + let headerHash = try headerData.map(contentHasher.hash) + let headerFile = SourceFile(path: headerFilePath, contentHash: headerHash) + let headerSideEffect = SideEffectDescriptor.file(.init(path: headerFilePath, contents: headerData, state: .present)) + + let gccPrefixHeader = "$(SRCROOT)/\(headerFile.path.relative(to: project.path).pathString)" + var settings = modifiedTarget.settings?.base ?? SettingsDictionary() + settings["GCC_PREFIX_HEADER"] = .string(gccPrefixHeader) + modifiedTarget.settings = modifiedTarget.settings?.with(base: settings) + + sideEffects.append(headerSideEffect) + + let (resourceAccessorPath, resourceAccessorData) = synthesizedObjcImplementationFile( + bundleName: bundleName, + target: target, + project: project + ) + modifiedTarget.sources.append( + SourceFile( + path: resourceAccessorPath, + contentHash: try resourceAccessorData.map(contentHasher.hash) + ) + ) + sideEffects.append( + SideEffectDescriptor.file( + FileDescriptor( + path: resourceAccessorPath, + contents: resourceAccessorData, + state: .present + ) + ) + ) + } + return ([modifiedTarget] + additionalTargets, sideEffects) } - func synthesizedFile(bundleName: String, target: Target, project: Project) -> (AbsolutePath, Data?) { - let filePath = project.path - .appending(component: Constants.DerivedDirectory.name) + func synthesizedSwiftFile(bundleName: String, target: Target, project: Project) -> (AbsolutePath, Data?) { + let filePath = project.derivedDirectoryPath(for: target) .appending(component: Constants.DerivedDirectory.sources) .appending(component: "TuistBundle+\(target.name.toValidSwiftIdentifier()).swift") let content: String = ResourcesProjectMapper.fileContent( targetName: target.name, - bundleName: bundleName.replacingOccurrences(of: "-", with: "_"), - target: target + bundleName: bundleName, + target: target, + in: project + ) + return (filePath, content.data(using: .utf8)) + } + + private func synthesizedObjcHeaderFile(bundleName _: String, target: Target, project: Project) -> (AbsolutePath, Data?) { + let filePath = synthesizedFilePath(target: target, project: project, fileExtension: "h") + + let content: String = ResourcesProjectMapper.objcHeaderFileContent(targetName: target.name) + return (filePath, content.data(using: .utf8)) + } + + private func synthesizedObjcImplementationFile( + bundleName: String, + target: Target, + project: Project + ) -> (AbsolutePath, Data?) { + let filePath = synthesizedFilePath(target: target, project: project, fileExtension: "m") + + let content: String = ResourcesProjectMapper.objcImplementationFileContent( + targetName: target.name, + bundleName: bundleName ) return (filePath, content.data(using: .utf8)) } + private func synthesizedFilePath(target: Target, project: Project, fileExtension: String) -> AbsolutePath { + let filename = "TuistBundle+\(target.name.uppercasingFirst).\(fileExtension)" + return project.derivedDirectoryPath(for: target).appending(components: Constants.DerivedDirectory.sources, filename) + } + // swiftlint:disable:next function_body_length - static func fileContent(targetName: String, bundleName: String, target: Target) -> String { + static func fileContent(targetName _: String, bundleName: String, target: Target, in project: Project) -> String { + var content = """ + // swiftlint:disable all + // swift-format-ignore-file + // swiftformat:disable all + import Foundation + """ if !target.supportsResources { - return """ - // swiftlint:disable all - // swift-format-ignore-file - // swiftformat:disable all - import Foundation - - // MARK: - Swift Bundle Accessor - - private class BundleFinder {} - - extension Foundation.Bundle { - /// Since \(targetName) is a \( - target - .product - ), the bundle containing the resources is copied into the final product. - static let module: Bundle = { - let bundleName = "\(bundleName)" - - let candidates = [ - Bundle.main.resourceURL, - Bundle(for: BundleFinder.self).resourceURL, - Bundle.main.bundleURL, - ] - - for candidate in candidates { - let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") - if let bundle = bundlePath.flatMap(Bundle.init(url:)) { - return bundle - } - } - fatalError("unable to find bundle named \(bundleName)") - }() - } + content += swiftSPMBundleAccessorString(for: target, and: bundleName) + } else { + content += swiftFrameworkBundleAccessorString(for: target) + } - // MARK: - Objective-C Bundle Accessor + // Add public accessors only for non external projects + if !project.isExternal, !target.sourcesContainsPublicResourceClassName { + content += publicBundleAccessorString(for: target) + } - @objc - public class \(target.productName.toValidSwiftIdentifier())Resources: NSObject { - @objc public class var bundle: Bundle { - return .module - } - } - // swiftlint:enable all - // swiftformat:enable all + content += """ + // swiftlint:enable all + // swiftformat:enable all + """ + return content + } - """ - } else { - return """ - // swiftlint:disable all - // swift-format-ignore-file - // swiftformat:disable all - import Foundation - - // MARK: - Swift Bundle Accessor - - private class BundleFinder {} - - extension Foundation.Bundle { - /// Since \(targetName) is a \( - target - .product - ), the bundle for classes within this module can be used directly. - static let module = Bundle(for: BundleFinder.self) - } + static func objcHeaderFileContent( + targetName: String + ) -> String { + return """ + #import + + #if __cplusplus + extern "C" { + #endif + + NSBundle* \(targetName)_SWIFTPM_MODULE_BUNDLE(void); + + #define SWIFTPM_MODULE_BUNDLE \(targetName)_SWIFTPM_MODULE_BUNDLE() + + #if __cplusplus + } + #endif + """ + } + + static func objcImplementationFileContent( + targetName: String, + bundleName: String + ) -> String { + return """ + #import + #import "TuistBundle+\(targetName).h" - // MARK: - Objective-C Bundle Accessor + NSBundle* \(targetName)_SWIFTPM_MODULE_BUNDLE(void) { + NSURL *bundleURL = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"\(bundleName).bundle"]; + + NSBundle *bundle = [NSBundle bundleWithURL:bundleURL]; + + return bundle; + } + """ + } + + private static func publicBundleAccessorString(for target: Target) -> String { + """ + // MARK: - Objective-C Bundle Accessor + @objc + public class \(target.productName.toValidSwiftIdentifier())Resources: NSObject { + @objc public class var bundle: Bundle { + return .module + } + } + """ + } - @objc - public class \(target.productName.toValidSwiftIdentifier())Resources: NSObject { - @objc public class var bundle: Bundle { - return .module + private static func swiftSPMBundleAccessorString(for target: Target, and bundleName: String) -> String { + """ + // MARK: - Swift Bundle Accessor - for SPM + private class BundleFinder {} + extension Foundation.Bundle { + /// Since \(target.name) is a \( + target + .product + ), the bundle containing the resources is copied into the final product. + static let module: Bundle = { + let bundleName = "\(bundleName)" + var candidates = [ + Bundle.main.resourceURL, + Bundle(for: BundleFinder.self).resourceURL, + Bundle.main.bundleURL, + ] + // This is a fix to make Previews work with bundled resources. + // Logic here is taken from SPM's generated `resource_bundle_accessors.swift` file, + // which is located under the derived data directory after building the project. + if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"] { + candidates.append(URL(fileURLWithPath: override)) + // Deleting derived data and not rebuilding the frameworks containing resources may result in a state + // where the bundles are only available in the framework's directory that is actively being previewed. + // Since we don't know which framework this is, we also need to look in all the framework subpaths. + if let subpaths = try? FileManager.default.contentsOfDirectory(atPath: override) { + for subpath in subpaths { + if subpath.hasSuffix(".framework") { + candidates.append(URL(fileURLWithPath: override + "/" + subpath)) + } + } + } } + for candidate in candidates { + let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") + if let bundle = bundlePath.flatMap(Bundle.init(url:)) { + return bundle + } } - // swiftlint:enable all - // swiftformat:enable all + fatalError("unable to find bundle named \(bundleName)") + }() + } + """ + } - """ + private static func swiftFrameworkBundleAccessorString(for target: Target) -> String { + """ + // MARK: - Swift Bundle Accessor for Frameworks + private class BundleFinder {} + extension Foundation.Bundle { + /// Since \(target.name) is a \( + target + .product + ), the bundle for classes within this module can be used directly. + static let module = Bundle(for: BundleFinder.self) } + """ + } +} + +extension [SourceFile] { + fileprivate var containsObjcFiles: Bool { + contains(where: { $0.path.extension == "m" || $0.path.extension == "mm" }) + } + + fileprivate var containsSwiftFiles: Bool { + contains(where: { $0.path.extension == "swift" }) + } +} + +extension ResourceFileElements { + fileprivate var containsBundleAccessedResources: Bool { + !resources.filter { $0.path.extension != "xcprivacy" }.isEmpty + } +} + +extension Target { + fileprivate var sourcesContainsPublicResourceClassName: Bool { + sources.contains(where: { $0.path.basename == "\(name)Resources.swift" }) } } diff --git a/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift b/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift index aad58ab2f99..9852cd95623 100644 --- a/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift @@ -1,9 +1,9 @@ import Foundation +import Path import SwiftGenKit -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph // swiftlint:disable:next type_name enum SynthesizedResourceInterfaceProjectMapperError: FatalError, Equatable { @@ -50,14 +50,16 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { / guard !project.options.disableSynthesizedResourceAccessors else { return (project, []) } + logger.debug("Transforming project \(project.name): Synthesizing resource accessors") - let mappings = try project.targets + let mappings = try project.targets.values .map { try mapTarget($0, project: project) } let targets: [Target] = mappings.map(\.0) let sideEffects: [SideEffectDescriptor] = mappings.map(\.1).flatMap { $0 } - - return (project.with(targets: targets), sideEffects) + var project = project + project.targets = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) }) + return (project, sideEffects) } // MARK: - Helpers @@ -69,7 +71,9 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { / /// Map and generate resource interfaces for a given `Target` and `Project` private func mapTarget(_ target: Target, project: Project) throws -> (Target, [SideEffectDescriptor]) { - guard !target.resources.isEmpty, target.supportsSources else { return (target, []) } + let resourcesForSynthesizersPaths = target.resources.resources + .map(\.path) + target.coreDataModels.map(\.path) + guard !resourcesForSynthesizersPaths.isEmpty, target.supportsSources else { return (target, []) } var target = target @@ -151,8 +155,8 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { / target: Target, developmentRegion: String? ) -> [AbsolutePath] { - let resourcesPaths = target.resources - .map(\.path) + let resourcesPaths = target.resources.resources + .map(\.path) + target.coreDataModels.map(\.path) var paths = resourcesPaths .filter { $0.extension.map(resourceSynthesizer.extensions.contains) ?? false } diff --git a/Sources/TuistGenerator/Mappers/TargetActionDisableShowEnvVarsProjectMapper.swift b/Sources/TuistGenerator/Mappers/TargetActionDisableShowEnvVarsProjectMapper.swift index 9dcc812a0fa..ebc62f9a244 100644 --- a/Sources/TuistGenerator/Mappers/TargetActionDisableShowEnvVarsProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/TargetActionDisableShowEnvVarsProjectMapper.swift @@ -1,5 +1,5 @@ import TuistCore -import TuistGraph +import XcodeGraph /// This mapper takes the `Project` `disableShowEnvironmentVarsInScriptPhases` option and pushes it down into all of the `Target`s /// shell script `TargetAction`s @@ -7,8 +7,13 @@ public final class TargetActionDisableShowEnvVarsProjectMapper: ProjectMapping { public init() {} public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { + logger + .debug( + "Transforming project \(project.name): Configuring 'disable show environment vars in script' in project targets' script phases" + ) + var project = project - project.targets = project.targets.map { target in + project.targets = project.targets.mapValues { target in var mappedTarget = target mappedTarget.scripts = mappedTarget.scripts.map { var script = $0 diff --git a/Sources/TuistGenerator/Settings/DefaultSettingsProvider.swift b/Sources/TuistGenerator/Settings/DefaultSettingsProvider.swift index dfbc82bcbae..d557ef86456 100644 --- a/Sources/TuistGenerator/Settings/DefaultSettingsProvider.swift +++ b/Sources/TuistGenerator/Settings/DefaultSettingsProvider.swift @@ -1,8 +1,8 @@ import Foundation import struct TSCUtility.Version import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XcodeProj public protocol DefaultSettingsProviding { diff --git a/Sources/TuistGenerator/Templates/AssetsTemplate.swift b/Sources/TuistGenerator/Templates/AssetsTemplate.swift index 89f32a5acde..82e05de7b37 100644 --- a/Sources/TuistGenerator/Templates/AssetsTemplate.swift +++ b/Sources/TuistGenerator/Templates/AssetsTemplate.swift @@ -74,7 +74,7 @@ extension SynthesizedResourceInterfaceTemplates { {% elif asset.type == "symbol" %} {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}") {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %} - {{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { + {{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: Sendable { {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %} } {% elif asset.items %} @@ -95,7 +95,7 @@ extension SynthesizedResourceInterfaceTemplates { {% endfor %} {% endmacro %} // swiftlint:disable identifier_name line_length nesting type_body_length type_name - {{accessModifier}} enum {{enumName}} { + {{accessModifier}} enum {{enumName}}: Sendable { {% if catalogs.count > 1 or param.forceFileNameEnum %} {% for catalog in catalogs %} {{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { @@ -111,8 +111,8 @@ extension SynthesizedResourceInterfaceTemplates { // MARK: - Implementation Details {% if resourceCount.arresourcegroup > 0 %} - {{accessModifier}} struct {{arResourceGroupType}} { - {{accessModifier}} fileprivate(set) var name: String + {{accessModifier}} struct {{arResourceGroupType}}: Sendable { + {{accessModifier}} let name: String #if os(iOS) @available(iOS 11.3, *) @@ -131,7 +131,7 @@ extension SynthesizedResourceInterfaceTemplates { @available(iOS 11.3, *) {{accessModifier}} extension ARReferenceImage { static func referenceImages(in asset: {{arResourceGroupType}}) -> Set { - let bundle = {{bundleToken}}.bundle + let bundle = Bundle.module return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set() } } @@ -139,7 +139,7 @@ extension SynthesizedResourceInterfaceTemplates { @available(iOS 12.0, *) {{accessModifier}} extension ARReferenceObject { static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set { - let bundle = {{bundleToken}}.bundle + let bundle = Bundle.module return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set() } } @@ -147,37 +147,27 @@ extension SynthesizedResourceInterfaceTemplates { {% endif %} {% if resourceCount.color > 0 %} - {{accessModifier}} final class {{colorType}} { - {{accessModifier}} fileprivate(set) var name: String + {{accessModifier}} final class {{colorType}}: Sendable { + {{accessModifier}} let name: String #if os(macOS) {{accessModifier}} typealias Color = NSColor - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) {{accessModifier}} typealias Color = UIColor #endif - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) - {{accessModifier}} private(set) lazy var color: Color = { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, visionOS 1.0, *) + {{accessModifier}} var color: Color { guard let color = Color(asset: self) else { fatalError("Unable to load color asset named \\(name).") } return color - }() + } #if canImport(SwiftUI) - private var _swiftUIColor: Any? = nil - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - {{accessModifier}} private(set) var swiftUIColor: SwiftUI.Color { - get { - if self._swiftUIColor == nil { - self._swiftUIColor = SwiftUI.Color(asset: self) - } - - return self._swiftUIColor as! SwiftUI.Color - } - set { - self._swiftUIColor = newValue - } + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, visionOS 1.0, *) + {{accessModifier}} var swiftUIColor: SwiftUI.Color { + return SwiftUI.Color(asset: self) } #endif @@ -187,10 +177,10 @@ extension SynthesizedResourceInterfaceTemplates { } {{accessModifier}} extension {{colorType}}.Color { - @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, visionOS 1.0, *) convenience init?(asset: {{colorType}}) { - let bundle = {{bundleToken}}.bundle - #if os(iOS) || os(tvOS) + let bundle = Bundle.module + #if os(iOS) || os(tvOS) || os(visionOS) self.init(named: asset.name, in: bundle, compatibleWith: nil) #elseif os(macOS) self.init(named: NSColor.Name(asset.name), bundle: bundle) @@ -201,10 +191,10 @@ extension SynthesizedResourceInterfaceTemplates { } #if canImport(SwiftUI) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, visionOS 1.0, *) {{accessModifier}} extension SwiftUI.Color { init(asset: {{colorType}}) { - let bundle = {{bundleToken}}.bundle + let bundle = Bundle.module self.init(asset.name, bundle: bundle) } } @@ -212,11 +202,11 @@ extension SynthesizedResourceInterfaceTemplates { {% endif %} {% if resourceCount.data > 0 %} - {{accessModifier}} struct {{dataType}} { - {{accessModifier}} fileprivate(set) var name: String + {{accessModifier}} struct {{dataType}}: Sendable { + {{accessModifier}} let name: String - #if os(iOS) || os(tvOS) || os(macOS) - @available(iOS 9.0, macOS 10.11, *) + #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) + @available(iOS 9.0, macOS 10.11, visionOS 1.0, *) {{accessModifier}} var data: NSDataAsset { guard let data = NSDataAsset(asset: self) else { fatalError("Unable to load data asset named \\(name).") @@ -226,12 +216,12 @@ extension SynthesizedResourceInterfaceTemplates { #endif } - #if os(iOS) || os(tvOS) || os(macOS) - @available(iOS 9.0, macOS 10.11, *) + #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) + @available(iOS 9.0, macOS 10.11, visionOS 1.0, *) {{accessModifier}} extension NSDataAsset { convenience init?(asset: {{dataType}}) { - let bundle = {{bundleToken}}.bundle - #if os(iOS) || os(tvOS) + let bundle = Bundle.module + #if os(iOS) || os(tvOS) || os(visionOS) self.init(name: asset.name, bundle: bundle) #elseif os(macOS) self.init(name: NSDataAsset.Name(asset.name), bundle: bundle) @@ -242,18 +232,18 @@ extension SynthesizedResourceInterfaceTemplates { {% endif %} {% if resourceCount.image > 0 or resourceCount.symbol > 0 %} - {{accessModifier}} struct {{imageType}} { - {{accessModifier}} fileprivate(set) var name: String + {{accessModifier}} struct {{imageType}}: Sendable { + {{accessModifier}} let name: String #if os(macOS) {{accessModifier}} typealias Image = NSImage - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) {{accessModifier}} typealias Image = UIImage #endif {{accessModifier}} var image: Image { - let bundle = {{bundleToken}}.bundle - #if os(iOS) || os(tvOS) + let bundle = Bundle.module + #if os(iOS) || os(tvOS) || os(visionOS) let image = Image(named: name, in: bundle, compatibleWith: nil) #elseif os(macOS) let image = bundle.image(forResource: NSImage.Name(name)) @@ -267,43 +257,28 @@ extension SynthesizedResourceInterfaceTemplates { } #if canImport(SwiftUI) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, visionOS 1.0, *) {{accessModifier}} var swiftUIImage: SwiftUI.Image { SwiftUI.Image(asset: self) } #endif } - {{accessModifier}} extension {{imageType}}.Image { - @available(macOS, deprecated, - message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property") - convenience init?(asset: {{imageType}}) { - #if os(iOS) || os(tvOS) - let bundle = {{bundleToken}}.bundle - self.init(named: asset.name, in: bundle, compatibleWith: nil) - #elseif os(macOS) - self.init(named: NSImage.Name(asset.name)) - #elseif os(watchOS) - self.init(named: asset.name) - #endif - } - } - #if canImport(SwiftUI) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, visionOS 1.0, *) {{accessModifier}} extension SwiftUI.Image { init(asset: {{imageType}}) { - let bundle = {{bundleToken}}.bundle + let bundle = Bundle.module self.init(asset.name, bundle: bundle) } init(asset: {{imageType}}, label: Text) { - let bundle = {{bundleToken}}.bundle + let bundle = Bundle.module self.init(asset.name, bundle: bundle, label: label) } init(decorative asset: {{imageType}}) { - let bundle = {{bundleToken}}.bundle + let bundle = Bundle.module self.init(decorative: asset.name, bundle: bundle) } } diff --git a/Sources/TuistGenerator/Templates/FontsTemplate.swift b/Sources/TuistGenerator/Templates/FontsTemplate.swift index cbf08ed4bde..90f72615df0 100644 --- a/Sources/TuistGenerator/Templates/FontsTemplate.swift +++ b/Sources/TuistGenerator/Templates/FontsTemplate.swift @@ -10,7 +10,7 @@ extension SynthesizedResourceInterfaceTemplates { {% set fontType %}{{param.name}}FontConvertible{% endset %} #if os(macOS) import AppKit.NSFont - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) import UIKit.UIFont #endif #if canImport(SwiftUI) @@ -30,9 +30,9 @@ extension SynthesizedResourceInterfaceTemplates { {{path|basename}} {% endif %} {% endfilter %}{% endmacro %} - {{accessModifier}} enum {{param.name}}FontFamily { + {{accessModifier}} enum {{param.name}}FontFamily: Sendable { {% for family in families %} - {{accessModifier}} enum {{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { + {{accessModifier}} enum {{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: Sendable { {% for font in family.fonts %} {{accessModifier}} static let {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{fontType}}(name: "{{font.name}}", family: "{{family.name}}", path: "{% call transformPath font.path %}") {% endfor %} @@ -48,14 +48,14 @@ extension SynthesizedResourceInterfaceTemplates { // MARK: - Implementation Details - {{accessModifier}} struct {{fontType}} { + {{accessModifier}} struct {{fontType}}: Sendable { {{accessModifier}} let name: String {{accessModifier}} let family: String {{accessModifier}} let path: String #if os(macOS) {{accessModifier}} typealias Font = NSFont - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) {{accessModifier}} typealias Font = UIFont #endif @@ -74,7 +74,7 @@ extension SynthesizedResourceInterfaceTemplates { } #if os(macOS) return SwiftUI.Font.custom(font.fontName, size: font.pointSize) - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) return SwiftUI.Font(font) #endif } @@ -98,7 +98,7 @@ extension SynthesizedResourceInterfaceTemplates { {{accessModifier}} extension {{fontType}}.Font { convenience init?(font: {{fontType}}, size: CGFloat) { - #if os(iOS) || os(tvOS) || os(watchOS) + #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) { font.register() } diff --git a/Sources/TuistGenerator/Templates/PlistsTemplate.swift b/Sources/TuistGenerator/Templates/PlistsTemplate.swift index 2c1c61e1e12..dd50b073826 100644 --- a/Sources/TuistGenerator/Templates/PlistsTemplate.swift +++ b/Sources/TuistGenerator/Templates/PlistsTemplate.swift @@ -70,7 +70,7 @@ extension SynthesizedResourceInterfaceTemplates { // swiftlint:disable identifier_name line_length number_separator type_body_length {% for file in files %} - {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { + {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: Sendable { {% filter indent:2," ",true %}{% call fileBlock file %}{% endfilter %} } {% endfor %} diff --git a/Sources/TuistGenerator/Templates/StringsTemplate.swift b/Sources/TuistGenerator/Templates/StringsTemplate.swift index 4bd3773f16c..37029849a58 100644 --- a/Sources/TuistGenerator/Templates/StringsTemplate.swift +++ b/Sources/TuistGenerator/Templates/StringsTemplate.swift @@ -54,7 +54,7 @@ extension SynthesizedResourceInterfaceTemplates { {% endfor %} {% for child in item.children %} - {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { + {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: Sendable { {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %} } {% endfor %} @@ -62,7 +62,7 @@ extension SynthesizedResourceInterfaceTemplates { // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name {% set enumName %}{{param.name}}Strings{% endset %} - {{accessModifier}} enum {{enumName}} { + {{accessModifier}} enum {{enumName}}: Sendable { {% if tables.count > 1 or param.forceFileNameEnum %} {% for table in tables %} {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { @@ -83,7 +83,7 @@ extension SynthesizedResourceInterfaceTemplates { {% if param.lookupFunction %} let format = {{ param.lookupFunction }}(key, table) {% else %} - let format = {{bundleToken}}.bundle.localizedString(forKey: key, value: nil, table: table) + let format = Bundle.module.localizedString(forKey: key, value: nil, table: table) {% endif %} return String(format: format, locale: Locale.current, arguments: args) } diff --git a/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift b/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift index a71e6257797..4f664288c8f 100644 --- a/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift +++ b/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore /// This protocols defines the interface of an utility that given a list of embeddable frameworks @@ -79,7 +79,7 @@ final class EmbedScriptGenerator: EmbedScriptGenerating { for frameworkReference in frameworkReferences { guard case let GraphDependencyReference - .framework(path, _, _, dsymPath, bcsymbolmapPaths, _, _, _, _, _) = frameworkReference + .framework(path, _, dsymPath, bcsymbolmapPaths, _, _, _, _, _) = frameworkReference else { preconditionFailure("references need to be of type framework") break diff --git a/Sources/TuistGenerator/Utils/SettingsHelper.swift b/Sources/TuistGenerator/Utils/SettingsHelper.swift index c051d12aa9a..92a33e764cf 100644 --- a/Sources/TuistGenerator/Utils/SettingsHelper.swift +++ b/Sources/TuistGenerator/Utils/SettingsHelper.swift @@ -1,15 +1,18 @@ import Foundation import TuistCore -import TuistGraph +import XcodeGraph import XcodeProj final class SettingsHelper { + /// - Parameters: + /// - inherit: Forces the new settings to inherit the old settings func extend( buildSettings: inout SettingsDictionary, - with other: SettingsDictionary + with other: SettingsDictionary, + inherit: Bool = false ) { for (key, newValue) in other { - buildSettings[key] = merge(oldValue: buildSettings[key], newValue: newValue).normalize() + buildSettings[key] = merge(oldValue: buildSettings[key], newValue: newValue, inherit: inherit).normalize() } } @@ -61,7 +64,7 @@ final class SettingsHelper { // MARK: - Private - private func merge(oldValue: SettingValue?, newValue: SettingValue) -> SettingValue { + private func merge(oldValue: SettingValue?, newValue: SettingValue, inherit: Bool) -> SettingValue { // No need to merge, just return newValue when the oldValue is nil (buildSettings[key] == nil). guard let oldValue else { return newValue @@ -84,21 +87,21 @@ final class SettingsHelper { // would result in ["$(inherited)", "$(inherited)", "VALUE_1", "VALUE_2"] if .sortAndTrim() was not used. let inherited = "$(inherited)" switch (oldValue, newValue) { - case let (.string(old), .string(new)) where new.contains(inherited): + case let (.string(old), .string(new)) where new.contains(inherited) || inherit: // Example: ("OLD", "$(inherited) NEW") -> ["$(inherited) NEW", "OLD"] // This case shouldn't happen as all default multi-value settings are defined as NSArray return .array(Self.sortAndTrim(array: [old, new], element: inherited)) - case let (.string(old), .array(new)) where new.contains(inherited): + case let (.string(old), .array(new)) where new.contains(inherited) || inherit: // Example: ("OLD", ["$(inherited)", "NEW"]) -> ["$(inherited)", "NEW", "OLD"] return .array(Self.sortAndTrim(array: [old] + new, element: inherited)) - case let (.array(old), .string(new)) where new.contains(inherited): + case let (.array(old), .string(new)) where new.contains(inherited) || inherit: // Example: (["OLD", "OLD_2"], "$(inherited) NEW") -> ["$(inherited) NEW", "OLD", "OLD_2"] // This case shouldn't happen as all default multi-value settings are defined as NSArray return .array(Self.sortAndTrim(array: old + [new], element: inherited)) - case let (.array(old), .array(new)) where new.contains(inherited): + case let (.array(old), .array(new)) where new.contains(inherited) || inherit: // Example: (["OLD", "OLD_2"], ["$(inherited)", "NEW"]) -> ["$(inherited)", "NEW", "OLD", OLD_2"] return .array(Self.sortAndTrim(array: old + new, element: inherited)) diff --git a/Sources/TuistGenerator/Utils/SwiftPackageManagerInteractor.swift b/Sources/TuistGenerator/Utils/SwiftPackageManagerInteractor.swift index 55f4e52352b..fbae2c9b5d3 100644 --- a/Sources/TuistGenerator/Utils/SwiftPackageManagerInteractor.swift +++ b/Sources/TuistGenerator/Utils/SwiftPackageManagerInteractor.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// Swift Package Manager Interactor /// @@ -58,12 +58,11 @@ public class SwiftPackageManagerInteractor: SwiftPackageManagerInteracting { .appending(try RelativePath(validating: "\(workspaceName)/xcshareddata/swiftpm")) let workspacePackageResolvedPath = workspacePackageResolvedFolderPath.appending(component: "Package.resolved") - if fileHandler.exists(rootPackageResolvedPath) { - try fileHandler.createFolder(workspacePackageResolvedFolderPath) - if fileHandler.exists(workspacePackageResolvedPath) { - try fileHandler.delete(workspacePackageResolvedPath) + if fileHandler.exists(rootPackageResolvedPath), !fileHandler.exists(workspacePackageResolvedPath) { + if !fileHandler.exists(workspacePackageResolvedPath.parentDirectory) { + try fileHandler.createFolder(workspacePackageResolvedPath.parentDirectory) } - try fileHandler.copy(from: rootPackageResolvedPath, to: workspacePackageResolvedPath) + try fileHandler.linkFile(atPath: rootPackageResolvedPath, toPath: workspacePackageResolvedPath) } let workspacePath = path.appending(component: workspaceName) @@ -85,20 +84,24 @@ public class SwiftPackageManagerInteractor: SwiftPackageManagerInteracting { arguments.append(contentsOf: ["-workspace", workspacePath.pathString, "-list"]) - let events = System.shared.publisher(arguments).mapToString().values - for try await event in events { - switch event { - case let .standardError(error): - logger.error("\(error)") - case let .standardOutput(output): + try System.shared.run( + arguments, + verbose: false, + environment: System.shared.env, + redirection: .stream(stdout: { bytes in + let output = String(decoding: bytes, as: Unicode.UTF8.self) logger.debug("\(output)") - } - } + }, stderr: { bytes in + let error = String(decoding: bytes, as: Unicode.UTF8.self) + logger.error("\(error)") + }) + ) - if fileHandler.exists(rootPackageResolvedPath) { - try fileHandler.delete(rootPackageResolvedPath) + if !fileHandler.exists(rootPackageResolvedPath), fileHandler.exists(workspacePackageResolvedPath) { + try fileHandler.copy(from: workspacePackageResolvedPath, to: rootPackageResolvedPath) + if !fileHandler.exists(workspacePackageResolvedPath) { + try fileHandler.linkFile(atPath: rootPackageResolvedPath, toPath: workspacePackageResolvedPath) + } } - - try fileHandler.linkFile(atPath: workspacePackageResolvedPath, toPath: rootPackageResolvedPath) } } diff --git a/Sources/TuistGenerator/Utils/SynthesizedResourceInterfacesGenerator.swift b/Sources/TuistGenerator/Utils/SynthesizedResourceInterfacesGenerator.swift index 05d8b02813d..d5dc1ad21cf 100644 --- a/Sources/TuistGenerator/Utils/SynthesizedResourceInterfacesGenerator.swift +++ b/Sources/TuistGenerator/Utils/SynthesizedResourceInterfacesGenerator.swift @@ -1,10 +1,10 @@ +import Path import PathKit import Stencil import StencilSwiftKit import SwiftGenKit -import TSCBasic -import TuistGraph import TuistSupport +import XcodeGraph protocol SynthesizedResourceInterfacesGenerating { func render( diff --git a/Sources/TuistGenerator/Writers/XcodeProjWriter.swift b/Sources/TuistGenerator/Writers/XcodeProjWriter.swift index 912113a7337..5875c67f7df 100644 --- a/Sources/TuistGenerator/Writers/XcodeProjWriter.swift +++ b/Sources/TuistGenerator/Writers/XcodeProjWriter.swift @@ -1,12 +1,13 @@ +import FileSystem import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XcodeProj public protocol XcodeProjWriting { - func write(project: ProjectDescriptor) throws - func write(workspace: WorkspaceDescriptor) throws + func write(project: ProjectDescriptor) async throws + func write(workspace: WorkspaceDescriptor) async throws } // MARK: - @@ -27,34 +28,37 @@ public final class XcodeProjWriter: XcodeProjWriting { private let config: Config private let sideEffectDescriptorExecutor: SideEffectDescriptorExecuting + private let fileSystem: FileSystem public init( sideEffectDescriptorExecutor: SideEffectDescriptorExecuting = SideEffectDescriptorExecutor(), - config: Config = .default + config: Config = .default, + fileSystem: FileSystem = FileSystem() ) { self.sideEffectDescriptorExecutor = sideEffectDescriptorExecutor self.config = config + self.fileSystem = fileSystem } - public func write(project: ProjectDescriptor) throws { - try write(project: project, schemesOrderHint: nil) + public func write(project: ProjectDescriptor) async throws { + try await write(project: project, schemesOrderHint: nil) } - public func write(workspace: WorkspaceDescriptor) throws { + public func write(workspace: WorkspaceDescriptor) async throws { let allSchemes = workspace.schemeDescriptors + workspace.projectDescriptors.flatMap(\.schemeDescriptors) let schemesOrderHint = schemesOrderHint(schemes: allSchemes) - try workspace.projectDescriptors.forEach(context: config.projectDescriptorWritingContext) { projectDescriptor in - try self.write(project: projectDescriptor, schemesOrderHint: schemesOrderHint) + try await workspace.projectDescriptors.forEach(context: config.projectDescriptorWritingContext) { projectDescriptor in + try await self.write(project: projectDescriptor, schemesOrderHint: schemesOrderHint) } try workspace.xcworkspace.write(path: workspace.xcworkspacePath.path, override: true) // Write all schemes (XCWorkspace doesn't manage any schemes like XcodeProj.sharedData) - try writeSchemes( + try await writeSchemes( schemeDescriptors: workspace.schemeDescriptors, xccontainerPath: workspace.xcworkspacePath, wipeSharedSchemesBeforeWriting: true ) - try writeXCSchemeManagement( + try await writeXCSchemeManagement( schemes: workspace.schemeDescriptors, xccontainerPath: workspace.xcworkspacePath, schemesOrderHint: schemesOrderHint @@ -66,14 +70,14 @@ public final class XcodeProjWriter: XcodeProjWriting { xccontainerPath: workspace.xcworkspacePath ) } else { - try deleteWorkspaceSettingsIfNeeded(xccontainerPath: workspace.xcworkspacePath) + try await deleteWorkspaceSettingsIfNeeded(xccontainerPath: workspace.xcworkspacePath) } - try sideEffectDescriptorExecutor.execute(sideEffects: workspace.sideEffectDescriptors) + try await sideEffectDescriptorExecutor.execute(sideEffects: workspace.sideEffectDescriptors) } // MARK: - Private - private func write(project: ProjectDescriptor, schemesOrderHint: [String: Int]?) throws { + private func write(project: ProjectDescriptor, schemesOrderHint: [String: Int]?) async throws { let schemesOrderHint = schemesOrderHint ?? self.schemesOrderHint(schemes: project.schemeDescriptors) // XcodeProj can manage writing of shared schemes, we have to manually manage the user schemes @@ -81,28 +85,28 @@ public final class XcodeProjWriter: XcodeProjWriting { try project.xcodeProj.write(path: project.xcodeprojPath.path) // Write user schemes only - try writeSchemes( + try await writeSchemes( schemeDescriptors: project.userSchemeDescriptors, xccontainerPath: project.xcodeprojPath, wipeSharedSchemesBeforeWriting: false // Since we are only writing user schemes ) - try writeXCSchemeManagement( + try await writeXCSchemeManagement( schemes: project.schemeDescriptors, xccontainerPath: project.xcodeprojPath, schemesOrderHint: schemesOrderHint ) - try sideEffectDescriptorExecutor.execute(sideEffects: project.sideEffectDescriptors) + try await sideEffectDescriptorExecutor.execute(sideEffects: project.sideEffectDescriptors) } private func writeSchemes( schemeDescriptors: [SchemeDescriptor], xccontainerPath: AbsolutePath, wipeSharedSchemesBeforeWriting: Bool - ) throws { + ) async throws { let sharedSchemesPath = try schemeDirectory(path: xccontainerPath, shared: true) if wipeSharedSchemesBeforeWriting, FileHandler.shared.exists(sharedSchemesPath) { - try FileHandler.shared.delete(sharedSchemesPath) + try await fileSystem.remove(sharedSchemesPath) } try schemeDescriptors.forEach { try write(scheme: $0, xccontainerPath: xccontainerPath) } } @@ -143,18 +147,17 @@ public final class XcodeProjWriter: XcodeProjWriting { .write(path: settingsPath.path, override: true) } - private func deleteWorkspaceSettingsIfNeeded(xccontainerPath: AbsolutePath) throws { + private func deleteWorkspaceSettingsIfNeeded(xccontainerPath: AbsolutePath) async throws { let settingsPath = WorkspaceSettingsDescriptor.xcsettingsFilePath(relativeToWorkspace: xccontainerPath) guard FileHandler.shared.exists(settingsPath) else { return } - - try FileHandler.shared.delete(settingsPath) + try await fileSystem.remove(settingsPath) } private func writeXCSchemeManagement( schemes: [SchemeDescriptor], xccontainerPath: AbsolutePath, schemesOrderHint: [String: Int] = [:] - ) throws { + ) async throws { let xcschememanagementPath = try schemeDirectory( path: xccontainerPath, shared: false @@ -168,7 +171,7 @@ public final class XcodeProjWriter: XcodeProjWriting { ) } if FileHandler.shared.exists(xcschememanagementPath) { - try FileHandler.shared.delete(xcschememanagementPath) + try await fileSystem.remove(xcschememanagementPath) } try FileHandler.shared.createFolder(xcschememanagementPath.parentDirectory) try XCSchemeManagement(schemeUserState: userStateSchemes, suppressBuildableAutocreation: nil) diff --git a/Sources/TuistGeneratorTesting/Descriptors/Mocks/MockSideEffectDescriptorExecutor.swift b/Sources/TuistGeneratorTesting/Descriptors/Mocks/MockSideEffectDescriptorExecutor.swift index 72e75721727..3c058ef2967 100644 --- a/Sources/TuistGeneratorTesting/Descriptors/Mocks/MockSideEffectDescriptorExecutor.swift +++ b/Sources/TuistGeneratorTesting/Descriptors/Mocks/MockSideEffectDescriptorExecutor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore @testable import TuistGenerator diff --git a/Sources/TuistGeneratorTesting/Descriptors/TestData/ProjectDescriptor+TestData.swift b/Sources/TuistGeneratorTesting/Descriptors/TestData/ProjectDescriptor+TestData.swift index 5afdcce3cb5..b26af76ca3b 100644 --- a/Sources/TuistGeneratorTesting/Descriptors/TestData/ProjectDescriptor+TestData.swift +++ b/Sources/TuistGeneratorTesting/Descriptors/TestData/ProjectDescriptor+TestData.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import XcodeProj diff --git a/Sources/TuistGeneratorTesting/Descriptors/TestData/SchemeDescriptor+TestData.swift b/Sources/TuistGeneratorTesting/Descriptors/TestData/SchemeDescriptor+TestData.swift index 73faf2df283..96ef54e437d 100644 --- a/Sources/TuistGeneratorTesting/Descriptors/TestData/SchemeDescriptor+TestData.swift +++ b/Sources/TuistGeneratorTesting/Descriptors/TestData/SchemeDescriptor+TestData.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XcodeProj @testable import TuistGenerator diff --git a/Sources/TuistGeneratorTesting/Descriptors/TestData/WorkspaceDescriptor+TestData.swift b/Sources/TuistGeneratorTesting/Descriptors/TestData/WorkspaceDescriptor+TestData.swift index c98867a6acc..53c5c47f53e 100644 --- a/Sources/TuistGeneratorTesting/Descriptors/TestData/WorkspaceDescriptor+TestData.swift +++ b/Sources/TuistGeneratorTesting/Descriptors/TestData/WorkspaceDescriptor+TestData.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import XcodeProj diff --git a/Sources/TuistGeneratorTesting/Generator/MockDescriptorGenerator.swift b/Sources/TuistGeneratorTesting/Generator/MockDescriptorGenerator.swift index acbb8ed9d4d..28cc8b5049c 100644 --- a/Sources/TuistGeneratorTesting/Generator/MockDescriptorGenerator.swift +++ b/Sources/TuistGeneratorTesting/Generator/MockDescriptorGenerator.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph import XcodeProj @testable import TuistGenerator diff --git a/Sources/TuistGeneratorTesting/Linter/MockEnvironmentLinter.swift b/Sources/TuistGeneratorTesting/Linter/MockEnvironmentLinter.swift index b6f19add9b8..59e31a3bd68 100644 --- a/Sources/TuistGeneratorTesting/Linter/MockEnvironmentLinter.swift +++ b/Sources/TuistGeneratorTesting/Linter/MockEnvironmentLinter.swift @@ -1,7 +1,6 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph @testable import TuistGenerator diff --git a/Sources/TuistGeneratorTesting/Linter/MockGraphLinter.swift b/Sources/TuistGeneratorTesting/Linter/MockGraphLinter.swift index 00c012316a9..41893d68efc 100644 --- a/Sources/TuistGeneratorTesting/Linter/MockGraphLinter.swift +++ b/Sources/TuistGeneratorTesting/Linter/MockGraphLinter.swift @@ -1,6 +1,5 @@ import Foundation import TuistCore -import TuistGraph import TuistSupport @testable import TuistGenerator diff --git a/Sources/TuistGraph/DependenciesGraph/DependenciesGraph.swift b/Sources/TuistGraph/DependenciesGraph/DependenciesGraph.swift deleted file mode 100644 index bb7e7e8893f..00000000000 --- a/Sources/TuistGraph/DependenciesGraph/DependenciesGraph.swift +++ /dev/null @@ -1,21 +0,0 @@ -import TSCBasic -import TuistSupport - -/// A directed acyclic graph (DAG) that Tuist uses to represent the dependency tree. -public struct DependenciesGraph: Equatable, Codable { - /// A dictionary where the keys are the supported platforms and the values are dictionaries where the keys are the names of - /// dependencies, and the values are the dependencies themselves. - public let externalDependencies: [String: [TargetDependency]] - - /// A dictionary where the keys are the folder of external projects, and the values are the projects themselves. - public let externalProjects: [AbsolutePath: Project] - - /// Create an instance of `DependenciesGraph` model. - public init(externalDependencies: [String: [TargetDependency]], externalProjects: [AbsolutePath: Project]) { - self.externalDependencies = externalDependencies - self.externalProjects = externalProjects - } - - /// An empty `DependenciesGraph`. - public static let none: DependenciesGraph = .init(externalDependencies: [:], externalProjects: [:]) -} diff --git a/Sources/TuistGraph/Graph/ConditionalGraphTarget.swift b/Sources/TuistGraph/Graph/ConditionalGraphTarget.swift deleted file mode 100644 index b4ee787e2f8..00000000000 --- a/Sources/TuistGraph/Graph/ConditionalGraphTarget.swift +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 6799d08f636..00000000000 --- a/Sources/TuistGraph/Graph/Graph.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import TSCBasic - -/// A directed acyclic graph (DAG) that Tuist uses to represent the dependency tree. -public struct Graph: Equatable, Codable { - /// The name of the graph - public var name: String - - /// The path where the graph has been loaded from. - public var path: AbsolutePath - - /// Graph's workspace. - public var workspace: Workspace - - /// A dictionary where the keys are the paths to the directories where the projects are defined, - /// and the values are the projects defined in the directories. - public var projects: [AbsolutePath: Project] - - /// A dictionary where the keys are paths to the directories where the projects that contain packages are defined, - /// and the values are dictionaries where the key is the reference to the package, and the values are the packages. - public var packages: [AbsolutePath: [String: Package]] - - /// A dictionary where the keys are paths to the directories where the projects that contain targets are defined, - /// and the values are dictionaries where the key is the name of the target, and the values are the targets. - public var targets: [AbsolutePath: [String: Target]] - - /// A dictionary that contains the one-to-many dependencies that represent the graph. - public var dependencies: [GraphDependency: Set] - - /// A dictionary that contains the Conditions to apply to a dependency relationship - public var dependencyConditions: [GraphEdge: PlatformCondition] - - public init( - name: String, - path: AbsolutePath, - workspace: Workspace, - projects: [AbsolutePath: Project], - packages: [AbsolutePath: [String: Package]], - targets: [AbsolutePath: [String: Target]], - dependencies: [GraphDependency: Set], - dependencyConditions: [GraphEdge: PlatformCondition] - ) { - self.name = name - self.path = path - self.workspace = workspace - self.projects = projects - self.packages = packages - self.targets = targets - self.dependencies = dependencies - 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 deleted file mode 100644 index cc5dd70e7db..00000000000 --- a/Sources/TuistGraph/Graph/GraphDependency.swift +++ /dev/null @@ -1,285 +0,0 @@ -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 - } - - case runtime = "runtime package product" - case plugin = "plugin package product" - case macro = "macro package product" - - public static func < (lhs: PackageProductType, rhs: PackageProductType) -> Bool { - lhs.description < rhs.description - } - } - - case xcframework(GraphDependency.XCFramework) - - /// A dependency that represents a pre-compiled framework. - case framework( - path: AbsolutePath, - binaryPath: AbsolutePath, - dsymPath: AbsolutePath?, - bcsymbolmapPaths: [AbsolutePath], - linking: BinaryLinking, - architectures: [BinaryArchitecture], - isCarthage: Bool, - status: FrameworkStatus - ) - - /// A dependency that represents a pre-compiled library. - case library( - path: AbsolutePath, - publicHeaders: AbsolutePath, - linking: BinaryLinking, - architectures: [BinaryArchitecture], - 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) - - /// A dependency that represents a package product. - case packageProduct(path: AbsolutePath, product: String, type: PackageProductType) - - /// A dependency that represents a target that is defined in the project at the given path. - case target(name: String, path: AbsolutePath) - - /// A dependency that represents an SDK - case sdk(name: String, path: AbsolutePath, status: SDKStatus, source: SDKSource) - - public func hash(into hasher: inout Hasher) { - switch self { - case let .macro(path): - hasher.combine(path) - case let .xcframework(xcframework): - hasher.combine(xcframework) - case let .framework(path, _, _, _, _, _, _, _): - hasher.combine("framework") - hasher.combine(path) - case let .library(path, _, _, _, _): - hasher.combine("library") - hasher.combine(path) - case let .bundle(path): - hasher.combine("bundle") - hasher.combine(path) - case let .packageProduct(path, product, isPlugin): - hasher.combine("package") - hasher.combine(path) - hasher.combine(product) - hasher.combine(isPlugin) - case let .target(name, path): - hasher.combine("target") - hasher.combine(name) - hasher.combine(path) - case let .sdk(name, path, status, source): - hasher.combine("sdk") - hasher.combine(name) - hasher.combine(path) - hasher.combine(status) - hasher.combine(source) - } - } - - public var isTarget: Bool { - switch self { - case .macro: return false - case .xcframework: return false - case .framework: return false - case .library: return false - case .bundle: return false - case .packageProduct: return false - case .target: return true - case .sdk: return false - } - } - - /** - When the graph dependency represents a pre-compiled static binary. - */ - public var isStaticPrecompiled: Bool { - switch self { - 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 - case .target: return false - case .sdk: return false - } - } - - /** - When the graph dependency represents a dynamic precompiled binary, it returns true. - */ - public var isDynamicPrecompiled: Bool { - switch self { - 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 - case .target: return false - case .sdk: return false - } - } - - public var isPrecompiled: Bool { - switch self { - case .macro: return true - case .xcframework: return true - case .framework: return true - case .library: return true - case .bundle: return true - case .packageProduct: return false - case .target: return false - case .sdk: return false - } - } - - 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)? { - switch self { - case let .target(name: name, path: path): - return (name, path) - default: - return nil - } - } - - // MARK: - CustomStringConvertible - - public var description: String { - switch self { - case .macro: - return "macro '\(name)'" - case .xcframework: - return "xcframework '\(name)'" - case .framework: - return "framework '\(name)'" - case .library: - return "library '\(name)'" - case .bundle: - return "bundle '\(name)'" - case .packageProduct: - return "package '\(name)'" - case .target: - return "target '\(name)'" - case .sdk: - return "sdk '\(name)'" - } - } - - public var name: String { - switch self { - 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, _, _, _, _): - return path.basename - case let .bundle(path): - return path.basename - case let .packageProduct(_, product, _): - return product - case let .target(name, _): - return name - case let .sdk(name, _, _, _): - return name - } - } - - // MARK: - Comparable - - public static func < (lhs: GraphDependency, rhs: GraphDependency) -> Bool { - lhs.description < rhs.description - } -} diff --git a/Sources/TuistGraph/Graph/GraphEdge.swift b/Sources/TuistGraph/Graph/GraphEdge.swift deleted file mode 100644 index da6a397561e..00000000000 --- a/Sources/TuistGraph/Graph/GraphEdge.swift +++ /dev/null @@ -1,17 +0,0 @@ -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/Graph/GraphTarget.swift b/Sources/TuistGraph/Graph/GraphTarget.swift deleted file mode 100644 index 8c80eff6527..00000000000 --- a/Sources/TuistGraph/Graph/GraphTarget.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import TSCBasic - -public struct GraphTarget: Equatable, Hashable, Comparable, CustomDebugStringConvertible, CustomStringConvertible, Codable { - /// Path to the directory that contains the project where the target is defined. - public let path: AbsolutePath - - /// Target representation. - public let target: Target - - /// Project that contains the target. - public let project: Project - - public init(path: AbsolutePath, target: Target, project: Project) { - self.path = path - self.target = target - self.project = project - } - - public static func < (lhs: GraphTarget, rhs: GraphTarget) -> Bool { - (lhs.path, lhs.target) < (rhs.path, rhs.target) - } - - // MARK: - CustomDebugStringConvertible/CustomStringConvertible - - public var debugDescription: String { - description - } - - public var description: String { - "Target '\(target.name)' at path '\(project.path)'" - } -} diff --git a/Sources/TuistGraph/Models/AnalyzeAction.swift b/Sources/TuistGraph/Models/AnalyzeAction.swift deleted file mode 100644 index 7f80868e973..00000000000 --- a/Sources/TuistGraph/Models/AnalyzeAction.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -public struct AnalyzeAction: Equatable, Codable { - // MARK: - Attributes - - public let configurationName: String - - // MARK: - Init - - public init(configurationName: String) { - self.configurationName = configurationName - } -} diff --git a/Sources/TuistGraph/Models/ArchiveAction.swift b/Sources/TuistGraph/Models/ArchiveAction.swift deleted file mode 100644 index 7902429b95d..00000000000 --- a/Sources/TuistGraph/Models/ArchiveAction.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import TSCBasic - -public struct ArchiveAction: Equatable, Codable { - // MARK: - Attributes - - public let configurationName: String - public let revealArchiveInOrganizer: Bool - public let customArchiveName: String? - public let preActions: [ExecutionAction] - public let postActions: [ExecutionAction] - - // MARK: - Init - - public init( - configurationName: String, - revealArchiveInOrganizer: Bool = true, - customArchiveName: String? = nil, - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [] - ) { - self.configurationName = configurationName - self.revealArchiveInOrganizer = revealArchiveInOrganizer - self.customArchiveName = customArchiveName - self.preActions = preActions - self.postActions = postActions - } -} diff --git a/Sources/TuistGraph/Models/Arguments.swift b/Sources/TuistGraph/Models/Arguments.swift deleted file mode 100644 index 8f43b1d75fa..00000000000 --- a/Sources/TuistGraph/Models/Arguments.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -/// Arguments contain commandline arguments passed on launch and Environment variables. -public struct Arguments: Codable { - // MARK: - Attributes - - /// Launch arguments that are passed by the scheme when running a scheme action. - public let launchArguments: [LaunchArgument] - /// The environment variables that are passed by the scheme when running a scheme action. - public let environmentVariables: [String: EnvironmentVariable] - - // MARK: - Init - - public init( - environmentVariables: [String: EnvironmentVariable] = [:], - launchArguments: [LaunchArgument] = [] - ) { - self.environmentVariables = environmentVariables - self.launchArguments = launchArguments - } -} - -extension Arguments: Equatable { - /// Implement `Equatable` manually so order of arguments doesn't matter. - public static func == (lhs: Arguments, rhs: Arguments) -> Bool { - lhs.environmentVariables == rhs.environmentVariables - && lhs.launchArguments.sorted { $0.name < $1.name } - == rhs.launchArguments.sorted { $0.name == $1.name } - } -} diff --git a/Sources/TuistGraph/Models/AutogenerationOptions.swift b/Sources/TuistGraph/Models/AutogenerationOptions.swift deleted file mode 100644 index 1fc374de51b..00000000000 --- a/Sources/TuistGraph/Models/AutogenerationOptions.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public enum AutogenerationOptions: Hashable { - case disabled - case enabled(TestingOptions) -} diff --git a/Sources/TuistGraph/Models/BinaryArchitecture.swift b/Sources/TuistGraph/Models/BinaryArchitecture.swift deleted file mode 100644 index ce2321a31fa..00000000000 --- a/Sources/TuistGraph/Models/BinaryArchitecture.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -public enum BinaryArchitecture: String, Codable { - case x8664 = "x86_64" - case i386 - case armv7 - case armv7s - case arm64 - case armv7k - case arm6432 = "arm64_32" - case arm64e -} - -public enum BinaryLinking: String, Hashable, Codable { - case `static`, dynamic -} - -extension Sequence { - /// Returns true if all the architectures are only for simulator. - public var onlySimulator: Bool { - let simulatorArchitectures: [BinaryArchitecture] = [.x8664, .i386, .arm64] - return allSatisfy { simulatorArchitectures.contains($0) } - } -} diff --git a/Sources/TuistGraph/Models/BuildAction.swift b/Sources/TuistGraph/Models/BuildAction.swift deleted file mode 100644 index 792226b78ae..00000000000 --- a/Sources/TuistGraph/Models/BuildAction.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import TSCBasic - -public struct BuildAction: Equatable, Codable { - // MARK: - Attributes - - public var targets: [TargetReference] - public var preActions: [ExecutionAction] - public var postActions: [ExecutionAction] - public var runPostActionsOnFailure: Bool - - // MARK: - Init - - public init( - targets: [TargetReference] = [], - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - runPostActionsOnFailure: Bool = false - ) { - self.targets = targets - self.preActions = preActions - self.postActions = postActions - self.runPostActionsOnFailure = runPostActionsOnFailure - } -} diff --git a/Sources/TuistGraph/Models/BuildConfiguration.swift b/Sources/TuistGraph/Models/BuildConfiguration.swift deleted file mode 100644 index b49438bba71..00000000000 --- a/Sources/TuistGraph/Models/BuildConfiguration.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation - -/// A build configuration acts as a configuration identifier. -/// -/// -/// -/// It hosts the name as well as the variant of -/// a configuration to help infer the appropriate -/// default settings. -public struct BuildConfiguration: Codable { - public enum Variant: String, Codable { - case debug, release - } - - public static let debug = BuildConfiguration(name: "Debug", variant: .debug) - public static let release = BuildConfiguration(name: "Release", variant: .release) - - public let name: String - public let variant: Variant - - public init(name: String, variant: Variant) { - self.name = name - self.variant = variant - } - - public static func release(_ name: String) -> BuildConfiguration { - BuildConfiguration(name: name, variant: .release) - } - - public static func debug(_ name: String) -> BuildConfiguration { - BuildConfiguration(name: name, variant: .debug) - } -} - -extension BuildConfiguration: Equatable { - public static func == (lhs: BuildConfiguration, rhs: BuildConfiguration) -> Bool { - lhs.name.caseInsensitiveCompare(rhs.name) == .orderedSame && lhs.variant == rhs.variant - } -} - -extension BuildConfiguration: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(name.lowercased()) - hasher.combine(variant) - } -} - -extension BuildConfiguration: Comparable { - public static func < (lhs: BuildConfiguration, rhs: BuildConfiguration) -> Bool { - lhs.name < rhs.name - } -} - -extension BuildConfiguration: CustomStringConvertible { - public var description: String { - "\(name) (\(variant.rawValue))" - } -} diff --git a/Sources/TuistGraph/Models/BuildRule+CompilerSpec.swift b/Sources/TuistGraph/Models/BuildRule+CompilerSpec.swift deleted file mode 100644 index e615f9d18e2..00000000000 --- a/Sources/TuistGraph/Models/BuildRule+CompilerSpec.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation - -extension BuildRule { - /// Mapping of compiler specs supported by a build rule to Xcode's internal representation. - /// All values were coppied from `pbxproj`. - public enum CompilerSpec: String, Codable { - case appIntentsMetadataExtractor = "com.apple.compilers.appintentsmetadata" - case appShortcutStringsMetadataExtractor = "com.apple.compilers.appshortcutstringsmetadata" - case appleClang = "com.apple.compilers.llvm.clang.1_0" - case assetCatalogCompiler = "com.apple.compilers.assetcatalog" - case codeSign = "com.apple.build-tools.codesign" - case compileRealityComposerProject = "com.apple.build-tasks.compile-rc-project.xcplugin" - case compileSceneKitShaders = "com.apple.build-tasks.compile-scenekit-shadercache" - case compileSkybox = "com.apple.build-tasks.compile-skybox.xcplugin" - case compileUSDZ = "com.apple.build-tasks.compile-usdz.xcplugin" - case compressPNG = "com.apple.build-tasks.copy-png-file" - case copyPlistFile = "com.apple.build-tasks.copy-plist-file" - case copySceneKitAssets = "com.apple.build-tasks.copy-scenekit-assets" - case copyStringsFile = "com.apple.build-tasks.copy-strings-file" - case copyTiffFile = "com.apple.build-tasks.copy-tiff-file" - case coreDataMappingModelCompiler = "com.apple.compilers.model.coredatamapping" - case coreMLModelCompiler = "com.apple.compilers.coreml" - case dataModelCompiler = "com.apple.compilers.model.coredata" - case defaultCompiler = "com.apple.compilers.gcc" - case documentationCompiler = "com.apple.compilers.documentation" - case dTrace = "com.apple.compilers.dtrace" - case generateSpriteKitTextureAtlas = "com.apple.build-tasks.generate-texture-atlas.xcplugin" - case iconutil = "com.apple.compilers.iconutil" - case instrumetsPackageBuilder = "com.apple.compilers.instruments-package-builder" - case intentDefinitionCompiler = "com.apple.compilers.intents" - case interfaceBuilderNIBPostprocessor = "com.apple.xcode.tools.ibtool.postprocessor" - case interfaceBuilderStoryboardCompiler = "com.apple.xcode.tools.ibtool.storyboard.compiler" - case interfaceBuilderStoryboardLinker = "com.apple.xcode.tools.ibtool.storyboard.linker" - case interfaceBuilderStoryboardPostprocessor = "com.apple.xcode.tools.ibtool.storyboard.postprocessor" - case interfaceBuilderXIBCompiler = "com.apple.xcode.tools.ibtool.compiler" - case ioKitInterfaceGenerator = "com.apple.compilers.iig" - case lex = "com.apple.compilers.lex" - case lsRegisterURL = "com.apple.build-tasks.ls-register-url" - case metalCompiler = "com.apple.compilers.metal" - case metalLinker = "com.apple.compilers.metal-linker" - case mig = "com.apple.compilers.mig" - case nasm = "com.apple.compilers.nasm" - case nmedit = "com.apple.build-tools.nmedit" - case openCL = "com.apple.compilers.opencl" - case osaCompile = "com.apple.compilers.osacompile" - case pbxcp = "com.apple.compilers.pbxcp" - case processSceneKitDocument = "com.apple.compilers.scntool" - case processXCAppExtensionPoints = "com.apple.compilers.process-xcappextensionpoints" - case rez = "com.apple.compilers.rez" - case stripSymbols = "com.apple.build-tools.strip" - case swiftCompiler = "com.apple.xcode.tools.swift.compiler" - case swiftABIBaselineGenerator = "com.apple.build-tools.swift-abi-generation" - case swiftFrameworkABIChecker = "com.apple.build-tools.swift-abi-checker" - case textBasedAPITool = "com.apple.build-tools.tapi.installapi" - case unifdef = "public.build-task.unifdef" - case yacc = "com.apple.compilers.yacc" - case customScript = "com.apple.compilers.proxy.script" - } -} diff --git a/Sources/TuistGraph/Models/BuildRule+FileType.swift b/Sources/TuistGraph/Models/BuildRule+FileType.swift deleted file mode 100644 index 3f6f957b39b..00000000000 --- a/Sources/TuistGraph/Models/BuildRule+FileType.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation - -extension BuildRule { - /// Mapping of file types supported by a build rule to Xcode's internal representation. - /// All values were coppied from `pbxproj`. - public enum FileType: String, Codable { - case instrumentsPackageDefinition = "com.apple.instruments.package-definition" - case metalAIR = "compiled.air" - case machO = "compiled.mach-o" - case machOObject = "compiled.mach-o.objfile" - case siriKitIntent = "file.intentdefinition" - case coreMLMachineLearning = "file.mlmodel" - case rcProjectDocument = "file.rcproject" - case skyboxDocument = "file.skybox" - case interfaceBuilderStoryboard = "file.storyboard" - case interfaceBuilder = "file.xib" - case documentationCatalog = "folder.documentationcatalog" - case coreMLMachineLearningModelPackage = "folder.mlpackage" - case assemblyAsm = "sourcecode.asm" - case assemblyAsmAsm = "sourcecode.asm.asm" - case llvmAssembly = "sourcecode.asm.llvm" - case cSource = "sourcecode.c" - case clipsSource = "sourcecode.clips" - case cppSource = "sourcecode.cpp" - case dtraceSource = "sourcecode.dtrace" - case dylanSource = "sourcecode.dylan" - case fortranSource = "sourcecode.fortran" - case glslSource = "sourcecode.glsl" - case iigSource = "sourcecode.iig" - case javaSource = "sourcecode.java" - case lexSource = "sourcecode.lex" - case metalShaderSource = "sourcecode.metal" - case migSource = "sourcecode.mig" - case nasmAssembly = "sourcecode.nasm" - case openCLSource = "sourcecode.opencl" - case pascalSource = "sourcecode.pascal" - case protobufSource = "sourcecode.protobuf" - case rezSource = "sourcecode.rez" - case swiftSource = "sourcecode.swift" - case yaccSource = "sourcecode.yacc" - case localizationString = "text.plist.strings" - case localizationStringDictionary = "text.plist.stringsdict" - case xcAppExtensionPoints = "text.plist.xcappextensionpoints" - case xcodeSpecificationPlist = "text.plist.xcspec" - case dae = "text.xml.dae" - case nib = "wrapper.nib" - case interfaceBuilderStoryboardPackage = "wrapper.storyboardc" - case classModel = "wrapper.xcclassmodel" - case dataModel = "wrapper.xcdatamodel" - case dataModelVersion = "wrapper.xcdatamodeld" - case mappingModel = "wrapper.xcmappingmodel" - case sourceFilesWithNamesMatching = "pattern.proxy" - } -} diff --git a/Sources/TuistGraph/Models/BuildRule.swift b/Sources/TuistGraph/Models/BuildRule.swift deleted file mode 100644 index 7464b4e10c1..00000000000 --- a/Sources/TuistGraph/Models/BuildRule.swift +++ /dev/null @@ -1,53 +0,0 @@ -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 { - /// Element compiler spec. - public let compilerSpec: CompilerSpec - - /// Element file patterns. - public let filePatterns: String? - - /// Element file type. - public let fileType: FileType - - /// Element name. - public let name: String? - - /// Element output files. - public let outputFiles: [String] - - /// Element input files. - public let inputFiles: [String]? - - /// Element output files compiler flags. - public let outputFilesCompilerFlags: [String]? - - /// Element script. - public let script: String? - - /// Element run once per architecture. - public let runOncePerArchitecture: Bool? - - public init( - compilerSpec: CompilerSpec, - fileType: FileType, - filePatterns: String? = nil, - name: String? = nil, - outputFiles: [String] = [], - inputFiles: [String]? = nil, - outputFilesCompilerFlags: [String]? = nil, - script: String? = nil, - runOncePerArchitecture: Bool? = nil - ) { - self.compilerSpec = compilerSpec - self.filePatterns = filePatterns - self.fileType = fileType - self.name = name - self.outputFiles = outputFiles - self.inputFiles = inputFiles - self.outputFilesCompilerFlags = outputFilesCompilerFlags - self.script = script - self.runOncePerArchitecture = runOncePerArchitecture - } -} diff --git a/Sources/TuistGraph/Models/Cache.swift b/Sources/TuistGraph/Models/Cache.swift deleted file mode 100644 index 2b7fe559862..00000000000 --- a/Sources/TuistGraph/Models/Cache.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version - -public struct Cache: Equatable, Hashable { - // Warning ⚠️ - // - // If new property is added to a caching profile, - // it must be added to `CacheProfileContentHasher` too. - public struct Profile: Equatable, Hashable, CustomStringConvertible { - public let name: String - public let configuration: String - public let device: String? - public let os: Version? - - public init( - name: String, - configuration: String, - device: String? = nil, - os: Version? = nil - ) { - self.name = name - self.configuration = configuration - self.device = device - self.os = os - } - - public var description: String { - name - } - } - - public let profiles: [Profile] - public let path: AbsolutePath? - - public init(profiles: [Profile], path: AbsolutePath?) { - self.profiles = profiles - self.path = path - } - - public static let `default` = Cache( - profiles: [ - Profile(name: "Development", configuration: "Debug"), - Profile(name: "Release", configuration: "Release"), - ], - path: nil - ) -} diff --git a/Sources/TuistGraph/Models/Cloud.swift b/Sources/TuistGraph/Models/Cloud.swift deleted file mode 100644 index 005a1d7f3b5..00000000000 --- a/Sources/TuistGraph/Models/Cloud.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -/// Cloud represents the configuration to connect to the server. -public struct Cloud: Equatable, Hashable { - /// Cloud option. - public enum Option: String, Codable, Equatable { - case disableAnalytics - case optional - } - - /// The base URL that points to the cloud server - public let url: URL - - /// The project unique identifier. - public let projectId: String - - /// Cloud options. - public let options: [Option] - - /// Initializes an instance of Cloud. - /// - Parameters: - /// - url: Cloud server base URL. - /// - projectId: Project unique identifier. - /// - options: Cloud options. - public init(url: URL, projectId: String, options: [Option]) { - self.url = url - self.projectId = projectId - self.options = options - } -} diff --git a/Sources/TuistGraph/Models/Config.swift b/Sources/TuistGraph/Models/Config.swift deleted file mode 100644 index 4aeba818d1f..00000000000 --- a/Sources/TuistGraph/Models/Config.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility - -/// This model allows to configure Tuist. -public struct Config: Equatable, Hashable { - /// List of `Plugin`s used to extend Tuist. - public let plugins: [PluginLocation] - - /// Generation options. - public let generationOptions: GenerationOptions - - /// List of Xcode versions the project or set of projects is compatible with. - public let compatibleXcodeVersions: CompatibleXcodeVersions - - /// Cloud configuration. - public let cloud: Cloud? - - /// Cache configuration. - public let cache: Cache? - - /// The version of Swift that will be used by Tuist. - /// If `nil` is passed then Tuist will use the environment’s version. - public let swiftVersion: Version? - - /// The path of the config file. - public let path: AbsolutePath? - - /// Returns the default Tuist configuration. - public static var `default`: Config { - Config( - compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, - swiftVersion: nil, - plugins: [], - generationOptions: .init( - resolveDependenciesWithSystemScm: false, - disablePackageVersionLocking: false, - staticSideEffectsWarningTargets: .all - ), - path: nil - ) - } - - /// Initializes the tuist cofiguration. - /// - /// - Parameters: - /// - compatibleXcodeVersions: List of Xcode versions the project or set of projects is compatible with. - /// - cloud: Cloud configuration. - /// - cache: Cache configuration. - /// - swiftVersion: The version of Swift that will be used by Tuist. - /// - plugins: List of locations to a `Plugin` manifest. - /// - generationOptions: Generation options. - /// - path: The path of the config file. - public init( - compatibleXcodeVersions: CompatibleXcodeVersions, - cloud: Cloud?, - cache: Cache?, - swiftVersion: Version?, - plugins: [PluginLocation], - generationOptions: GenerationOptions, - path: AbsolutePath? - ) { - self.compatibleXcodeVersions = compatibleXcodeVersions - self.cloud = cloud - self.cache = cache - self.swiftVersion = swiftVersion - self.plugins = plugins - self.generationOptions = generationOptions - self.path = path - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(generationOptions) - hasher.combine(cloud) - hasher.combine(cache) - hasher.combine(swiftVersion) - hasher.combine(compatibleXcodeVersions) - } -} diff --git a/Sources/TuistGraph/Models/CopyFilesAction.swift b/Sources/TuistGraph/Models/CopyFilesAction.swift deleted file mode 100644 index ef64fd67875..00000000000 --- a/Sources/TuistGraph/Models/CopyFilesAction.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import TSCBasic - -public struct CopyFilesAction: Equatable, Codable { - /// Name of the build phase when the project gets generated. - public var name: String - - /// Destination to copy files to. - public var destination: Destination - - /// Path to a folder inside the destination. - public var subpath: String? - - /// Relative paths to the files to be copied. - public var files: [FileElement] - - /// Destination path. - public enum Destination: String, Equatable, Codable { - case absolutePath - case productsDirectory - case wrapper - case executables - case resources - case javaResources - case frameworks - case sharedFrameworks - case sharedSupport - case plugins - case other - } - - public init( - name: String, - destination: Destination, - subpath: String? = nil, - files: [FileElement] - ) { - self.name = name - self.destination = destination - self.subpath = subpath - self.files = files - } -} diff --git a/Sources/TuistGraph/Models/CoreDataModel.swift b/Sources/TuistGraph/Models/CoreDataModel.swift deleted file mode 100644 index 4301b6cb0f2..00000000000 --- a/Sources/TuistGraph/Models/CoreDataModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import TSCBasic - -/// Represents a Core Data model -public struct CoreDataModel: Equatable, Codable { - // MARK: - Attributes - - /// Relative path to the Core Data model. - public let path: AbsolutePath - - /// Paths to the versions. - public let versions: [AbsolutePath] - - /// Current version without the extension. - public let currentVersion: String - - // MARK: - Init - - public init( - path: AbsolutePath, - versions: [AbsolutePath], - currentVersion: String - ) { - self.path = path - self.versions = versions - self.currentVersion = currentVersion - } -} diff --git a/Sources/TuistGraph/Models/Dependencies/CarthageDependencies.swift b/Sources/TuistGraph/Models/Dependencies/CarthageDependencies.swift deleted file mode 100644 index 00e04f038a1..00000000000 --- a/Sources/TuistGraph/Models/Dependencies/CarthageDependencies.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation - -/// Contains descriptions of dependencies to be fetched with Carthage. -public struct CarthageDependencies: Equatable { - /// List of dependencies that can be installed using Carthage. - public let dependencies: [Dependency] - - /// Initializes a new `CarthageDependencies` instance. - /// - Parameters: - /// - dependencies: List of dependencies that can be installed using Carthage. - public init( - _ dependencies: [Dependency] - ) { - self.dependencies = dependencies - } - - /// Returns `Cartfile` representation. - public func cartfileValue() -> String { - dependencies - .map(\.cartfileValue) - .joined(separator: "\n") - } -} - -// MARK: - CarthageDependencies.Dependency - -extension CarthageDependencies { - public enum Dependency: Equatable { - /// GitHub repositories (both GitHub.com and GitHub Enterprise). - case github(path: String, requirement: Requirement) - /// Other Git repositories. - case git(path: String, requirement: Requirement) - /// Dependencies that are only available as compiled binary `.framework`s. - case binary(path: String, requirement: Requirement) - - /// Returns `Cartfile` representation. - public var cartfileValue: String { - switch self { - case let .github(path, requirement): - return #"github "\#(path)" \#(requirement.cartfileValue)"# - case let .git(path, requirement): - return #"git "\#(path)" \#(requirement.cartfileValue)"# - case let .binary(path, requirement): - return #"binary "\#(path)" \#(requirement.cartfileValue)"# - } - } - } -} - -// MARK: - CarthageDependencies.Requirement - -extension CarthageDependencies { - public enum Requirement: Equatable { - case exact(String) - case upToNext(String) - case atLeast(String) - case branch(String) - case revision(String) - - /// Returns `Cartfile` representation. - public var cartfileValue: String { - switch self { - case let .exact(version): - return "== \(version)" - case let .upToNext(version): - return "~> \(version)" - case let .atLeast(version): - return ">= \(version)" - case let .branch(branch): - return #""\#(branch)""# - case let .revision(revision): - return #""\#(revision)""# - } - } - } -} diff --git a/Sources/TuistGraph/Models/Dependencies/Dependencies.swift b/Sources/TuistGraph/Models/Dependencies/Dependencies.swift deleted file mode 100644 index 961dbe07186..00000000000 --- a/Sources/TuistGraph/Models/Dependencies/Dependencies.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -public struct Dependencies: Equatable { - public let carthage: CarthageDependencies? - public let swiftPackageManager: SwiftPackageManagerDependencies? - public let platforms: Set - - public init( - carthage: CarthageDependencies?, - swiftPackageManager: SwiftPackageManagerDependencies?, - platforms: Set - ) { - self.carthage = carthage - self.swiftPackageManager = swiftPackageManager - self.platforms = platforms - } -} diff --git a/Sources/TuistGraph/Models/Dependencies/PackageSettings.swift b/Sources/TuistGraph/Models/Dependencies/PackageSettings.swift deleted file mode 100644 index 8c0b5479077..00000000000 --- a/Sources/TuistGraph/Models/Dependencies/PackageSettings.swift +++ /dev/null @@ -1,39 +0,0 @@ -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/Dependencies/SwiftPackageManagerDependencies.swift b/Sources/TuistGraph/Models/Dependencies/SwiftPackageManagerDependencies.swift deleted file mode 100644 index 61c0e8493fc..00000000000 --- a/Sources/TuistGraph/Models/Dependencies/SwiftPackageManagerDependencies.swift +++ /dev/null @@ -1,135 +0,0 @@ -import Foundation -import TSCBasic - -public enum PackagesOrManifest: Equatable { - case packages([Package]) - case manifest -} - -/// Contains the description of a dependency that can be installed using Swift Package Manager. -public struct SwiftPackageManagerDependencies: 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 - - /// 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] - - /// Initializes a new `SwiftPackageManagerDependencies` instance. - /// - Parameters: - /// - packagesOrManifest: The path to the `Package.swift` manifest defining the dependencies, or the list of packages - /// that will be installed using Swift Package Manager. - /// - 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( - _ packagesOrManifest: PackagesOrManifest, - productTypes: [String: Product], - baseSettings: Settings, - targetSettings: [String: SettingsDictionary], - projectOptions: [String: TuistGraph.Project.Options] = [:] - ) { - self.packagesOrManifest = packagesOrManifest - self.productTypes = productTypes - self.baseSettings = baseSettings - self.targetSettings = targetSettings - self.projectOptions = projectOptions - } -} - -extension SwiftPackageManagerDependencies { - public enum Manifest: Equatable { - case content(String) - case manifest - } - - /// Returns `Package.swift` representation. - public func manifest(isLegacy: Bool, packageManifestFolder: AbsolutePath) -> Manifest { - switch packagesOrManifest { - case let .packages(packages): - return .content( - """ - import PackageDescription - - let package = Package( - name: "PackageName", - dependencies: [ - \(packages.map { - let manifest = $0.manifestValue(isLegacy: isLegacy, packageManifestFolder: packageManifestFolder) - return manifest + "," - }.joined(separator: "\n ")) - ] - ) - """ - ) - case .manifest: - return .manifest - } - } -} - -// MARK: - Package.manifestValue() - -extension Package { - /// Returns `Package.swift` representation. - fileprivate func manifestValue(isLegacy: Bool, packageManifestFolder: AbsolutePath) -> String { - switch self { - case let .local(path): - return #".package(path: "\#(path.relative(to: packageManifestFolder))")"# - case let .remote(url, requirement): - let requirementManifestValue = isLegacy ? requirement.legacyManifestValue : requirement.manifestValue - return #".package(url: "\#(url)", \#(requirementManifestValue))"# - } - } -} - -// MARK: - Requirement.manifestValue() - -extension Requirement { - /// Returns `Package.swift` representation. - fileprivate var manifestValue: String { - switch self { - case let .exact(version): - return #"exact: "\#(version)""# - case let .upToNextMajor(version): - return #"from: "\#(version)""# - case let .upToNextMinor(version): - return #".upToNextMinor(from: "\#(version)")"# - case let .branch(branch): - return #"branch: "\#(branch)""# - case let .revision(revision): - return #"revision: "\#(revision)""# - case let .range(from, to): - return #""\#(from)" ..< "\#(to)""# - } - } - - /// Returns legacy `Package.swift` representation. - fileprivate var legacyManifestValue: String { - switch self { - case let .exact(version): - return #".exact("\#(version)")"# - case let .upToNextMajor(version): - return #".upToNextMajor(from: "\#(version)")"# - case let .upToNextMinor(version): - return #".upToNextMinor(from: "\#(version)")"# - case let .branch(branch): - return #".branch("\#(branch)")"# - case let .revision(revision): - return #".revision("\#(revision)")"# - case let .range(from, to): - return #""\#(from)" ..< "\#(to)""# - } - } -} diff --git a/Sources/TuistGraph/Models/DeploymentDevice.swift b/Sources/TuistGraph/Models/DeploymentDevice.swift deleted file mode 100644 index dc43656b280..00000000000 --- a/Sources/TuistGraph/Models/DeploymentDevice.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -// MARK: - DeploymentDevice - -public struct DeploymentDevice: OptionSet, Codable, Hashable { - public static let iphone = DeploymentDevice(rawValue: 1 << 0) - public static let ipad = DeploymentDevice(rawValue: 1 << 1) - public static let mac = DeploymentDevice(rawValue: 1 << 2) - public static let vision = DeploymentDevice(rawValue: iphone.rawValue | ipad.rawValue | mac.rawValue) - - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - - // All deployment devices. - public static var all: DeploymentDevice { - [.iphone, .ipad, .mac, .vision] - } -} diff --git a/Sources/TuistGraph/Models/DeploymentTarget.swift b/Sources/TuistGraph/Models/DeploymentTarget.swift deleted file mode 100644 index fb4ae0df748..00000000000 --- a/Sources/TuistGraph/Models/DeploymentTarget.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -// MARK: - DeploymentTarget - -public enum DeploymentTarget: Hashable, Codable { - case iOS(String) - case macOS(String) - case watchOS(String) - case tvOS(String) - case visionOS(String) - - public var platform: Platform { - switch self { - case .iOS: - return .iOS - case .macOS: - return .macOS - case .tvOS: - return .tvOS - case .visionOS: - return .visionOS - case .watchOS: - return .watchOS - } - } - - public var version: String { - switch self { - case let .iOS(version): return version - case let .macOS(version): return version - case let .watchOS(version): return version - case let .tvOS(version): return version - case let .visionOS(version): return version - } - } -} diff --git a/Sources/TuistGraph/Models/DeploymentTargets.swift b/Sources/TuistGraph/Models/DeploymentTargets.swift deleted file mode 100644 index 54db36d7226..00000000000 --- a/Sources/TuistGraph/Models/DeploymentTargets.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation - -// MARK: - DeploymentTargets - -public struct DeploymentTargets: Hashable, Codable { - public let iOS: String? - public let macOS: String? - public let watchOS: String? - public let tvOS: String? - public let visionOS: String? - - public init(iOS: String? = nil, macOS: String? = nil, watchOS: String? = nil, tvOS: String? = nil, visionOS: String? = nil) { - self.iOS = iOS - self.macOS = macOS - self.watchOS = watchOS - self.tvOS = tvOS - self.visionOS = visionOS - } - - public subscript(platform: Platform) -> String? { - switch platform { - case .iOS: - return iOS - case .macOS: - return macOS - case .watchOS: - return watchOS - case .tvOS: - return tvOS - case .visionOS: - return visionOS - } - } - - public var configuredVersions: [(platform: Platform, versionString: String)] { - var versions = [(Platform, String)]() - - if let iOS { - versions.append((.iOS, iOS)) - } - - if let macOS { - versions.append((.macOS, macOS)) - } - - if let watchOS { - versions.append((.watchOS, watchOS)) - } - - if let tvOS { - versions.append((.tvOS, tvOS)) - } - - if let visionOS { - versions.append((.visionOS, visionOS)) - } - - return versions - } - - public static func iOS(_ version: String) -> DeploymentTargets { - DeploymentTargets(iOS: version) - } - - public static func macOS(_ version: String) -> DeploymentTargets { - DeploymentTargets(macOS: version) - } - - public static func watchOS(_ version: String) -> DeploymentTargets { - DeploymentTargets(watchOS: version) - } - - public static func tvOS(_ version: String) -> DeploymentTargets { - DeploymentTargets(tvOS: version) - } - - public static func visionOS(_ version: String) -> DeploymentTargets { - DeploymentTargets(visionOS: version) - } - - public static func empty() -> DeploymentTargets { - DeploymentTargets() - } -} diff --git a/Sources/TuistGraph/Models/Destination.swift b/Sources/TuistGraph/Models/Destination.swift deleted file mode 100644 index 0d7b2028437..00000000000 --- a/Sources/TuistGraph/Models/Destination.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation - -public typealias Destinations = Set - -extension Destinations { - public static var watchOS: Destinations = [.appleWatch] - public static var iOS: Destinations = [.iPhone, .iPad, .macWithiPadDesign] - public static var macOS: Destinations = [.mac] - public static var tvOS: Destinations = [.appleTv] - public static var visionOS: Destinations = [.appleVision] -} - -extension Destinations { - public var platforms: Set { - let platforms = map(\.platform) - return Set(platforms) - } -} - -/// A supported platform representation. -public enum Destination: String, Codable, Equatable, CaseIterable { - case iPhone - case iPad - case mac - case macWithiPadDesign - case macCatalyst - case appleWatch - case appleTv - case appleVision - case appleVisionWithiPadDesign - - public var platform: Platform { - switch self { - case .iPad, .iPhone, .macCatalyst, .macWithiPadDesign, .appleVisionWithiPadDesign: - return .iOS - case .mac: - return .macOS - case .appleTv: - return .tvOS - case .appleWatch: - return .watchOS - case .appleVision: - return .visionOS - } - } - - public var platformFilter: PlatformFilter { - switch self { - case .iPad, .iPhone, .macWithiPadDesign, .appleVisionWithiPadDesign: - return .ios - case .macCatalyst: - return .catalyst - case .mac: - return .macos - case .appleTv: - return .tvos - case .appleWatch: - return .watchos - case .appleVision: - return .visionos - } - } -} - -extension Collection { - public func supports(_ platform: Platform) -> Bool { - contains(where: { $0.platform == platform }) - } -} diff --git a/Sources/TuistGraph/Models/EnvironmentVariable.swift b/Sources/TuistGraph/Models/EnvironmentVariable.swift deleted file mode 100644 index 9e40a70cbcf..00000000000 --- a/Sources/TuistGraph/Models/EnvironmentVariable.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// It represents an environment variable that is passed when running a scheme's action -public struct EnvironmentVariable: Equatable, Codable, Hashable, ExpressibleByStringLiteral { - // MARK: - Attributes - - /// The value of the environment variable - public let value: String - /// Whether the variable is enabled or not - public let isEnabled: Bool - - // MARK: - Init - - public init(value: String, isEnabled: Bool) { - self.value = value - self.isEnabled = isEnabled - } - - public init(stringLiteral value: StringLiteralType) { - self.value = value - isEnabled = true - } -} diff --git a/Sources/TuistGraph/Models/ExecutionAction.swift b/Sources/TuistGraph/Models/ExecutionAction.swift deleted file mode 100644 index 9aed0a32944..00000000000 --- a/Sources/TuistGraph/Models/ExecutionAction.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import TSCBasic - -/// A execution action -public struct ExecutionAction: Equatable, Codable { - // MARK: - Attributes - - /// Name of a script. - public let title: String - /// An inline shell script. - public let scriptText: String - /// Name of the build or test target that will provide the action's build settings. - public let 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 let showEnvVarsInLog: Bool - - // MARK: - Init - - public init( - title: String, - scriptText: String, - target: TargetReference?, - shellPath: String?, - showEnvVarsInLog: Bool = true - ) { - self.title = title - self.scriptText = scriptText - self.target = target - self.shellPath = shellPath - self.showEnvVarsInLog = showEnvVarsInLog - } -} diff --git a/Sources/TuistGraph/Models/FileCodeGen.swift b/Sources/TuistGraph/Models/FileCodeGen.swift deleted file mode 100644 index e947f212154..00000000000 --- a/Sources/TuistGraph/Models/FileCodeGen.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -/// FileCodeGen: Soure file code generation attribues -/// -/// - `public`: public codegen attribute `settings = {ATTRIBUTES = (codegen, )`} -/// - `private`: private codegen attribute `settings = {ATTRIBUTES = (private_codegen, )}` -/// - `project`: project codegen attribute `settings = {ATTRIBUTES = (project_codegen, )}` -/// - `disabled`: disabled codegen attribute `settings = {ATTRIBUTES = (no_codegen, )}` -/// -public enum FileCodeGen: String, Codable, Equatable { - case `public` = "codegen" - case `private` = "private_codegen" - case project = "project_codegen" - case disabled = "no_codegen" -} diff --git a/Sources/TuistGraph/Models/FileElement.swift b/Sources/TuistGraph/Models/FileElement.swift deleted file mode 100644 index bb2ed322b87..00000000000 --- a/Sources/TuistGraph/Models/FileElement.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import TSCBasic - -public enum FileElement: Equatable, Hashable, Codable { - case file(path: AbsolutePath) - case folderReference(path: AbsolutePath) - - public var path: AbsolutePath { - switch self { - case let .file(path): - return path - case let .folderReference(path): - return path - } - } - - public var isReference: Bool { - switch self { - case .file: - return false - case .folderReference: - return true - } - } -} diff --git a/Sources/TuistGraph/Models/Headers.swift b/Sources/TuistGraph/Models/Headers.swift deleted file mode 100644 index e78422075f5..00000000000 --- a/Sources/TuistGraph/Models/Headers.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import TSCBasic - -/// Headers -public struct Headers: Equatable, Codable { - // MARK: - Attributes - - public let `public`: [AbsolutePath] - public let `private`: [AbsolutePath] - public let project: [AbsolutePath] - - // MARK: - Init - - public init( - public: [AbsolutePath], - private: [AbsolutePath], - project: [AbsolutePath] - ) { - self.public = `public` - self.private = `private` - self.project = project - } -} diff --git a/Sources/TuistGraph/Models/IDETemplateMacros.swift b/Sources/TuistGraph/Models/IDETemplateMacros.swift deleted file mode 100644 index 59490b2af0e..00000000000 --- a/Sources/TuistGraph/Models/IDETemplateMacros.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -public struct IDETemplateMacros: Codable, Hashable { - private enum CodingKeys: String, CodingKey { - case fileHeader = "FILEHEADER" - } - - public let fileHeader: String? - - public init(fileHeader: String?) { - self.fileHeader = fileHeader.map(Self.normalize) - } - - private static func normalize(fileHeader: String) -> String { - var fileHeader = fileHeader - - // As Xcode appends file header directly after `//` it adds to the beginning of template, - // it is desired to also add leading space if it is not already present, - // but if header starts with `//` then I know what I am doing and it should be kept intact - if !fileHeader.hasPrefix("//"), let first = fileHeader.first.map(String.init), - first.trimmingCharacters(in: .whitespacesAndNewlines) == first - { - fileHeader.insert(" ", at: fileHeader.startIndex) - } - - // Xcode by default adds comment slashes to first line of header template - if fileHeader.hasPrefix("//") { - fileHeader.removeFirst(2) - } - - // Xcode by default adds extra newline at the end, so if it is present, just remove it - if fileHeader.last == "\n" { - fileHeader.removeLast() - } - - return fileHeader - } -} diff --git a/Sources/TuistGraph/Models/LaunchArgument.swift b/Sources/TuistGraph/Models/LaunchArgument.swift deleted file mode 100644 index 8ad8d6979de..00000000000 --- a/Sources/TuistGraph/Models/LaunchArgument.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -/// It represents an argument that is passed when running a scheme's action -public struct LaunchArgument: Equatable, Codable, Hashable { - // MARK: - Attributes - - /// The name of the launch argument - public let name: String - /// Whether the argument is enabled or not - public let isEnabled: Bool - - // MARK: - Init - - public init(name: String, isEnabled: Bool) { - self.name = name - self.isEnabled = isEnabled - } -} diff --git a/Sources/TuistGraph/Models/LaunchStyle.swift b/Sources/TuistGraph/Models/LaunchStyle.swift deleted file mode 100644 index 93e06b7c9de..00000000000 --- a/Sources/TuistGraph/Models/LaunchStyle.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public enum LaunchStyle: Codable { - case automatically - case waitForExecutableToBeLaunched -} diff --git a/Sources/TuistGraph/Models/MergedBinaryType.swift b/Sources/TuistGraph/Models/MergedBinaryType.swift deleted file mode 100644 index 2de57535084..00000000000 --- a/Sources/TuistGraph/Models/MergedBinaryType.swift +++ /dev/null @@ -1,37 +0,0 @@ -/// Represents the different options to configure a target for mergeable libraries -/// -/// https://developer.apple.com/documentation/xcode/configuring-your-project-to-use-mergeable-libraries -public enum MergedBinaryType: Equatable, Codable { - /// Target is never going to merge available dependencies - case disabled - - /// Target is going to merge direct target dependencies (just the ones declared as part of it's project). With this build - /// setting, - /// Xcode treats mergeable dependencies like normal dynamic libraries in debug builds, - /// but performs steps in release mode to automatically handle merging for **direct dependencies** - /// - /// A direct dependency is a library that meets two criteria: - /// - The library is listed in your target’s Link Binary with Libraries build phase. - /// - The library is the product of another target in your project. - case automatic - - /// Target is going to merge direct and specified dependencies that are not part of the project. The set of dependencies - /// is going to reflect the list of precompiled dynamic dependencies you want to merge as part of the target. These binaries - /// must be compiled with `MAKE_MERGEABLE` flag set to true - /// - /// In some cases, you may want to manually configure merging between your app or framework target and dependent libraries. - /// For example, you might not want to automatically merge dependencies that you share between an app and an app extension - /// if you’re concerned about the app extension’s binary size. To set up manual merging, configure your app or framework - /// target, - /// then configure your dependent libraries. - /// - /// In your app or framework target, add the flag `mergedBinaryType` and set it to manual. After you add that setting to your - /// target: - /// - In release builds, Xcode merges the products of any of its direct dependencies which have - /// MAKE_MERGEABLE enabled using the linker flags -merge_framework, -merge-l and so on. - /// - In debug builds, Xcode links any of your target’s direct dependencies which have MERGEABLE_LIBRARY - /// enabled, but not MAKE_MERGEABLE with the linker flags -reexport_framework, -reexport-l, and so on. - /// - Xcode uses normal linking for targets that don’t have MERGEABLE_LIBRARY enabled. This is the same linking - /// that Xcode uses for static libraries, or dynamic libraries that aren’t mergeable. - case manual(mergeableDependencies: Set) -} diff --git a/Sources/TuistGraph/Models/Metadata/FrameworkMetadata.swift b/Sources/TuistGraph/Models/Metadata/FrameworkMetadata.swift deleted file mode 100644 index 7b223652fe9..00000000000 --- a/Sources/TuistGraph/Models/Metadata/FrameworkMetadata.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import TSCBasic - -/// The metadata associated with a precompiled framework (.framework) -public struct FrameworkMetadata: Equatable { - public var path: AbsolutePath - public var binaryPath: AbsolutePath - public var dsymPath: AbsolutePath? - public var bcsymbolmapPaths: [AbsolutePath] - public var linking: BinaryLinking - public var architectures: [BinaryArchitecture] - public var isCarthage: Bool - public var status: FrameworkStatus - - public init( - path: AbsolutePath, - binaryPath: AbsolutePath, - dsymPath: AbsolutePath?, - bcsymbolmapPaths: [AbsolutePath], - linking: BinaryLinking, - architectures: [BinaryArchitecture], - isCarthage: Bool, - status: FrameworkStatus - ) { - self.path = path - self.binaryPath = binaryPath - self.dsymPath = dsymPath - self.bcsymbolmapPaths = bcsymbolmapPaths - self.linking = linking - self.architectures = architectures - self.isCarthage = isCarthage - self.status = status - } -} diff --git a/Sources/TuistGraph/Models/Metadata/LibraryMetadata.swift b/Sources/TuistGraph/Models/Metadata/LibraryMetadata.swift deleted file mode 100644 index eba1eabc992..00000000000 --- a/Sources/TuistGraph/Models/Metadata/LibraryMetadata.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import TSCBasic - -/// The metadata associated with a precompiled library (.a / .dylib) -public struct LibraryMetadata: Equatable { - public var path: AbsolutePath - public var publicHeaders: AbsolutePath - public var swiftModuleMap: AbsolutePath? - public var architectures: [BinaryArchitecture] - public var linking: BinaryLinking - - public init( - path: AbsolutePath, - publicHeaders: AbsolutePath, - swiftModuleMap: AbsolutePath?, - architectures: [BinaryArchitecture], - linking: BinaryLinking - ) { - self.path = path - self.publicHeaders = publicHeaders - self.swiftModuleMap = swiftModuleMap - self.architectures = architectures - self.linking = linking - } -} diff --git a/Sources/TuistGraph/Models/Metadata/SystemFrameworkMetadata.swift b/Sources/TuistGraph/Models/Metadata/SystemFrameworkMetadata.swift deleted file mode 100644 index 5055f531e8f..00000000000 --- a/Sources/TuistGraph/Models/Metadata/SystemFrameworkMetadata.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import TSCBasic - -/// The metadata associated with a system framework or library (e.g. UIKit.framework, libc++.tbd) -public struct SystemFrameworkMetadata: Equatable { - public var name: String - public var path: AbsolutePath - public var status: SDKStatus - public var source: SDKSource - - public init( - name: String, - path: AbsolutePath, - status: SDKStatus, - source: SDKSource - ) { - self.name = name - self.path = path - self.status = status - self.source = source - } -} diff --git a/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift b/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift deleted file mode 100644 index 21e78ffd11d..00000000000 --- a/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import TSCBasic - -/// The metadata associated with a precompiled xcframework -public struct XCFrameworkMetadata: Equatable { - public var path: AbsolutePath - public var infoPlist: XCFrameworkInfoPlist - public var primaryBinaryPath: AbsolutePath - public var linking: BinaryLinking - public var mergeable: Bool - public var status: FrameworkStatus - public var macroPath: AbsolutePath? - - 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 - self.macroPath = macroPath - } -} diff --git a/Sources/TuistGraph/Models/Package.swift b/Sources/TuistGraph/Models/Package.swift deleted file mode 100644 index f1b59f48b11..00000000000 --- a/Sources/TuistGraph/Models/Package.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import TSCBasic - -public enum Package: Equatable, Codable { - case remote(url: String, requirement: Requirement) - case local(path: AbsolutePath) -} - -extension TuistGraph.Package { - public var isRemote: Bool { - switch self { - case .remote: - return true - case .local: - return false - } - } -} diff --git a/Sources/TuistGraph/Models/Platform.swift b/Sources/TuistGraph/Models/Platform.swift deleted file mode 100644 index 87c00c41b56..00000000000 --- a/Sources/TuistGraph/Models/Platform.swift +++ /dev/null @@ -1,150 +0,0 @@ -import Foundation - -public enum Platform: String, CaseIterable, Codable, Comparable { - case iOS = "ios" - case macOS = "macos" - case tvOS = "tvos" - case watchOS = "watchos" - case visionOS = "visionos" - - public var caseValue: String { - switch self { - case .iOS: return "iOS" - case .macOS: return "macOS" - case .tvOS: return "tvOS" - case .watchOS: return "watchOS" - case .visionOS: return "visionOS" - } - } - - public static func < (lhs: Platform, rhs: Platform) -> Bool { - lhs.rawValue < rhs.rawValue - } -} - -public enum PackagePlatform: String, CaseIterable, Codable, Comparable { - case iOS = "ios" - case macCatalyst = "maccatalyst" - case macOS = "macos" - case tvOS = "tvos" - case watchOS = "watchos" - case visionOS = "visionos" - - public var caseValue: String { - switch self { - case .iOS: return "iOS" - case .macCatalyst: return "macCatalyst" - case .macOS: return "macOS" - case .tvOS: return "tvOS" - case .watchOS: return "watchOS" - case .visionOS: return "visionOS" - } - } - - public static func < (lhs: PackagePlatform, rhs: PackagePlatform) -> Bool { - lhs.rawValue < rhs.rawValue - } -} - -extension Platform { - public var xcodeSdkRoot: String { - switch self { - case .macOS: - return "macosx" - case .iOS: - return "iphoneos" - case .tvOS: - return "appletvos" - case .watchOS: - return "watchos" - case .visionOS: - return "xros" - } - } - - /// Returns whether the platform has simulators. - public var hasSimulators: Bool { - switch self { - case .macOS: return false - default: return true - } - } - - /// It returns the destination that should be used to - /// compile a product for this platform's simulator. - public var xcodeSimulatorDestination: String? { - switch self { - case .macOS: return nil - default: return "platform=\(caseValue) Simulator" - } - } - - /// Returns the SDK of the platform's simulator - /// If the platform doesn't have simulators, like macOS, it returns nil. - public var xcodeSimulatorSDK: String? { - switch self { - case .tvOS: return "appletvsimulator" - case .iOS: return "iphonesimulator" - case .watchOS: return "watchsimulator" - case .visionOS: return "xrsimulator" - case .macOS: return nil - } - } - - /// Returns the SDK to build for the platform's device. - public var xcodeDeviceSDK: String { - switch self { - case .tvOS: - return "appletvos" - case .iOS: - return "iphoneos" - case .macOS: - return "macosx" - case .watchOS: - return "watchos" - case .visionOS: - return "xros" - } - } - - /// The SDK Root Path within Xcode's developer directory - public var xcodeSdkRootPath: String { - switch self { - case .iOS: - return "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk" - case .macOS: - return "Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" - case .tvOS: - return "Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk" - case .watchOS: - return "Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk" - case .visionOS: - return "Platforms/XROS.platform/Developer/SDKs/XROS.sdk" - } - } - - public var xcodeDeveloperSdkRootPath: String { - switch self { - case .iOS: - return "Platforms/iPhoneOS.platform/Developer/Library" - case .macOS: - return "Platforms/MacOSX.platform/Developer/Library" - case .tvOS: - return "Platforms/AppleTVOS.platform/Developer/Library" - case .watchOS: - return "Platforms/WatchOS.platform/Developer/Library" - case .visionOS: - return "Platforms/XROS.platform/Developer/Library" - } - } - - /// Returns the directory name whose Carthage uses to save frameworks. - public var carthageDirectory: String { - switch self { - case .iOS, .watchOS, .tvOS, .visionOS: - return caseValue - case .macOS: - return "Mac" - } - } -} diff --git a/Sources/TuistGraph/Models/PlatformCondition.swift b/Sources/TuistGraph/Models/PlatformCondition.swift deleted file mode 100644 index 1833efeb18a..00000000000 --- a/Sources/TuistGraph/Models/PlatformCondition.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import TSCBasic - -public struct PlatformCondition: Codable, Hashable, Equatable, Comparable { - public static func < (lhs: PlatformCondition, rhs: PlatformCondition) -> Bool { - 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 - } - - public static func when(_ platformFilters: Set) -> PlatformCondition? { - guard !platformFilters.isEmpty else { return nil } - return PlatformCondition(platformFilters: platformFilters) - } - - public func intersection(_ other: PlatformCondition?) -> CombinationResult { - guard let otherFilters = other?.platformFilters else { return .condition(self) } - let filters = platformFilters.intersection(otherFilters) - - if filters.isEmpty { - return .incompatible - } else { - return .condition(PlatformCondition(platformFilters: filters)) - } - } - - public func union(_ other: PlatformCondition?) -> CombinationResult { - guard let otherFilters = other?.platformFilters else { return .condition(nil) } - let filters = platformFilters.union(otherFilters) - - if filters.isEmpty { - return .condition(nil) - } else { - return .condition(PlatformCondition(platformFilters: filters)) - } - } - - public enum CombinationResult: Equatable { - case incompatible - case condition(PlatformCondition?) - - public func combineWith(_ other: CombinationResult) -> CombinationResult { - switch (self, other) { - case (.incompatible, .incompatible): - return .incompatible - case (_, .incompatible): - return self - case (.incompatible, _): - return other - case let (.condition(lhs), .condition(rhs)): - guard let lhs, let rhs else { return .condition(nil) } - return lhs.union(rhs) - } - } - } -} diff --git a/Sources/TuistGraph/Models/PlatformFilter.swift b/Sources/TuistGraph/Models/PlatformFilter.swift deleted file mode 100644 index 00f2b2ed29e..00000000000 --- a/Sources/TuistGraph/Models/PlatformFilter.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation - -/// Convenience typealias to be used to ensure unique filters are applied -public typealias PlatformFilters = Set - -extension PlatformFilters: Comparable { - public static func < (lhs: Set, rhs: Set) -> Bool { - lhs.map(\.xcodeprojValue).sorted().joined() < rhs.map(\.xcodeprojValue).sorted().joined() - } -} - -/// Defines a set of platforms that can be used to limit where things -/// like build files, resources, and dependencies are used. -/// Context: https://github.com/tuist/tuist/pull/3152 -public enum PlatformFilter: Comparable, Hashable, Codable, CaseIterable { - case ios - case macos - case tvos - case catalyst - case driverkit - case watchos - case visionos - - public var xcodeprojValue: String { - switch self { - case .catalyst: - return "maccatalyst" - case .macos: - return "macos" - case .tvos: - return "tvos" - case .ios: - return "ios" - case .driverkit: - return "driverkit" - case .watchos: - return "watchos" - case .visionos: - 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 { - /// Examples - - /// This should not be here but downstream - /// `platformFilter = ios` - /// `platformFilters = (ios, xros, )` - public var xcodeprojValue: [String] { - map(\.xcodeprojValue).sorted() - } -} diff --git a/Sources/TuistGraph/Models/Plist.swift b/Sources/TuistGraph/Models/Plist.swift deleted file mode 100644 index 2b5eb101b9a..00000000000 --- a/Sources/TuistGraph/Models/Plist.swift +++ /dev/null @@ -1,161 +0,0 @@ -import Foundation -import TSCBasic - -// MARK: - Plist - -public enum Plist { - case infoPlist(InfoPlist) - case entitlements(Entitlements) - - public indirect enum Value: Equatable, Codable { - case string(String) - case integer(Int) - case real(Double) - case boolean(Bool) - case dictionary([String: Value]) - case array([Value]) - - public var value: Any { - switch self { - case let .array(array): - return array.map(\.value) - case let .boolean(boolean): - return boolean - case let .dictionary(dictionary): - return dictionary.mapValues { $0.value } - case let .integer(integer): - return integer - case let .string(string): - return string - case let .real(double): - return double - } - } - } -} - -// MARK: - Plist.Value - ExpressibleByStringLiteral - -extension Plist.Value: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self = .string(value) - } -} - -// MARK: - Plist.Value - ExpressibleByIntegerLiteral - -extension Plist.Value: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { - self = .integer(value) - } -} - -// MARK: - Plist.Value - ExpressibleByIntegerLiteral - -extension Plist.Value: ExpressibleByFloatLiteral { - public init(floatLiteral value: Double) { - self = .real(value) - } -} - -// MARK: - Plist.Value - ExpressibleByBooleanLiteral - -extension Plist.Value: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { - self = .boolean(value) - } -} - -// MARK: - Plist.Value - ExpressibleByDictionaryLiteral - -extension Plist.Value: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, Plist.Value)...) { - self = .dictionary(Dictionary(uniqueKeysWithValues: elements)) - } -} - -// MARK: - Plist.Value - ExpressibleByArrayLiteral - -extension Plist.Value: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: Plist.Value...) { - self = .array(elements) - } -} - -// MARK: - Dictionary (Plist.Value) - -extension Dictionary where Value == Plist.Value { - public func unwrappingValues() -> [Key: Any] { - mapValues { $0.value } - } -} - -// MARK: - InfoPlist - -public enum InfoPlist: Equatable, Codable { - // Path to a user defined info.plist file (already exists on disk). - case file(path: AbsolutePath) - - // Path to a generated info.plist file (may not exist on disk at the time of project generation). - // Data of the generated file - case generatedFile(path: AbsolutePath, data: Data) - - // User defined dictionary of keys/values for an info.plist file. - case dictionary([String: Plist.Value]) - - // User defined dictionary of keys/values for an info.plist file extending the default set of keys/values - // for the target type. - case extendingDefault(with: [String: Plist.Value]) - - // MARK: - Public - - public var path: AbsolutePath? { - switch self { - case let .file(path), let .generatedFile(path: path, data: _): - return path - default: - return nil - } - } -} - -// MARK: - InfoPlist - ExpressibleByStringLiteral - -extension InfoPlist: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self = .file(path: try! AbsolutePath(validating: value)) // swiftlint:disable:this force_try - } -} - -// MARK: - Entitlements - -public enum Entitlements: Equatable, Codable { - // Path to a user defined .entitlements file (already exists on disk). - case file(path: AbsolutePath) - - // Path to a generated .entitlements file (may not exist on disk at the time of project generation). - // Data of the generated file - case generatedFile(path: AbsolutePath, data: Data) - - // User defined dictionary of keys/values for an .entitlements file. - case dictionary([String: Plist.Value]) - - // MARK: - Public - - public var path: AbsolutePath? { - switch self { - case let .file(path), let .generatedFile(path: path, data: _): - return path - default: - return nil - } - } -} - -// MARK: - Entitlements - ExpressibleByStringLiteral - -extension Entitlements: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self = .file(path: try! AbsolutePath(validating: value)) // swiftlint:disable:this force_try - } -} diff --git a/Sources/TuistGraph/Models/Plugin.swift b/Sources/TuistGraph/Models/Plugin.swift deleted file mode 100644 index 4125503d171..00000000000 --- a/Sources/TuistGraph/Models/Plugin.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import TSCBasic - -/// A `Plugin` used to extend Tuist. -public struct Plugin: Equatable, Hashable { - /// The name of the plugin. - public let name: String - - /// Creates a `Plugin` - /// - /// - Parameters: - /// - name: The name of the plugin. - public init(name: String) { - self.name = name - } -} - -extension Plugin: CustomStringConvertible { - public var description: String { - "Plugin: \(name)" - } -} diff --git a/Sources/TuistGraph/Models/PluginResourceSynthesizer.swift b/Sources/TuistGraph/Models/PluginResourceSynthesizer.swift deleted file mode 100644 index 2137b86d707..00000000000 --- a/Sources/TuistGraph/Models/PluginResourceSynthesizer.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import TSCBasic - -/// Resource synthesizer plugin model -public struct PluginResourceSynthesizer: Equatable { - /// Name of the plugin - public let name: String - /// Path to `ResourceSynthesizers` directory where all resource templates are located - public let path: AbsolutePath - - public init( - name: String, - path: AbsolutePath - ) { - self.name = name - self.path = path - } -} diff --git a/Sources/TuistGraph/Models/Plugins.swift b/Sources/TuistGraph/Models/Plugins.swift deleted file mode 100644 index 7f14b5c394a..00000000000 --- a/Sources/TuistGraph/Models/Plugins.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import TSCBasic - -/// A model which contains all loaded plugin representations. -public struct Plugins: Equatable { - /// List of the loaded custom helper plugins. - public let projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin] - - /// List of paths to template definitions. - public let templateDirectories: [AbsolutePath] - - /// List of paths pointing to resource templates - public let resourceSynthesizers: [PluginResourceSynthesizer] - - /// Creates a `Plugins`. - /// - /// - Parameters: - /// - projectDescriptionHelpers: List of the loaded helper plugins. - /// - templatePaths: List of paths to the `Templates/` directory for the loaded plugins. - /// - resourceSynthesizers: List of the loaded resource synthesizer plugins - public init( - projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin], - templatePaths: [AbsolutePath], - resourceSynthesizers: [PluginResourceSynthesizer] - ) { - self.projectDescriptionHelpers = projectDescriptionHelpers - templateDirectories = templatePaths - self.resourceSynthesizers = resourceSynthesizers - } - - /// An empty `Plugins`. - public static let none: Plugins = .init( - projectDescriptionHelpers: [], - templatePaths: [], - resourceSynthesizers: [] - ) -} diff --git a/Sources/TuistGraph/Models/Product.swift b/Sources/TuistGraph/Models/Product.swift deleted file mode 100644 index c0c38986466..00000000000 --- a/Sources/TuistGraph/Models/Product.swift +++ /dev/null @@ -1,241 +0,0 @@ -import Foundation - -public enum Product: String, CustomStringConvertible, CaseIterable, Codable { - case app - case staticLibrary = "static_library" - case dynamicLibrary = "dynamic_library" - case framework - case staticFramework - case unitTests = "unit_tests" - case uiTests = "ui_tests" - case bundle - case commandLineTool - case appExtension = "app_extension" - // case watchApp = "watch_app" - case watch2App = "watch_2_app" - // case watchExtension = "watch_extension" - case watch2Extension = "watch_2_extension" - case tvTopShelfExtension = "tv_top_shelf_extension" - // case tvIntentsExtension = "tv_intents_extension" - // case messagesApplication = "messages_application" - case messagesExtension = "messages_extension" - case stickerPackExtension = "sticker_pack_extension" - case appClip - case xpc - case systemExtension = "system_extension" - case extensionKitExtension = "extension_kit_extension" - case macro - - public var caseValue: String { - switch self { - case .app: - return "app" - case .staticLibrary: - return "staticLibrary" - case .dynamicLibrary: - return "dynamicLibrary" - case .framework: - return "framework" - case .staticFramework: - return "staticFramework" - case .unitTests: - return "unitTests" - case .uiTests: - return "uiTests" - case .bundle: - return "bundle" - case .appExtension: - return "appExtension" - // case .watchApp: - // return "watchApp" - case .watch2App: - return "watch2App" - // case .watchExtension: - // return "watchExtension" - case .watch2Extension: - return "watch2Extension" - case .tvTopShelfExtension: - return "tvTopShelfExtension" - // case .tvIntentsExtension: - // return "tvIntentsExtension" - // case .messagesApplication: - // return "messagesApplication" - case .messagesExtension: - return "messagesExtension" - case .stickerPackExtension: - return "stickerPackExtension" - case .commandLineTool: - return "commandLineTool" - case .appClip: - return "appClip" - case .xpc: - return "xpc" - case .systemExtension: - return "systemExtension" - case .extensionKitExtension: - return "extensionKitExtension" - case .macro: - return "macro" - } - } - - public var description: String { - switch self { - case .app: - return "application" - case .staticLibrary: - return "static library" - case .dynamicLibrary: - return "dynamic library" - case .framework: - return "framework" - case .staticFramework: - return "staticFramework" - case .unitTests: - return "unit tests" - case .uiTests: - return "ui tests" - case .bundle: - return "bundle" - case .appExtension: - return "app extension" - // case .watchApp: - // return "watch application" - case .watch2App: - return "watch 2 application" - // case .watchExtension: - // return "watch extension" - case .watch2Extension: - return "watch 2 extension" - case .tvTopShelfExtension: - return "tv top shelf extension" - // case .tvIntentsExtension: - // return "tv intents extension" - // case .messagesApplication: - // return "iMessage application" - case .messagesExtension: - return "iMessage extension" - case .stickerPackExtension: - return "sticker pack extension" - case .commandLineTool: - return "command line tool" - case .appClip: - return "appClip" - case .xpc: - return "xpc" - case .systemExtension: - return "system extension" - case .extensionKitExtension: - return "extensionKit extension" - case .macro: - return "Swift Macro" - } - } - - /// Returns true if the target can be ran. - public var runnable: Bool { - switch self { - case - .app, - .appClip, - .commandLineTool, - .watch2App, - .appExtension, - .messagesExtension, - .stickerPackExtension, - .tvTopShelfExtension, - .watch2Extension, - .extensionKitExtension, - .macro: - return true - case - .bundle, - .systemExtension, - .dynamicLibrary, - .framework, - .staticFramework, - .staticLibrary, - .unitTests, - .uiTests, - .xpc: - return false - } - } - - /// Returns true if the product is a tests bundle. - public var testsBundle: Bool { - self == .uiTests || self == .unitTests - } - - public static func forPlatform(_ platform: Platform) -> Set { - var base: [Product] = [ - .app, - .staticLibrary, - .dynamicLibrary, - .framework, - ] - - if platform == .iOS { - base.append(.stickerPackExtension) - // base.append(.messagesApplication) - base.append(.messagesExtension) - base.append(.appClip) - } - if platform == .iOS || platform == .visionOS { - base.append(.appExtension) - } - - if platform == .tvOS { - base.append(.tvTopShelfExtension) - // base.append(.tvIntentsExtension) - } - - if platform == .macOS || - platform == .tvOS || - platform == .iOS || - platform == .visionOS - { - base.append(.unitTests) - } - - if platform == .macOS || - platform == .tvOS || - platform == .iOS - { - base.append(.uiTests) - } - - if platform == .macOS { - base.append(.commandLineTool) - base.append(.macro) - base.append(.xpc) - base.append(.systemExtension) - } - - // if platform == .watchOS { - // base.append(contentsOf: [ - // .watchApp, - // .watch2App, - // .watchExtension, - // .watch2Extension, - // ]) - // } - return Set(base) - } - - public var isStatic: Bool { - [.staticLibrary, .staticFramework].contains(self) - } - - public var isFramework: Bool { - [.framework, .staticFramework].contains(self) - } - - public var isDynamic: Bool { - [.framework, .dynamicLibrary].contains(self) - } - - public func canHostTests() -> Bool { - [.app, .appClip, .watch2App].contains(self) - } -} diff --git a/Sources/TuistGraph/Models/ProfileAction.swift b/Sources/TuistGraph/Models/ProfileAction.swift deleted file mode 100644 index 106943290fe..00000000000 --- a/Sources/TuistGraph/Models/ProfileAction.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -public struct ProfileAction: Equatable, Codable { - // MARK: - Attributes - - public let configurationName: String - public let preActions: [ExecutionAction] - public let postActions: [ExecutionAction] - public let executable: TargetReference? - public let arguments: Arguments? - - // MARK: - Init - - public init( - configurationName: String, - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - executable: TargetReference? = nil, - arguments: Arguments? = nil - ) { - self.configurationName = configurationName - self.preActions = preActions - self.postActions = postActions - self.executable = executable - self.arguments = arguments - } -} diff --git a/Sources/TuistGraph/Models/Project.swift b/Sources/TuistGraph/Models/Project.swift deleted file mode 100644 index 27e91eb8190..00000000000 --- a/Sources/TuistGraph/Models/Project.swift +++ /dev/null @@ -1,202 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility - -public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible, Codable { - // MARK: - Attributes - - /// Path to the folder that contains the project manifest. - public var path: AbsolutePath - - /// Path to the root of the project sources. - public var sourceRootPath: AbsolutePath - - /// Path to the Xcode project that will be generated. - public var xcodeProjPath: AbsolutePath - - /// Project name. - public var name: String - - /// Organization name. - public let organizationName: String? - - /// Default known regions - public let defaultKnownRegions: [String]? - - /// Development region code e.g. `en`. - public let developmentRegion: String? - - /// Additional project options. - public var options: Options - - /// Project targets. - public var targets: [Target] - - /// Project swift packages. - public var packages: [Package] - - /// Project schemes - public var schemes: [Scheme] - - /// Project settings. - public var settings: Settings - - /// The group to place project files within - public var filesGroup: ProjectGroup - - /// Additional files to include in the project - public var additionalFiles: [FileElement] - - /// IDE template macros that represent content of IDETemplateMacros.plist - public var ideTemplateMacros: IDETemplateMacros? - - /// `ResourceSynthesizers` that will be applied on individual target's resources - public let resourceSynthesizers: [ResourceSynthesizer] - - /// The version in which a check happened related to recommended settings after updating Xcode. - public var lastUpgradeCheck: Version? - - /// Indicates whether the project is imported through `Dependencies.swift`. - public let isExternal: Bool - - // MARK: - Init - - /// Initializes the project with its attributes. - /// - /// - Parameters: - /// - path: Path to the folder that contains the project manifest. - /// - sourceRootPath: Path to the directory where the Xcode project will be generated. - /// - xcodeProjPath: Path to the Xcode project that will be generated. - /// - name: Project name. - /// - organizationName: Organization name. - /// - defaultKnownRegions: Default known regions. - /// - developmentRegion: Development region. - /// - options: Additional project options. - /// - settings: The settings to apply at the project level - /// - filesGroup: The root group to place project files within - /// - targets: The project targets - /// *(Those won't be included in any build phases)* - /// - packages: Project swift packages. - /// - schemes: Project schemes. - /// - ideTemplateMacros: IDE template macros that represent content of IDETemplateMacros.plist. - /// - additionalFiles: The additional files to include in the project - /// - resourceSynthesizers: `ResourceSynthesizers` that will be applied on individual target's resources - /// - lastUpgradeCheck: The version in which a check happened related to recommended settings after updating Xcode. - /// - isExternal: Indicates whether the project is imported through `Dependencies.swift`. - public init( - path: AbsolutePath, - sourceRootPath: AbsolutePath, - xcodeProjPath: AbsolutePath, - name: String, - organizationName: String?, - defaultKnownRegions: [String]?, - developmentRegion: String?, - options: Options, - settings: Settings, - filesGroup: ProjectGroup, - targets: [Target], - packages: [Package], - schemes: [Scheme], - ideTemplateMacros: IDETemplateMacros?, - additionalFiles: [FileElement], - resourceSynthesizers: [ResourceSynthesizer], - lastUpgradeCheck: Version?, - isExternal: Bool - ) { - self.path = path - self.sourceRootPath = sourceRootPath - self.xcodeProjPath = xcodeProjPath - self.name = name - self.organizationName = organizationName - self.defaultKnownRegions = defaultKnownRegions - self.developmentRegion = developmentRegion - self.options = options - self.targets = targets - self.packages = packages - self.schemes = schemes - self.settings = settings - self.filesGroup = filesGroup - self.ideTemplateMacros = ideTemplateMacros - self.additionalFiles = additionalFiles - self.resourceSynthesizers = resourceSynthesizers - self.lastUpgradeCheck = lastUpgradeCheck - self.isExternal = isExternal - } - - // MARK: - CustomStringConvertible - - public var description: String { - name - } - - // MARK: - CustomDebugStringConvertible - - public var debugDescription: String { - name - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(path) - } - - // MARK: - Public - - /// Returns a copy of the project with the given targets set. - /// - Parameter targets: Targets to be set to the copy. - public func with(targets: [Target]) -> Project { - Project( - path: path, - sourceRootPath: sourceRootPath, - xcodeProjPath: xcodeProjPath, - name: name, - organizationName: organizationName, - defaultKnownRegions: defaultKnownRegions, - developmentRegion: developmentRegion, - options: options, - settings: settings, - filesGroup: filesGroup, - targets: targets, - packages: packages, - schemes: schemes, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles, - resourceSynthesizers: resourceSynthesizers, - lastUpgradeCheck: lastUpgradeCheck, - isExternal: isExternal - ) - } - - /// Returns a copy of the project with the given schemes set. - /// - Parameter schemes: Schemes to be set to the copy. - public func with(schemes: [Scheme]) -> Project { - Project( - path: path, - sourceRootPath: sourceRootPath, - xcodeProjPath: xcodeProjPath, - name: name, - organizationName: organizationName, - defaultKnownRegions: defaultKnownRegions, - developmentRegion: developmentRegion, - options: options, - settings: settings, - filesGroup: filesGroup, - targets: targets, - packages: packages, - schemes: schemes, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles, - resourceSynthesizers: resourceSynthesizers, - lastUpgradeCheck: lastUpgradeCheck, - isExternal: isExternal - ) - } - - /// Returns the name of the default configuration. - public var defaultDebugBuildConfigurationName: String { - let debugConfiguration = settings.defaultDebugBuildConfiguration() - let buildConfiguration = debugConfiguration ?? settings.configurations.keys.first - return buildConfiguration?.name ?? BuildConfiguration.debug.name - } -} diff --git a/Sources/TuistGraph/Models/ProjectGroup.swift b/Sources/TuistGraph/Models/ProjectGroup.swift deleted file mode 100644 index cebcdb1fd71..00000000000 --- a/Sources/TuistGraph/Models/ProjectGroup.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -public enum ProjectGroup: Hashable, Codable { - case group(name: String) -} diff --git a/Sources/TuistGraph/Models/ProjectOptions.swift b/Sources/TuistGraph/Models/ProjectOptions.swift deleted file mode 100644 index 219aae9cf40..00000000000 --- a/Sources/TuistGraph/Models/ProjectOptions.swift +++ /dev/null @@ -1,172 +0,0 @@ -import Foundation - -extension Project { - /// Additional options related to the `Project` - public struct Options: Codable, Hashable { - /// Defines how to generate automatic schemes - public let automaticSchemesOptions: AutomaticSchemesOptions - - /// Disables generating Bundle accessors. - public let disableBundleAccessors: Bool - - /// Tuist disables echoing the ENV in shell script build phases - public let disableShowEnvironmentVarsInScriptPhases: Bool - - /// Disable the synthesized resource accessors generation - public let disableSynthesizedResourceAccessors: Bool - - /// Text settings to override user ones for current project - public let textSettings: TextSettings - - public init( - automaticSchemesOptions: AutomaticSchemesOptions, - disableBundleAccessors: Bool, - disableShowEnvironmentVarsInScriptPhases: Bool, - disableSynthesizedResourceAccessors: Bool, - textSettings: TextSettings - ) { - self.automaticSchemesOptions = automaticSchemesOptions - self.disableBundleAccessors = disableBundleAccessors - self.disableShowEnvironmentVarsInScriptPhases = disableShowEnvironmentVarsInScriptPhases - self.disableSynthesizedResourceAccessors = disableSynthesizedResourceAccessors - self.textSettings = textSettings - } - } -} - -// MARK: - AutomaticSchemesOptions - -extension Project.Options { - /// The automatic schemes options - public enum AutomaticSchemesOptions: Codable, Hashable { - /// Defines how to group targets into scheme for autogenerated schemes - public enum TargetSchemesGrouping: Codable, Hashable { - /// Generate a single scheme per project - case singleScheme - - /// Group the targets according to their name suffixes - case byNameSuffix(build: Set, test: Set, run: Set) - - /// Do not group targets, create a scheme for each target - case notGrouped - } - - /// Enable autogenerated schemes - case enabled( - targetSchemesGrouping: TargetSchemesGrouping, - codeCoverageEnabled: Bool, - testingOptions: TestingOptions, - testLanguage: String? = nil, - testRegion: String? = nil, - testScreenCaptureFormat: ScreenCaptureFormat? = nil, - runLanguage: String? = nil, - runRegion: String? = nil - ) - - /// Disable autogenerated schemes - case disabled - } - - /// The text settings options - public struct TextSettings: Codable, Hashable { - /// Whether tabs should be used instead of spaces - public let usesTabs: Bool? - - /// The width of space indent - public let indentWidth: UInt? - - /// The width of tab indent - public let tabWidth: UInt? - - /// Whether lines should be wrapped or not - public let wrapsLines: Bool? - - public init( - usesTabs: Bool?, - indentWidth: UInt?, - tabWidth: UInt?, - wrapsLines: Bool? - ) { - self.usesTabs = usesTabs - self.indentWidth = indentWidth - self.tabWidth = tabWidth - self.wrapsLines = wrapsLines - } - } -} - -// MARK: - Array + ProjectOption - -extension Project.Options { - public var targetSchemesGrouping: AutomaticSchemesOptions.TargetSchemesGrouping? { - switch automaticSchemesOptions { - case let .enabled(targetSchemesGrouping, _, _, _, _, _, _, _): - return targetSchemesGrouping - case .disabled: - return nil - } - } - - public var codeCoverageEnabled: Bool { - switch automaticSchemesOptions { - case let .enabled(_, codeCoverageEnabled, _, _, _, _, _, _): - return codeCoverageEnabled - case .disabled: - return false - } - } - - public var testingOptions: TestingOptions { - switch automaticSchemesOptions { - case let .enabled(_, _, testingOptions, _, _, _, _, _): - return testingOptions - case .disabled: - return [] - } - } - - public var testLanguage: String? { - switch automaticSchemesOptions { - case let .enabled(_, _, _, language, _, _, _, _): - return language - case .disabled: - return nil - } - } - - public var testRegion: String? { - switch automaticSchemesOptions { - case let .enabled(_, _, _, _, region, _, _, _): - return region - case .disabled: - return nil - } - } - - public var testScreenCaptureFormat: ScreenCaptureFormat? { - switch automaticSchemesOptions { - case let .enabled(_, _, _, _, _, testScreenCaptureFormat, _, _): - return testScreenCaptureFormat - case .disabled: - return nil - } - } - - public var runLanguage: String? { - switch automaticSchemesOptions { - case let .enabled(_, _, _, _, _, _, language, _): - return language - case .disabled: - return nil - } - } - - public var runRegion: String? { - switch automaticSchemesOptions { - case let .enabled(_, _, _, _, _, _, _, region): - return region - case .disabled: - return nil - } - } -} diff --git a/Sources/TuistGraph/Models/RawScriptBuildPhase.swift b/Sources/TuistGraph/Models/RawScriptBuildPhase.swift deleted file mode 100644 index 63c337ad922..00000000000 --- a/Sources/TuistGraph/Models/RawScriptBuildPhase.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import TSCBasic - -/// It represents a raw script build phase. -public struct RawScriptBuildPhase: Equatable, Codable { - /// The name of the build phase. - public let name: String - - /// Script. - public let script: String - - /// Whether we want the build phase to show the environment variables in the logs. - public let showEnvVarsInLog: Bool - - /// Whether the script should be hashed for caching purposes. - public let hashable: Bool - - /// The path to the shell which shall execute this script. - public let shellPath: String - - /// Initializes the target script. - /// - Parameter name: The name of the build phase. - /// - Parameter script: Script. - /// - Parameter showEnvVarsInLog: Whether we want the build phase to show the environment variables in the logs. - /// - Parameter hashable: Whether the script should be hashed for caching purposes. - /// - Parameter shellPath: The path to the shell which shall execute this script. Default is `/bin/sh`. - public init( - name: String, - script: String, - showEnvVarsInLog: Bool, - hashable: Bool, - shellPath: String = "/bin/sh" - ) { - self.name = name - self.script = script - self.showEnvVarsInLog = showEnvVarsInLog - self.hashable = hashable - self.shellPath = shellPath - } -} diff --git a/Sources/TuistGraph/Models/Requirement.swift b/Sources/TuistGraph/Models/Requirement.swift deleted file mode 100644 index 6b3852f39dc..00000000000 --- a/Sources/TuistGraph/Models/Requirement.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -public enum Requirement: Equatable, Codable { - case upToNextMajor(String) - case upToNextMinor(String) - case range(from: String, to: String) - case exact(String) - case branch(String) - case revision(String) -} diff --git a/Sources/TuistGraph/Models/ResourceFileElement.swift b/Sources/TuistGraph/Models/ResourceFileElement.swift deleted file mode 100644 index 9c5b9b27304..00000000000 --- a/Sources/TuistGraph/Models/ResourceFileElement.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import TSCBasic - -public enum ResourceFileElement: Equatable, Hashable, Codable { - /// A file path (or glob pattern) to include, a list of file paths (or glob patterns) to exclude, ODR tags list and inclusion - /// condition. - /// For convenience, a string literal can be used as an alternate way to specify this option. - case file(path: AbsolutePath, tags: [String] = [], inclusionCondition: PlatformCondition? = nil) - /// A directory path to include as a folder reference, ODR tags list and inclusion condition. - case folderReference(path: AbsolutePath, tags: [String] = [], inclusionCondition: PlatformCondition? = nil) - - public var path: AbsolutePath { - switch self { - case let .file(path, _, _): - return path - case let .folderReference(path, _, _): - return path - } - } - - public var isReference: Bool { - switch self { - case .file: - return false - case .folderReference: - return true - } - } - - public var tags: [String] { - switch self { - case let .file(_, tags, _): - return tags - case let .folderReference(_, tags, _): - return tags - } - } - - public var inclusionCondition: PlatformCondition? { - switch self { - case let .file(_, _, condition): - return condition - case let .folderReference(_, _, condition): - return condition - } - } - - public init(path: AbsolutePath) { - self = .file(path: path) - } -} - -extension [TuistGraph.ResourceFileElement] { - public mutating func remove(path: AbsolutePath) { - guard let index = firstIndex(of: TuistGraph.ResourceFileElement(path: path)) else { return } - remove(at: index) - } -} diff --git a/Sources/TuistGraph/Models/ResourceSynthesizer.swift b/Sources/TuistGraph/Models/ResourceSynthesizer.swift deleted file mode 100644 index 9905c3759a6..00000000000 --- a/Sources/TuistGraph/Models/ResourceSynthesizer.swift +++ /dev/null @@ -1,96 +0,0 @@ -import AnyCodable -import Foundation -import TSCBasic - -public struct ResourceSynthesizer: Equatable, Hashable, Codable { - public let parser: Parser - public let parserOptions: [String: Parser.Option] - public let extensions: Set - public let template: Template - - public enum Template: Equatable, Hashable, Codable { - case file(AbsolutePath) - case defaultTemplate(String) - } - - public enum Parser: String, Equatable, Hashable, Codable { - case strings - case assets - case plists - case fonts - case coreData - case interfaceBuilder - case json - case yaml - case files - - public struct Option: Equatable, Hashable, Codable { - public var value: Any { anyCodableValue.value } - private let anyCodableValue: AnyCodable - - public init(value: some Any) { - anyCodableValue = AnyCodable(value) - } - } - } - - public init( - parser: Parser, - parserOptions: [String: Parser.Option], - extensions: Set, - template: Template - ) { - self.parser = parser - self.parserOptions = parserOptions - self.extensions = extensions - self.template = template - } -} - -// MARK: - ResourceSynthesizer.Parser.Option - ExpressibleByStringInterpolation - -extension ResourceSynthesizer.Parser.Option: ExpressibleByStringInterpolation { - public init(stringLiteral value: String) { - self = .init(value: value) - } -} - -// MARK: - ResourceSynthesizer.Parser.Option - ExpressibleByIntegerLiteral - -extension ResourceSynthesizer.Parser.Option: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { - self = .init(value: value) - } -} - -// MARK: - ResourceSynthesizer.Parser.Option - ExpressibleByFloatLiteral - -extension ResourceSynthesizer.Parser.Option: ExpressibleByFloatLiteral { - public init(floatLiteral value: Double) { - self = .init(value: value) - } -} - -// MARK: - ResourceSynthesizer.Parser.Option - ExpressibleByBooleanLiteral - -extension ResourceSynthesizer.Parser.Option: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { - self = .init(value: value) - } -} - -// MARK: - ResourceSynthesizer.Parser.Option - ExpressibleByDictionaryLiteral - -extension ResourceSynthesizer.Parser.Option: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, Self)...) { - self = .init(value: Dictionary(uniqueKeysWithValues: elements)) - } -} - -// MARK: - ResourceSynthesizer.Parser.Option - ExpressibleByArrayLiteral - -extension ResourceSynthesizer.Parser.Option: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: Self...) { - self = .init(value: elements) - } -} diff --git a/Sources/TuistGraph/Models/RunAction.swift b/Sources/TuistGraph/Models/RunAction.swift deleted file mode 100644 index 76232050a1a..00000000000 --- a/Sources/TuistGraph/Models/RunAction.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import TSCBasic - -public struct RunAction: Equatable, Codable { - // MARK: - Attributes - - public let configurationName: String - public let attachDebugger: Bool - public let customLLDBInitFile: AbsolutePath? - public let preActions: [ExecutionAction] - public let postActions: [ExecutionAction] - public let executable: TargetReference? - public let filePath: AbsolutePath? - public let arguments: Arguments? - public let options: RunActionOptions - public let diagnosticsOptions: Set - public let expandVariableFromTarget: TargetReference? - public let launchStyle: LaunchStyle - - // MARK: - Init - - public init( - configurationName: String, - attachDebugger: Bool, - customLLDBInitFile: AbsolutePath?, - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - executable: TargetReference?, - filePath: AbsolutePath?, - arguments: Arguments?, - options: RunActionOptions = .init(), - diagnosticsOptions: Set, - expandVariableFromTarget: TargetReference? = nil, - launchStyle: LaunchStyle = .automatically - ) { - self.configurationName = configurationName - self.attachDebugger = attachDebugger - self.customLLDBInitFile = customLLDBInitFile - self.preActions = preActions - self.postActions = postActions - self.executable = executable - self.filePath = filePath - self.arguments = arguments - self.options = options - self.diagnosticsOptions = diagnosticsOptions - self.expandVariableFromTarget = expandVariableFromTarget - self.launchStyle = launchStyle - } -} diff --git a/Sources/TuistGraph/Models/RunActionOptions.swift b/Sources/TuistGraph/Models/RunActionOptions.swift deleted file mode 100644 index 2ae79b94435..00000000000 --- a/Sources/TuistGraph/Models/RunActionOptions.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation -import TSCBasic - -/// Options for the `RunAction` action -public struct RunActionOptions: Equatable, Codable { - /// App Language. - public let language: String? - - /// App Region. - public let 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: AbsolutePath? - - /// A simulated location used when running the provided run action. - public let simulatedLocation: SimulatedLocation? - - /// Configure your project to work with the Metal frame debugger. - public let enableGPUFrameCaptureMode: GPUFrameCaptureMode - - /// Creates an `RunActionOptions` instance - /// - /// - Parameters: - /// - language: language (e.g. "pl"). - /// - /// - storeKitConfigurationPath: The absolute path of the - /// [StoreKit configuration - /// file](https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode#3625700). - /// The default value is `nil`, which results in no - /// configuration defined for the scheme - /// - /// - simulatedLocation: The simulated GPS location to use when running the app. - /// - /// - enableGPUFrameCaptureMode: The Metal Frame Capture mode to use. e.g: .disabled - /// If your target links to the Metal framework, Xcode enables GPU Frame Capture. - /// You can disable it to test your app in best perfomance. - - public init( - language: String? = nil, - region: String? = nil, - storeKitConfigurationPath: AbsolutePath? = nil, - simulatedLocation: SimulatedLocation? = nil, - enableGPUFrameCaptureMode: GPUFrameCaptureMode = .autoEnabled - ) { - self.language = language - self.region = region - self.storeKitConfigurationPath = storeKitConfigurationPath - self.simulatedLocation = simulatedLocation - self.enableGPUFrameCaptureMode = enableGPUFrameCaptureMode - } -} - -extension RunActionOptions { - public enum SimulatedLocation { - case gpxFile(AbsolutePath) - case reference(String) - - /// A unique identifier string for the selected simulated location. - /// - /// In case of Xcode's simulated locations, this is a string representing the location. - /// In case of a custom GPX file, this is a path to that file. - public var identifier: String { - switch self { - case let .gpxFile(path): - return path.pathString - case let .reference(identifier): - return identifier - } - } - - /// A reference type is 1 if using Xcode's built-in simulated locations. - /// Otherwise, it is 0. - public var referenceType: String { - if case .gpxFile = self { return "0" } - return "1" - } - } -} - -extension RunActionOptions.SimulatedLocation: Equatable, Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let value = try container.decode(String.self) - - guard value.hasSuffix(".gpx") else { - self = .reference(value) - return - } - - self = .gpxFile(try AbsolutePath(validating: value)) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(identifier) - } -} - -extension RunActionOptions { - public enum GPUFrameCaptureMode: String, Codable, Equatable { - case autoEnabled - case metal - case openGL - case disabled - - public static var `default`: GPUFrameCaptureMode { - .autoEnabled - } - } -} diff --git a/Sources/TuistGraph/Models/SDKSource.swift b/Sources/TuistGraph/Models/SDKSource.swift deleted file mode 100644 index 2c52a66e931..00000000000 --- a/Sources/TuistGraph/Models/SDKSource.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -public enum SDKSource: String, Equatable, Codable { - case developer // Platforms/iPhoneOS.platform/Developer/Library - case system // Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library - - /// Returns the framework search path that should be used in Xcode to locate the SDK. - public var frameworkSearchPath: String? { - switch self { - case .developer: - return "$(PLATFORM_DIR)/Developer/Library/Frameworks" - case .system: - return nil - } - } -} diff --git a/Sources/TuistGraph/Models/SDKType.swift b/Sources/TuistGraph/Models/SDKType.swift deleted file mode 100644 index 7d970cece76..00000000000 --- a/Sources/TuistGraph/Models/SDKType.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -public enum SDKType: String, CaseIterable, Equatable { - case framework - case library = "tbd" - - public static var supportedTypesDescription: String { - let supportedTypes = allCases - .map { ".\($0.rawValue)" } - .joined(separator: ", ") - return "[\(supportedTypes)]" - } -} diff --git a/Sources/TuistGraph/Models/Scheme.swift b/Sources/TuistGraph/Models/Scheme.swift deleted file mode 100644 index 4e1716fd53e..00000000000 --- a/Sources/TuistGraph/Models/Scheme.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import TSCBasic - -public struct Scheme: Equatable, Codable { - // MARK: - Attributes - - public var name: String - public var shared: Bool - public var hidden: Bool - public var buildAction: BuildAction? - public var testAction: TestAction? - public var runAction: RunAction? - public var archiveAction: ArchiveAction? - public var profileAction: ProfileAction? - public var analyzeAction: AnalyzeAction? - - // MARK: - Init - - public init( - name: String, - shared: Bool = false, - hidden: Bool = false, - buildAction: BuildAction? = nil, - testAction: TestAction? = nil, - runAction: RunAction? = nil, - archiveAction: ArchiveAction? = nil, - profileAction: ProfileAction? = nil, - analyzeAction: AnalyzeAction? = nil - ) { - self.name = name - self.shared = shared - self.hidden = hidden - self.buildAction = buildAction - self.testAction = testAction - self.runAction = runAction - self.archiveAction = archiveAction - self.profileAction = profileAction - self.analyzeAction = analyzeAction - } -} diff --git a/Sources/TuistGraph/Models/SchemeDiagnosticsOption.swift b/Sources/TuistGraph/Models/SchemeDiagnosticsOption.swift deleted file mode 100644 index 4cab0bf5620..00000000000 --- a/Sources/TuistGraph/Models/SchemeDiagnosticsOption.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -public enum SchemeDiagnosticsOption: String, Equatable, Codable { - case enableAddressSanitizer - case enableASanStackUseAfterReturn - case enableThreadSanitizer - case mainThreadChecker - case performanceAntipatternChecker -} diff --git a/Sources/TuistGraph/Models/ScreenCaptureFormat.swift b/Sources/TuistGraph/Models/ScreenCaptureFormat.swift deleted file mode 100644 index 49e7f59afe7..00000000000 --- a/Sources/TuistGraph/Models/ScreenCaptureFormat.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public enum ScreenCaptureFormat: String, Codable { - case screenshots - case screenRecording -} diff --git a/Sources/TuistGraph/Models/Settings.swift b/Sources/TuistGraph/Models/Settings.swift deleted file mode 100644 index 5684ce288ad..00000000000 --- a/Sources/TuistGraph/Models/Settings.swift +++ /dev/null @@ -1,182 +0,0 @@ -import Foundation -import TSCBasic - -public typealias SettingsDictionary = [String: SettingValue] - -public enum SettingValue: ExpressibleByStringInterpolation, ExpressibleByStringLiteral, ExpressibleByArrayLiteral, Equatable, - Codable -{ - case string(String) - case array([String]) - - public init(stringLiteral value: String) { - self = .string(value) - } - - public init(arrayLiteral elements: String...) { - self = .array(elements) - } - - public func normalize() -> SettingValue { - switch self { - case let .array(currentValue): - if currentValue.count == 1 { - return .string(currentValue[0]) - } - return self - case .string: - return self - } - } -} - -extension SettingsDictionary { - /// Overlays a SettingsDictionary by adding a `[sdk=*]` qualifier - /// e.g. for a multiplatform target - /// `LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks` - /// `LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = @executable_path/../Frameworks` - public mutating func overlay( - with other: SettingsDictionary, - for platform: Platform - ) { - for (key, newValue) in other { - if self[key] == nil { - self[key] = newValue - } else if self[key] != newValue { - let newKey = "\(key)[sdk=\(platform.xcodeSdkRoot)*]" - self[newKey] = newValue - if platform.hasSimulators, let simulatorSDK = platform.xcodeSimulatorSDK { - let newKey = "\(key)[sdk=\(simulatorSDK)*]" - self[newKey] = newValue - } - } - } - } -} - -public struct Configuration: Equatable, Codable { - // MARK: - Attributes - - public let settings: SettingsDictionary - public let xcconfig: AbsolutePath? - - // MARK: - Init - - public init(settings: SettingsDictionary = [:], xcconfig: AbsolutePath? = nil) { - self.settings = settings - self.xcconfig = xcconfig - } - - // MARK: - Public - - /// Returns a copy of the configuration with the given settings set. - /// - Parameter settings: SettingsDictionary to be set to the copy. - public func with(settings: SettingsDictionary) -> Configuration { - Configuration( - settings: settings, - xcconfig: xcconfig - ) - } -} - -public enum DefaultSettings: Codable, Equatable { - case recommended(excluding: Set = []) - case essential(excluding: Set = []) - case none -} - -extension DefaultSettings { - public static var recommended: DefaultSettings { - .recommended(excluding: []) - } - - public static var essential: DefaultSettings { - .essential(excluding: []) - } -} - -public struct Settings: Equatable, Codable { - public static let `default` = Settings( - configurations: [.release: nil, .debug: nil], - defaultSettings: .recommended - ) - - // 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 - - // MARK: - Init - - public init( - base: SettingsDictionary = [:], - baseDebug: SettingsDictionary = [:], - configurations: [BuildConfiguration: Configuration?], - defaultSettings: DefaultSettings = .recommended - ) { - self.base = base - self.baseDebug = baseDebug - self.configurations = configurations - self.defaultSettings = defaultSettings - } - - public func with(base: SettingsDictionary) -> Settings { - .init( - base: base, - configurations: configurations, - defaultSettings: defaultSettings - ) - } -} - -extension Settings { - /// Finds the default debug `BuildConfiguration` if it exists, otherwise returns the first debug configuration available. - /// - /// - Returns: The default debug `BuildConfiguration` - public func defaultDebugBuildConfiguration() -> BuildConfiguration? { - let debugConfigurations = configurations.keys - .filter { $0.variant == .debug } - .sorted() - let defaultConfiguration = debugConfigurations.first(where: { $0 == BuildConfiguration.debug }) - return defaultConfiguration ?? debugConfigurations.first - } - - /// Finds the default release `BuildConfiguration` if it exists, otherwise returns the first release configuration available. - /// - /// - Returns: The default release `BuildConfiguration` - public func defaultReleaseBuildConfiguration() -> BuildConfiguration? { - let releaseConfigurations = configurations.keys - .filter { $0.variant == .release } - .sorted() - let defaultConfiguration = releaseConfigurations.first(where: { $0 == BuildConfiguration.release }) - return defaultConfiguration ?? releaseConfigurations.first - } -} - -extension [BuildConfiguration: Configuration?] { - public func sortedByBuildConfigurationName() -> [(key: BuildConfiguration, value: Configuration?)] { - sorted(by: { first, second -> Bool in first.key < second.key }) - } - - public func xcconfigs() -> [AbsolutePath] { - sortedByBuildConfigurationName() - .map(\.value) - .compactMap { $0?.xcconfig } - } -} - -extension [String: SettingValue] { - public func toAny() -> [String: Any] { - mapValues { value in - switch value { - case let .array(array): - return array - case let .string(string): - return string - } - } - } -} diff --git a/Sources/TuistGraph/Models/SourceFile.swift b/Sources/TuistGraph/Models/SourceFile.swift deleted file mode 100644 index b80872afc46..00000000000 --- a/Sources/TuistGraph/Models/SourceFile.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import TSCBasic - -/// A type that represents a source file. -public struct SourceFile: ExpressibleByStringLiteral, Equatable, Codable { - /// Source file path. - public var path: AbsolutePath - - /// Compiler flags - /// When source files are added to a target, they can contain compiler flags that Xcode's build system - /// passes to the compiler when compiling those files. By default none is passed. - public var compilerFlags: String? - - /// This is intended to be used by the mappers that generate files through side effects. - /// This attribute is used by the content hasher used by the caching functionality. - public var contentHash: String? - - /// Source file code generation attribute - public let codeGen: FileCodeGen? - - /// Source file condition for platform filters - public let compilationCondition: PlatformCondition? - - public init( - path: AbsolutePath, - compilerFlags: String? = nil, - contentHash: String? = nil, - codeGen: FileCodeGen? = nil, - compilationCondition: PlatformCondition? = nil - ) { - self.path = path - self.compilerFlags = compilerFlags - self.contentHash = contentHash - self.codeGen = codeGen - self.compilationCondition = compilationCondition - } - - // MARK: - ExpressibleByStringLiteral - - public init(stringLiteral value: String) { - path = try! AbsolutePath(validating: value) // swiftlint:disable:this force_try - compilerFlags = nil - contentHash = nil - codeGen = nil - compilationCondition = nil - } -} diff --git a/Sources/TuistGraph/Models/SourceFileGlob.swift b/Sources/TuistGraph/Models/SourceFileGlob.swift deleted file mode 100644 index 5c142f48117..00000000000 --- a/Sources/TuistGraph/Models/SourceFileGlob.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation - -/// A type that represents a list of source files defined by a glob. -public struct SourceFileGlob: Equatable { - /// Glob pattern to unfold all the source files. - public let glob: String - - /// Glob pattern used for filtering out files. - public let excluding: [String] - - /// Compiler flags. - public let compilerFlags: String? - - /// Source file code generation attribute. - public let codeGen: FileCodeGen? - - /// Compilation condition the source file. - public let compilationCondition: PlatformCondition? - - /// Initializes the source file glob. - /// - Parameters: - /// - glob: Glob pattern to unfold all the source files. - /// - excluding: Glob pattern used for filtering out files. - /// - compilerFlags: Compiler flags. - /// - codeGen: Source file code generation attribute. - /// - compilationCondition: Condition for file compilation. - public init( - glob: String, - excluding: [String] = [], - compilerFlags: String? = nil, - codeGen: FileCodeGen? = nil, - compilationCondition: PlatformCondition? = nil - ) { - self.glob = glob - self.excluding = excluding - self.compilerFlags = compilerFlags - self.codeGen = codeGen - self.compilationCondition = compilationCondition - } -} diff --git a/Sources/TuistGraph/Models/Target.swift b/Sources/TuistGraph/Models/Target.swift deleted file mode 100644 index 8589b391c84..00000000000 --- a/Sources/TuistGraph/Models/Target.swift +++ /dev/null @@ -1,362 +0,0 @@ -import Foundation -import TSCBasic - -// swiftlint:disable:next type_body_length -public struct Target: Equatable, Hashable, Comparable, Codable { - // MARK: - Static - - // Note: The `.docc` file type is technically both a valid source extension and folder extension - // in order to compile the documentation archive (including Tutorials, Articles, etc.) - public static let validSourceExtensions: [String] = [ - "m", "swift", "mm", "cpp", "cc", "c", "d", "s", "intentdefinition", "xcmappingmodel", "metal", "mlmodel", "docc", - "playground", "rcproject", "mlpackage", - ] - public static let validFolderExtensions: [String] = [ - "framework", "bundle", "app", "xcassets", "appiconset", "scnassets", - ] - - // MARK: - Attributes - - public var name: String - public var destinations: Destinations - public var product: Product - public var bundleId: String - public var productName: String - public var deploymentTargets: DeploymentTargets - - // An info.plist file is needed for (dynamic) frameworks, applications and executables - // however is not needed for other products such as static libraries. - public var infoPlist: InfoPlist? - public var entitlements: Entitlements? - public var settings: Settings? - public var dependencies: [TargetDependency] - public var sources: [SourceFile] - public var resources: [ResourceFileElement] - public var copyFiles: [CopyFilesAction] - public var headers: Headers? - public var coreDataModels: [CoreDataModel] - public var scripts: [TargetScript] - public var environmentVariables: [String: EnvironmentVariable] - public var launchArguments: [LaunchArgument] - public var filesGroup: ProjectGroup - public var rawScriptBuildPhases: [RawScriptBuildPhase] - public var playgrounds: [AbsolutePath] - public let additionalFiles: [FileElement] - public var buildRules: [BuildRule] - public var prune: Bool - public let mergedBinaryType: MergedBinaryType - public let mergeable: Bool - - // MARK: - Init - - public init( - name: String, - destinations: Destinations, - product: Product, - productName: String?, - bundleId: String, - deploymentTargets: DeploymentTargets = DeploymentTargets(), - infoPlist: InfoPlist? = nil, - entitlements: Entitlements? = nil, - settings: Settings? = nil, - sources: [SourceFile] = [], - resources: [ResourceFileElement] = [], - copyFiles: [CopyFilesAction] = [], - headers: Headers? = nil, - coreDataModels: [CoreDataModel] = [], - scripts: [TargetScript] = [], - environmentVariables: [String: EnvironmentVariable] = [:], - launchArguments: [LaunchArgument] = [], - filesGroup: ProjectGroup, - dependencies: [TargetDependency] = [], - rawScriptBuildPhases: [RawScriptBuildPhase] = [], - playgrounds: [AbsolutePath] = [], - additionalFiles: [FileElement] = [], - buildRules: [BuildRule] = [], - prune: Bool = false, - mergedBinaryType: MergedBinaryType = .disabled, - mergeable: Bool = false - ) { - self.name = name - self.product = product - self.destinations = destinations - self.bundleId = bundleId - self.productName = productName ?? name.replacingOccurrences(of: "-", with: "_") - self.deploymentTargets = deploymentTargets - self.infoPlist = infoPlist - self.entitlements = entitlements - self.settings = settings - self.sources = sources - self.resources = resources - self.copyFiles = copyFiles - self.headers = headers - self.coreDataModels = coreDataModels - self.scripts = scripts - self.environmentVariables = environmentVariables - self.launchArguments = launchArguments - self.filesGroup = filesGroup - self.dependencies = dependencies - self.rawScriptBuildPhases = rawScriptBuildPhases - self.playgrounds = playgrounds - self.additionalFiles = additionalFiles - self.buildRules = buildRules - self.prune = prune - self.mergedBinaryType = mergedBinaryType - self.mergeable = mergeable - } - - /// Target can be included in the link phase of other targets - public func isLinkable() -> Bool { - [.dynamicLibrary, .staticLibrary, .framework, .staticFramework].contains(product) - } - - /// Returns whether a target is exclusive to a single platform - public func isExclusiveTo(_ platform: Platform) -> Bool { - destinations.map(\.platform).allSatisfy { $0 == platform } - } - - /// Returns whether a target supports a platform - public func supports(_ platform: Platform) -> Bool { - destinations.map(\.platform).contains(platform) - } - - /// List of platforms this target deploys to - public var supportedPlatforms: Set { - Set(destinations.map(\.platform)) - } - - /// Returns target's pre scripts. - public var preScripts: [TargetScript] { - scripts.filter { $0.order == .pre } - } - - /// Returns target's post scripts. - public var postScripts: [TargetScript] { - scripts.filter { $0.order == .post } - } - - /// Returns true if the target supports Mac Catalyst - public var supportsCatalyst: Bool { - destinations.contains(.macCatalyst) - } - - /// Target can link static products (e.g. an app can link a staticLibrary) - public func canLinkStaticProducts() -> Bool { - [ - .framework, - .app, - .commandLineTool, - .xpc, - .unitTests, - .uiTests, - .appExtension, - .watch2Extension, - .messagesExtension, - .appClip, - .tvTopShelfExtension, - .systemExtension, - .extensionKitExtension, - .macro, - ].contains(product) - } - - /// Returns true if the target supports having a headers build phase.. - public var shouldIncludeHeadersBuildPhase: Bool { - switch product { - case .framework, .staticFramework, .staticLibrary, .dynamicLibrary: - return true - default: - return false - } - } - - /// Returns true if the target supports having sources. - public var supportsSources: Bool { - switch product { - case .stickerPackExtension, .watch2App: - return false - case .bundle: - // Bundles only support source when targetting macOS only - return isExclusiveTo(.macOS) - default: - return true - } - } - - /// Returns true if the target deploys to more then one platform - public var isMultiplatform: Bool { - supportedPlatforms.count > 1 - } - - /// Returns true if the target supports hosting resources - public var supportsResources: Bool { - switch product { - case .app, - .framework, - .unitTests, - .uiTests, - .bundle, - .appExtension, - .watch2App, - .watch2Extension, - .tvTopShelfExtension, - .messagesExtension, - .stickerPackExtension, - .appClip, - .systemExtension, - .extensionKitExtension: - return true - - case .commandLineTool, - .macro, - .dynamicLibrary, - .staticLibrary, - .staticFramework, - .xpc: - return false - } - } - - public var legacyPlatform: Platform { - destinations.first?.platform ?? .iOS - } - - /// Returns true if the target is an AppClip - public var isAppClip: Bool { - if case .appClip = product { - return true - } - return false - } - - /// Determines if the target is an embeddable watch application - /// i.e. a product that can be bundled with a host iOS application - public func isEmbeddableWatchApplication() -> Bool { - let isWatchOS = isExclusiveTo(.watchOS) - let isApp = (product == .watch2App || product == .app) - return isWatchOS && isApp - } - - /// Determines if the target is an embeddable xpc service - /// i.e. a product that can be bundled with a host macOS application - public func isEmbeddableXPCService() -> Bool { - product == .xpc - } - - /// Determines if the target is an embeddable system extension - /// i.e. a product that can be bundled with a host macOS application - public func isEmbeddableSystemExtension() -> Bool { - product == .systemExtension - } - - /// 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 { - 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 { - supports(.macOS) && product == .app - } - - /// Return the a set of PlatformFilters to control linking based on what platform is being compiled - /// This allows a target to link against a dependency conditionally when it is being compiled for a compatible platform - /// E.g. An app linking against CarPlay only when built for iOS. - public var dependencyPlatformFilters: PlatformFilters { - Set(destinations.map(\.platformFilter)) - } - - // MARK: - Equatable - - public static func == (lhs: Target, rhs: Target) -> Bool { - lhs.name == rhs.name && - lhs.destinations == rhs.destinations && - lhs.product == rhs.product && - lhs.bundleId == rhs.bundleId && - lhs.productName == rhs.productName && - lhs.infoPlist == rhs.infoPlist && - lhs.entitlements == rhs.entitlements && - lhs.settings == rhs.settings && - lhs.sources == rhs.sources && - lhs.resources == rhs.resources && - lhs.headers == rhs.headers && - lhs.coreDataModels == rhs.coreDataModels && - lhs.scripts == rhs.scripts && - lhs.dependencies == rhs.dependencies && - lhs.mergedBinaryType == rhs.mergedBinaryType && - lhs.mergeable == rhs.mergeable && - lhs.environmentVariables == rhs.environmentVariables - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(name) - hasher.combine(destinations) - hasher.combine(product) - hasher.combine(bundleId) - hasher.combine(productName) - hasher.combine(environmentVariables) - } - - /// Returns a new copy of the target with the given InfoPlist set. - /// - Parameter infoPlist: InfoPlist to be set to the copied instance. - public func with(infoPlist: InfoPlist) -> Target { - var copy = self - copy.infoPlist = infoPlist - return copy - } - - /// Returns a new copy of the target with the given entitlements set. - /// - Parameter entitlements: entitlements to be set to the copied instance. - public func with(entitlements: Entitlements) -> Target { - var copy = self - copy.entitlements = entitlements - return copy - } - - /// Returns a new copy of the target with the given scripts. - /// - Parameter scripts: Actions to be set to the copied instance. - public func with(scripts: [TargetScript]) -> Target { - var copy = self - copy.scripts = scripts - return copy - } - - /// Returns a new copy of the target with the given additional settings - /// - Parameter additionalSettings: settings to be added. - public func with(additionalSettings: SettingsDictionary) -> Target { - var copy = self - if let oldSettings = copy.settings { - copy.settings = Settings( - base: oldSettings.base.merging(additionalSettings, uniquingKeysWith: { $1 }), - configurations: oldSettings.configurations, - defaultSettings: oldSettings.defaultSettings - ) - } else { - copy.settings = Settings( - base: additionalSettings, - configurations: [:] - ) - } - return copy - } - - // MARK: - Comparable - - public static func < (lhs: Target, rhs: Target) -> Bool { - lhs.name < rhs.name - } -} - -extension Sequence { - /// Filters and returns only the targets that are test bundles. - var testBundles: [Target] { - filter(\.product.testsBundle) - } - - /// Filters and returns only the targets that are apps and app clips. - var apps: [Target] { - filter { $0.product == .app || $0.product == .appClip } - } -} diff --git a/Sources/TuistGraph/Models/TargetDependency.swift b/Sources/TuistGraph/Models/TargetDependency.swift deleted file mode 100644 index bfee06a3669..00000000000 --- a/Sources/TuistGraph/Models/TargetDependency.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation -import TSCBasic - -public enum FrameworkStatus: String, Hashable, Codable { - case required - case optional -} - -public enum SDKStatus: String, Codable { - case required - case optional -} - -public enum TargetDependency: Equatable, Hashable, Codable { - public enum PackageType: String, Equatable, Hashable, Codable { - case runtime - case plugin - case macro - } - - case target(name: String, condition: PlatformCondition? = nil) - case project(target: String, path: AbsolutePath, condition: PlatformCondition? = nil) - case framework(path: AbsolutePath, status: FrameworkStatus, condition: PlatformCondition? = nil) - case xcframework(path: AbsolutePath, status: FrameworkStatus, condition: PlatformCondition? = nil) - case library( - path: AbsolutePath, - publicHeaders: AbsolutePath, - swiftModuleMap: AbsolutePath?, - condition: PlatformCondition? = nil - ) - case package(product: String, type: PackageType, condition: PlatformCondition? = nil) - case sdk(name: String, status: SDKStatus, condition: PlatformCondition? = nil) - case xctest - - public var condition: PlatformCondition? { - switch self { - case .target(name: _, condition: let condition): - condition - case .project(target: _, path: _, condition: let condition): - condition - case .framework(path: _, status: _, condition: let condition): - condition - case .xcframework(path: _, status: _, condition: let condition): - condition - case .library(path: _, publicHeaders: _, swiftModuleMap: _, condition: let condition): - condition - case .package(product: _, type: _, condition: let condition): - condition - case .sdk(name: _, status: _, condition: let condition): - condition - case .xctest: nil - } - } - - public func withCondition(_ condition: PlatformCondition?) -> TargetDependency { - switch self { - case .target(name: let name, condition: _): - return .target(name: name, condition: condition) - case .project(target: let target, path: let path, condition: _): - return .project(target: target, path: path, condition: condition) - case .framework(path: let path, status: let status, condition: _): - return .framework(path: path, status: status, condition: condition) - case .xcframework(path: let path, status: let status, condition: _): - return .xcframework(path: path, status: status, condition: condition) - case .library(path: let path, publicHeaders: let headers, swiftModuleMap: let moduleMap, condition: _): - return .library(path: path, publicHeaders: headers, swiftModuleMap: moduleMap, condition: condition) - case .package(product: let product, type: let type, condition: _): - return .package(product: product, type: type, condition: condition) - case .sdk(name: let name, status: let status, condition: _): - return .sdk(name: name, status: status, condition: condition) - case .xctest: return .xctest - } - } -} diff --git a/Sources/TuistGraph/Models/TargetReference.swift b/Sources/TuistGraph/Models/TargetReference.swift deleted file mode 100644 index d6c13eb84e6..00000000000 --- a/Sources/TuistGraph/Models/TargetReference.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import TSCBasic - -public struct TargetReference: Hashable, Codable { - public var projectPath: AbsolutePath - public var name: String - - public init(projectPath: AbsolutePath, name: String) { - self.projectPath = projectPath - self.name = name - } -} diff --git a/Sources/TuistGraph/Models/TargetScript.swift b/Sources/TuistGraph/Models/TargetScript.swift deleted file mode 100644 index a32b82fb043..00000000000 --- a/Sources/TuistGraph/Models/TargetScript.swift +++ /dev/null @@ -1,152 +0,0 @@ -import Foundation -import TSCBasic - -/// It represents a target script build phase -public struct TargetScript: Equatable, Codable { - /// Order when the script gets executed. - /// - /// - pre: Before the sources and resources build phase. - /// - post: After the sources and resources build phase. - public enum Order: String, Equatable, Codable { - case pre - case post - } - - /// Specifies how to execute the target script - /// - /// - tool: Executes the tool with the given arguments. Tuist will look up the tool on the environment's PATH. - /// - scriptPath: Executes the file at the path with the given arguments. - /// - embedded: Executes the embedded script. This should be a short command. - public enum Script: Equatable, Codable { - case tool(path: String, args: [String] = []) - case scriptPath(path: AbsolutePath, args: [String] = []) - case embedded(String) - } - - /// Name of the build phase when the project gets generated - public let name: String - - /// The script to execute in the script - public let script: Script - - /// The text of the embedded script - public var embeddedScript: String? { - if case let Script.embedded(embeddedScript) = script { - return embeddedScript - } - - return nil - } - - /// Name of the tool to execute. Tuist will look up the tool on the environment's PATH. - public var tool: String? { - if case let Script.tool(tool, _) = script { - return tool - } - - return nil - } - - /// Path to the script to execute. - public var path: AbsolutePath? { - if case let Script.scriptPath(path, _) = script { - return path - } - - return nil - } - - /// Target script order - public let order: Order - - /// Arguments that to be passed - public var arguments: [String] { - switch script { - case let .scriptPath(_, args), let .tool(_, args): - return args - - case .embedded: - return [] - } - } - - /// List of input file paths - public let inputPaths: [String] - - /// List of input filelist paths - public let inputFileListPaths: [AbsolutePath] - - /// List of output file paths - public let outputPaths: [String] - - /// List of output filelist paths - public let outputFileListPaths: [AbsolutePath] - - /// Show environment variables in the logs - public var showEnvVarsInLog: Bool - - /// Whether to skip running this script in incremental builds, if nothing has changed - public let basedOnDependencyAnalysis: Bool? - - /// Whether this script only runs on install builds (default is false) - public let runForInstallBuildsOnly: Bool - - /// The path to the shell which shall execute this script. - public let shellPath: String - - /// The path to the dependency file - public let dependencyFile: AbsolutePath? - - /// Initializes a new target script with its attributes using a script at the given path to be executed. - /// - /// - Parameters: - /// - name: Name of the build phase when the project gets generated - /// - order: Target script order - /// - script: The script to execute in the script - /// - inputPaths: List of input file paths - /// - inputFileListPaths: List of input filelist paths - /// - outputPaths: List of output file paths - /// - outputFileListPaths: List of output filelist paths - /// - showEnvVarsInLog: Show environment variables in the logs - /// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds - /// - runForInstallBuildsOnly: Whether this script only runs on install builds (default is false) - /// - shellPath: The path to the shell which shall execute this script. Default is `/bin/sh`. - /// - dependencyFile: The path to the dependency file. Default is `nil`. - public init( - name: String, - order: Order, - script: Script = .embedded(""), - inputPaths: [String] = [], - inputFileListPaths: [AbsolutePath] = [], - outputPaths: [String] = [], - outputFileListPaths: [AbsolutePath] = [], - showEnvVarsInLog: Bool = true, - basedOnDependencyAnalysis: Bool? = nil, - runForInstallBuildsOnly: Bool = false, - shellPath: String = "/bin/sh", - dependencyFile: AbsolutePath? = nil - ) { - self.name = name - self.order = order - self.script = script - self.inputPaths = inputPaths - self.inputFileListPaths = inputFileListPaths - self.outputPaths = outputPaths - self.outputFileListPaths = outputFileListPaths - self.showEnvVarsInLog = showEnvVarsInLog - self.basedOnDependencyAnalysis = basedOnDependencyAnalysis - self.runForInstallBuildsOnly = runForInstallBuildsOnly - self.shellPath = shellPath - self.dependencyFile = dependencyFile - } -} - -extension [TargetScript] { - public var preScripts: [TargetScript] { - filter { $0.order == .pre } - } - - public var postScripts: [TargetScript] { - filter { $0.order == .post } - } -} diff --git a/Sources/TuistGraph/Models/Template.swift b/Sources/TuistGraph/Models/Template.swift deleted file mode 100644 index ba223cc5d6a..00000000000 --- a/Sources/TuistGraph/Models/Template.swift +++ /dev/null @@ -1,59 +0,0 @@ -import TSCBasic - -public struct Template: Equatable { - public let description: String - public let attributes: [Attribute] - public let items: [Item] - - public init( - description: String, - attributes: [Attribute] = [], - items: [Item] = [] - ) { - self.description = description - self.attributes = attributes - self.items = items - } - - public enum Attribute: Equatable { - case required(String) - case optional(String, default: String) - - public var isOptional: Bool { - switch self { - case .required: - return false - case .optional: - return true - } - } - - public var name: String { - switch self { - case let .required(name): - return name - case let .optional(name, default: _): - return name - } - } - } - - public enum Contents: Equatable { - case string(String) - case file(AbsolutePath) - case directory(AbsolutePath) - } - - public struct Item: Equatable { - public let path: RelativePath - public let contents: Contents - - public init( - path: RelativePath, - contents: Contents - ) { - self.path = path - self.contents = contents - } - } -} diff --git a/Sources/TuistGraph/Models/TestAction.swift b/Sources/TuistGraph/Models/TestAction.swift deleted file mode 100644 index 062e3e915af..00000000000 --- a/Sources/TuistGraph/Models/TestAction.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import TSCBasic - -public struct TestAction: Equatable, Codable { - // MARK: - Attributes - - public var testPlans: [TestPlan]? - public var targets: [TestableTarget] - public var arguments: Arguments? - public var configurationName: String - public var attachDebugger: Bool - public var coverage: Bool - public var codeCoverageTargets: [TargetReference] - public var expandVariableFromTarget: TargetReference? - public var preActions: [ExecutionAction] - public var postActions: [ExecutionAction] - public var diagnosticsOptions: Set - public var language: String? - public var region: String? - public var preferredScreenCaptureFormat: ScreenCaptureFormat? - public var skippedTests: [String]? - - // MARK: - Init - - public init( - targets: [TestableTarget], - arguments: Arguments?, - configurationName: String, - attachDebugger: Bool, - coverage: Bool, - codeCoverageTargets: [TargetReference], - expandVariableFromTarget: TargetReference?, - preActions: [ExecutionAction], - postActions: [ExecutionAction], - diagnosticsOptions: Set, - language: String? = nil, - region: String? = nil, - preferredScreenCaptureFormat: ScreenCaptureFormat? = nil, - testPlans: [TestPlan]? = nil, - skippedTests: [String]? = nil - ) { - self.testPlans = testPlans - self.targets = targets - self.arguments = arguments - self.configurationName = configurationName - self.attachDebugger = attachDebugger - self.coverage = coverage - self.preActions = preActions - self.postActions = postActions - self.codeCoverageTargets = codeCoverageTargets - self.expandVariableFromTarget = expandVariableFromTarget - self.diagnosticsOptions = diagnosticsOptions - self.language = language - self.region = region - self.preferredScreenCaptureFormat = preferredScreenCaptureFormat - self.skippedTests = skippedTests - } -} diff --git a/Sources/TuistGraph/Models/TestPlan.swift b/Sources/TuistGraph/Models/TestPlan.swift deleted file mode 100644 index aa5b7ee7a62..00000000000 --- a/Sources/TuistGraph/Models/TestPlan.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import TSCBasic - -public struct TestPlan: Hashable, Codable { - public let name: String - public let path: AbsolutePath - public let testTargets: [TestableTarget] - public let isDefault: Bool - - public init(path: AbsolutePath, testTargets: [TestableTarget], isDefault: Bool) { - name = path.basenameWithoutExt - self.path = path - self.testTargets = testTargets - self.isDefault = isDefault - } -} diff --git a/Sources/TuistGraph/Models/TestableTarget.swift b/Sources/TuistGraph/Models/TestableTarget.swift deleted file mode 100644 index 67ffc6021e9..00000000000 --- a/Sources/TuistGraph/Models/TestableTarget.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import TSCBasic - -/// Testable target describe target and tests information. -public struct TestableTarget: Equatable, Hashable, Codable { - /// The target name and its project path. - public let target: TargetReference - /// Skip test target from TestAction. - public let isSkipped: Bool - /// Execute tests in parallel. - public let isParallelizable: Bool - /// Execute tests in random order. - public let isRandomExecutionOrdering: Bool - - public init( - target: TargetReference, - skipped: Bool = false, - parallelizable: Bool = false, - randomExecutionOrdering: Bool = false - ) { - self.target = target - isSkipped = skipped - isParallelizable = parallelizable - isRandomExecutionOrdering = randomExecutionOrdering - } -} diff --git a/Sources/TuistGraph/Models/TestingOptions.swift b/Sources/TuistGraph/Models/TestingOptions.swift deleted file mode 100644 index 675db447ed8..00000000000 --- a/Sources/TuistGraph/Models/TestingOptions.swift +++ /dev/null @@ -1,10 +0,0 @@ -public struct TestingOptions: OptionSet, Codable, Hashable { - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let parallelizable = TestingOptions(rawValue: 1 << 0) - public static let randomExecutionOrdering = TestingOptions(rawValue: 1 << 1) -} diff --git a/Sources/TuistGraph/Models/Workspace.swift b/Sources/TuistGraph/Models/Workspace.swift deleted file mode 100644 index 13a3252d6b9..00000000000 --- a/Sources/TuistGraph/Models/Workspace.swift +++ /dev/null @@ -1,150 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility - -public struct Workspace: Equatable, Codable { - // MARK: - Attributes - - /// Path to where the manifest / root directory of this workspace is located - public var path: AbsolutePath - /// Path to where the `.xcworkspace` will be generated - public var xcWorkspacePath: AbsolutePath - public var name: String - public var projects: [AbsolutePath] - public var schemes: [Scheme] - public var ideTemplateMacros: IDETemplateMacros? - public var additionalFiles: [FileElement] - public var generationOptions: GenerationOptions - - // MARK: - Init - - public init( - path: AbsolutePath, - xcWorkspacePath: AbsolutePath, - name: String, - projects: [AbsolutePath], - schemes: [Scheme] = [], - generationOptions: GenerationOptions = .init( - enableAutomaticXcodeSchemes: false, - autogeneratedWorkspaceSchemes: .enabled( - codeCoverageMode: .disabled, - testingOptions: [], - testLanguage: nil, - testRegion: nil, - testScreenCaptureFormat: nil - ), - lastXcodeUpgradeCheck: nil, - renderMarkdownReadme: false - ), - ideTemplateMacros: IDETemplateMacros? = nil, - additionalFiles: [FileElement] = [] - ) { - self.path = path - self.xcWorkspacePath = xcWorkspacePath - self.name = name - self.projects = projects - self.schemes = schemes - self.generationOptions = generationOptions - self.ideTemplateMacros = ideTemplateMacros - self.additionalFiles = additionalFiles - } -} - -extension Workspace { - public func with(name: String) -> Workspace { - var copy = self - copy.name = name - return copy - } - - public func adding(files: [AbsolutePath]) -> Workspace { - Workspace( - path: path, - xcWorkspacePath: xcWorkspacePath, - name: name, - projects: projects, - schemes: schemes, - generationOptions: generationOptions, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles + files.map { .file(path: $0) } - ) - } - - public func replacing(projects: [AbsolutePath]) -> Workspace { - Workspace( - path: path, - xcWorkspacePath: xcWorkspacePath, - name: name, - projects: projects, - schemes: schemes, - generationOptions: generationOptions, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles - ) - } - - public func merging(projects otherProjects: [AbsolutePath]) -> Workspace { - Workspace( - path: path, - xcWorkspacePath: xcWorkspacePath, - name: name, - projects: Array(Set(projects + otherProjects)), - schemes: schemes, - generationOptions: generationOptions, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles - ) - } - - public func codeCoverageTargets(projects: [Project]) -> [TargetReference] { - switch codeCoverageMode { - case .all, .disabled: return [] - case let .targets(targets): return targets - case .relevant: - let allSchemes = schemes + projects.flatMap(\.schemes) - var resultTargets = Set() - - allSchemes.forEach { scheme in - // try to add code coverage targets only if code coverage is enabled - guard let testAction = scheme.testAction, testAction.coverage else { return } - - let schemeCoverageTargets = testAction.codeCoverageTargets - - // having empty `codeCoverageTargets` means that we should gather code coverage for all build targets - if schemeCoverageTargets.isEmpty, let buildAction = scheme.buildAction { - resultTargets.formUnion(buildAction.targets) - } else { - resultTargets.formUnion(schemeCoverageTargets) - } - } - - // if we find no schemes that gather code coverage data, there are no relevant targets, - // so we disable code coverage - if resultTargets.isEmpty { - return [] - } - - return Array(resultTargets) - } - } -} - -extension Workspace { - public var codeCoverageMode: GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode { - switch generationOptions.autogeneratedWorkspaceSchemes { - case let .enabled(codeCoverageMode, _, _, _, _): - return codeCoverageMode - case .disabled: - return .disabled - } - } - - public var testingOptions: TestingOptions { - switch generationOptions.autogeneratedWorkspaceSchemes { - case let .enabled(_, testingOptions, _, _, _): - return testingOptions - case .disabled: - return [] - } - } -} diff --git a/Sources/TuistGraph/Models/WorkspaceGenerationOptions.swift b/Sources/TuistGraph/Models/WorkspaceGenerationOptions.swift deleted file mode 100644 index bd86d8cadf2..00000000000 --- a/Sources/TuistGraph/Models/WorkspaceGenerationOptions.swift +++ /dev/null @@ -1,104 +0,0 @@ -import TSCUtility - -extension Workspace { - /// Contains options related to the workspace generation. - public struct GenerationOptions: Codable, Equatable { - /// Contains options for autogenerated workspace schemes - public enum AutogeneratedWorkspaceSchemes: Codable, Equatable { - /// Contains options for code coverage - public enum CodeCoverageMode: Codable, Equatable { - /// Gather code coverage for all targets - case all - /// Gather code coverage only for targets, that have code coverage enabled at least in one scheme - case relevant - /// Gather code coverage only for given targets - case targets([TargetReference]) - /// Do not gather code coverage - case disabled - } - - /// Tuist will not automatically generate the workspace schemes - case disabled - /// Tuist will generate the workspace schemes - case enabled( - codeCoverageMode: CodeCoverageMode = .disabled, - testingOptions: TestingOptions = [], - testLanguage: String? = nil, - testRegion: String? = nil, - testScreenCaptureFormat: ScreenCaptureFormat? = nil - ) - - public var codeCoverageMode: CodeCoverageMode { - switch self { - case let .enabled(codeCoverageMode, _, _, _, _): - return codeCoverageMode - case .disabled: - return .disabled - } - } - - public var testingOptions: TestingOptions { - switch self { - case let .enabled(_, testingOptions, _, _, _): - return testingOptions - case .disabled: - return [] - } - } - - public var testLanguage: String? { - switch self { - case let .enabled(_, _, language, _, _): - return language - case .disabled: - return nil - } - } - - public var testRegion: String? { - switch self { - case let .enabled(_, _, _, region, _): - return region - case .disabled: - return nil - } - } - - public var testScreenCaptureFormat: ScreenCaptureFormat? { - switch self { - case let .enabled(_, _, _, _, testScreenCaptureFormat): - return testScreenCaptureFormat - case .disabled: - return nil - } - } - } - - /// Tuist generates a WorkspaceSettings.xcsettings file, setting the related key to the associated value. - public let enableAutomaticXcodeSchemes: Bool? - - /// Options for autogenerated workspace schemes - public let 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? - - /// Allows to render markdown files inside the workspace including an .xcodesamples.plist inside it. - public let renderMarkdownReadme: Bool - - public init( - enableAutomaticXcodeSchemes: Bool?, - autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes, - lastXcodeUpgradeCheck: Version?, - renderMarkdownReadme: Bool - ) { - self.enableAutomaticXcodeSchemes = enableAutomaticXcodeSchemes - self.autogeneratedWorkspaceSchemes = autogeneratedWorkspaceSchemes - self.lastXcodeUpgradeCheck = lastXcodeUpgradeCheck - self.renderMarkdownReadme = renderMarkdownReadme - } - } -} diff --git a/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift b/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift deleted file mode 100644 index e7b7986f208..00000000000 --- a/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation -import TSCBasic - -/// It represents th Info.plist contained in an .xcframework bundle. -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, Hashable, Equatable { - private enum CodingKeys: String, CodingKey { - case identifier = "LibraryIdentifier" - case path = "LibraryPath" - case architectures = "SupportedArchitectures" - case mergeable = "MergeableMetadata" - } - - /// It represents the library's platform. - public enum Platform: String, Hashable, Codable { - case ios - } - - /// Binary name used to import the library - public var binaryName: String { - path.basenameWithoutExt - } - - /// Library identifier. - public let identifier: String - - /// Path to the library. - public let path: RelativePath - - /// Declares if the library is mergeable or not - public let mergeable: Bool - - /// Architectures the binary is built for. - public let architectures: [BinaryArchitecture] - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(identifier, forKey: .identifier) - try container.encode(path, forKey: .path) - try container.encode(mergeable, forKey: .mergeable) - try container.encode(architectures, forKey: .architectures) - } - - public init( - identifier: String, - path: RelativePath, - mergeable: Bool, - architectures: [BinaryArchitecture] - ) { - self.identifier = identifier - self.path = path - self.mergeable = mergeable - self.architectures = architectures - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - identifier = try container.decode(String.self, forKey: .identifier) - path = try container.decode(RelativePath.self, forKey: .path) - architectures = try container.decode([BinaryArchitecture].self, forKey: .architectures) - mergeable = try container.decodeIfPresent(Bool.self, forKey: .mergeable) ?? false - } - } - - /// List of libraries that are part of the .xcframework. - public let libraries: [Library] -} diff --git a/Sources/TuistGraphTesting/DependenciesGraph/DependenciesGraph+TestData.swift b/Sources/TuistGraphTesting/DependenciesGraph/DependenciesGraph+TestData.swift deleted file mode 100644 index 48cbe8e7922..00000000000 --- a/Sources/TuistGraphTesting/DependenciesGraph/DependenciesGraph+TestData.swift +++ /dev/null @@ -1,232 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph - -extension DependenciesGraph { - /// A snapshot of `graph.json` file. - public static var testJson: String { - """ - { - "externalDependencies": { - "RxSwift": [ - { - "xcframework": { - "path": "/Tuist/Dependencies/Carthage/RxSwift.xcframework", - "platformFilters": [ - { - "catalyst" : { } - }, - { - "watchos" : { } - }, - { - "ios" : { } - }, - { - "driverkit" : { } - }, - { - "tvos" : { } - }, - { - "macos" : { } - }, - { - "visionos" : { } - } - ], - "status": "required" - } - } - ] - }, - "externalProjects": [] - } - """ - } - - /// A snapshot of `Dependencies.swift` file. - public static var testDependenciesFile: String { - """ - import ProjectDescription - - let dependencies = Dependencies( - carthage: [ - .github(path: "RxSwift/RxSwift", requirement: .exact("5.0.4")), - ], - platforms: [.iOS] - ) - """ - } - - public static func test( - externalDependencies: [String: [TargetDependency]] = [:], - externalProjects: [AbsolutePath: Project] = [:] - ) -> Self { - .init(externalDependencies: externalDependencies, externalProjects: externalProjects) - } - - public static func testXCFramework( - name: String = "Test", - // swiftlint:disable:next force_try - path: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "Test.xcframework")), - status: FrameworkStatus = .required - ) -> DependenciesGraph { - let externalDependencies = [name: [TargetDependency.xcframework(path: path, status: status)]] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func test( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "Tuist": [ - TargetDependency.project( - target: "Tuist", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func aDependency( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "ALibrary": [ - TargetDependency.project( - target: "ALibrary", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func anotherDependency( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "AnotherLibrary": [ - TargetDependency.project( - target: "AnotherLibrary", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func alamofire( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "Alamofire": [ - TargetDependency.project( - target: "Alamofire", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func googleAppMeasurement( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "GoogleAppMeasurement": [ - TargetDependency.project( - target: "GoogleAppMeasurementTarget", - path: packageFolder - ), - ], - "GoogleAppMeasurementWithoutAdIdSupport": [ - TargetDependency.project( - target: "GoogleAppMeasurementWithoutAdIdSupportTarget", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func googleUtilities( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "GULAppDelegateSwizzler": [ - TargetDependency.project( - target: "GULAppDelegateSwizzler", - path: packageFolder - ), - ], - "GULMethodSwizzler": [ - TargetDependency.project( - target: "GULMethodSwizzler", - path: packageFolder - ), - ], - "GULNSData": [ - TargetDependency.project( - target: "GULNSData", - path: packageFolder - ), - ], - "GULNetwork": [ - TargetDependency.project( - target: "GULNetwork", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } - - public static func nanopb( - packageFolder: AbsolutePath - ) -> Self { - let externalDependencies = [ - "nanopb": [ - TargetDependency.project( - target: "nanopb", - path: packageFolder - ), - ], - ] - - return .init( - externalDependencies: externalDependencies, - externalProjects: [:] - ) - } -} diff --git a/Sources/TuistGraphTesting/Graph/Graph+TestData.swift b/Sources/TuistGraphTesting/Graph/Graph+TestData.swift deleted file mode 100644 index 4733127da21..00000000000 --- a/Sources/TuistGraphTesting/Graph/Graph+TestData.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph - -extension Graph { - public static func test( - name: String = "graph", - path: AbsolutePath = .root, - workspace: Workspace = .test(), - projects: [AbsolutePath: Project] = [:], - packages: [AbsolutePath: [String: Package]] = [:], - targets: [AbsolutePath: [String: Target]] = [:], - dependencies: [GraphDependency: Set] = [:], - dependencyConditions: [GraphEdge: PlatformCondition] = [:] - ) -> Graph { - Graph( - name: name, - path: path, - workspace: workspace, - projects: projects, - packages: packages, - targets: targets, - dependencies: dependencies, - dependencyConditions: dependencyConditions - ) - } -} diff --git a/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift b/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift deleted file mode 100644 index 7d190bf3fea..00000000000 --- a/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift +++ /dev/null @@ -1,115 +0,0 @@ -import Foundation -import TSCBasic - -@testable import TuistGraph - -// swiftlint:disable force_try - -extension GraphDependency { - public static func testFramework( - path: AbsolutePath = AbsolutePath.root.appending(component: "Test.framework"), - binaryPath: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "Test.framework/Test")), - dsymPath: AbsolutePath? = nil, - bcsymbolmapPaths: [AbsolutePath] = [], - linking: BinaryLinking = .dynamic, - architectures: [BinaryArchitecture] = [.armv7], - isCarthage: Bool = false, - status: FrameworkStatus = .required - ) -> GraphDependency { - GraphDependency.framework( - path: path, - binaryPath: binaryPath, - dsymPath: dsymPath, - bcsymbolmapPaths: bcsymbolmapPaths, - linking: linking, - architectures: architectures, - isCarthage: isCarthage, - status: status - ) - } - - 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, - macroPath: AbsolutePath? = nil - ) -> GraphDependency { - .xcframework( - GraphDependency.XCFramework( - path: path, - infoPlist: infoPlist, - primaryBinaryPath: primaryBinaryPath, - linking: linking, - mergeable: false, - status: status, - macroPath: macroPath - ) - ) - } - - public static func testTarget( - name: String = "Test", - path: AbsolutePath = .root - ) -> GraphDependency { - .target( - name: name, - path: path - ) - } - - public static func testSDK( - name: String = "XCTest", - path: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "XCTest.framework")), - status: SDKStatus = .required, - source: SDKSource = .system - ) -> GraphDependency { - .sdk( - name: name, - path: path, - status: status, - source: source - ) - } - - public static func testLibrary( - path: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "libTuist.a")), - publicHeaders: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "headers")), - linking: BinaryLinking = .dynamic, - architectures: [BinaryArchitecture] = [.armv7], - swiftModuleMap: AbsolutePath? = nil - ) -> GraphDependency { - .library( - path: path, - publicHeaders: publicHeaders, - linking: linking, - architectures: architectures, - swiftModuleMap: swiftModuleMap - ) - } - - 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" - ) -> GraphDependency { - .packageProduct( - path: path, - product: product, - type: .runtime - ) - } -} - -// swiftlint:enable force_try diff --git a/Sources/TuistGraphTesting/Graph/GraphTarget+TestData.swift b/Sources/TuistGraphTesting/Graph/GraphTarget+TestData.swift deleted file mode 100644 index 8d474c3c3c1..00000000000 --- a/Sources/TuistGraphTesting/Graph/GraphTarget+TestData.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import TSCBasic - -@testable import TuistGraph - -extension GraphTarget { - public static func test( - path: AbsolutePath = .root, - target: Target = .test(), - project: Project = .test() - ) -> GraphTarget { - GraphTarget( - path: path, - target: target, - project: project - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/AnalyzeAction+TestData.swift b/Sources/TuistGraphTesting/Models/AnalyzeAction+TestData.swift deleted file mode 100644 index 6372cfafca9..00000000000 --- a/Sources/TuistGraphTesting/Models/AnalyzeAction+TestData.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension AnalyzeAction { - public static func test(configurationName: String = "Beta Release") -> AnalyzeAction { - AnalyzeAction(configurationName: configurationName) - } -} diff --git a/Sources/TuistGraphTesting/Models/ArchiveAction+TestData.swift b/Sources/TuistGraphTesting/Models/ArchiveAction+TestData.swift deleted file mode 100644 index c6b650d0958..00000000000 --- a/Sources/TuistGraphTesting/Models/ArchiveAction+TestData.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension ArchiveAction { - public static func test( - configurationName: String = "Beta Release", - revealArchiveInOrganizer: Bool = true, - customArchiveName: String? = nil, - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [] - ) -> ArchiveAction { - ArchiveAction( - configurationName: configurationName, - revealArchiveInOrganizer: revealArchiveInOrganizer, - customArchiveName: customArchiveName, - preActions: preActions, - postActions: postActions - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Arguments+TestData.swift b/Sources/TuistGraphTesting/Models/Arguments+TestData.swift deleted file mode 100644 index 135bd787322..00000000000 --- a/Sources/TuistGraphTesting/Models/Arguments+TestData.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Arguments { - public static func test( - environmentVariables: [String: EnvironmentVariable] = [:], - launchArguments: [LaunchArgument] = [] - ) -> Arguments { - Arguments( - environmentVariables: environmentVariables, - launchArguments: launchArguments - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/BuildAction+TestData.swift b/Sources/TuistGraphTesting/Models/BuildAction+TestData.swift deleted file mode 100644 index e82639c6644..00000000000 --- a/Sources/TuistGraphTesting/Models/BuildAction+TestData.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension BuildAction { - public static func test( - targets: [TargetReference] = [TargetReference(projectPath: "/Project", name: "App")], - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [] - ) -> BuildAction { - BuildAction(targets: targets, preActions: preActions, postActions: postActions) - } -} diff --git a/Sources/TuistGraphTesting/Models/Cache+TestData.swift b/Sources/TuistGraphTesting/Models/Cache+TestData.swift deleted file mode 100644 index 35136bc9add..00000000000 --- a/Sources/TuistGraphTesting/Models/Cache+TestData.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version -@testable import TuistGraph - -extension Cache { - public static func test(profiles: [Cache.Profile] = [Cache.Profile.test()]) -> Cache { - Cache(profiles: profiles, path: nil) - } -} - -extension Cache.Profile { - public static func test( - name: String = "Development", - configuration: String = "Debug", - device: String? = nil, - os: Version? = nil - ) -> Cache.Profile { - Cache.Profile(name: name, configuration: configuration, device: device, os: os) - } -} diff --git a/Sources/TuistGraphTesting/Models/Cloud+TestData.swift b/Sources/TuistGraphTesting/Models/Cloud+TestData.swift deleted file mode 100644 index fb8a600e543..00000000000 --- a/Sources/TuistGraphTesting/Models/Cloud+TestData.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupportTesting -@testable import TuistGraph - -extension Cloud { - public static func test( - url: URL = URL.test(), - projectId: String = "123", - options: [Cloud.Option] = [] - ) -> Cloud { - Cloud(url: url, projectId: projectId, options: options) - } -} diff --git a/Sources/TuistGraphTesting/Models/Condition+TestData.swift b/Sources/TuistGraphTesting/Models/Condition+TestData.swift deleted file mode 100644 index 9957d0fa0d1..00000000000 --- a/Sources/TuistGraphTesting/Models/Condition+TestData.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import TuistGraph -import XCTest - -extension PlatformCondition { - static func test(_ platformFilters: PlatformFilters) throws -> PlatformCondition { - try XCTUnwrap(.when(platformFilters)) - } -} diff --git a/Sources/TuistGraphTesting/Models/Config+TestData.swift b/Sources/TuistGraphTesting/Models/Config+TestData.swift deleted file mode 100644 index ff2174de0ec..00000000000 --- a/Sources/TuistGraphTesting/Models/Config+TestData.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility -@testable import TuistGraph - -extension Config { - public static func test( - compatibleXcodeVersions: CompatibleXcodeVersions = .all, - cloud: Cloud? = Cloud.test(), - cache: Cache? = Cache.test(), - swiftVersion: Version? = nil, - plugins: [PluginLocation] = [], - generationOptions: GenerationOptions = Config.default.generationOptions, - path: AbsolutePath? = nil - ) -> Config { - .init( - compatibleXcodeVersions: compatibleXcodeVersions, - cloud: cloud, - cache: cache, - swiftVersion: swiftVersion, - plugins: plugins, - generationOptions: generationOptions, - path: path - ) - } -} - -extension Config.GenerationOptions { - public static func test( - resolveDependenciesWithSystemScm: Bool = false, - disablePackageVersionLocking: Bool = false, - clonedSourcePackagesDirPath: AbsolutePath? = nil, - staticSideEffectsWarningTargets: TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets = .all, - enforceExplicitDependencies: Bool = false - ) -> Self { - .init( - resolveDependenciesWithSystemScm: resolveDependenciesWithSystemScm, - disablePackageVersionLocking: disablePackageVersionLocking, - clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, - staticSideEffectsWarningTargets: staticSideEffectsWarningTargets, - enforceExplicitDependencies: enforceExplicitDependencies - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Headers+TestData.swift b/Sources/TuistGraphTesting/Models/Headers+TestData.swift deleted file mode 100644 index aa6948c82af..00000000000 --- a/Sources/TuistGraphTesting/Models/Headers+TestData.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Headers { - public static func test( - public: [AbsolutePath] = [], - private: [AbsolutePath] = [], - project: [AbsolutePath] = [] - ) -> Headers { - Headers( - public: `public`, - private: `private`, - project: project - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/IDETemplateMacros+TestData.swift b/Sources/TuistGraphTesting/Models/IDETemplateMacros+TestData.swift deleted file mode 100644 index 959cea8b6da..00000000000 --- a/Sources/TuistGraphTesting/Models/IDETemplateMacros+TestData.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import TuistGraph -import TuistSupportTesting - -extension IDETemplateMacros { - public static func test(fileHeader: String? = "Header template") -> IDETemplateMacros { - IDETemplateMacros(fileHeader: fileHeader) - } -} diff --git a/Sources/TuistGraphTesting/Models/Metadata/FrameworkMetadata+TestData.swift b/Sources/TuistGraphTesting/Models/Metadata/FrameworkMetadata+TestData.swift deleted file mode 100644 index 267fe16a909..00000000000 --- a/Sources/TuistGraphTesting/Models/Metadata/FrameworkMetadata+TestData.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension FrameworkMetadata { - public static func test( - path: AbsolutePath = "/Frameworks/TestFramework.xframework", - binaryPath: AbsolutePath = "/Frameworks/TestFramework.xframework/TestFramework", - dsymPath: AbsolutePath? = nil, - bcsymbolmapPaths: [AbsolutePath] = [], - linking: BinaryLinking = .dynamic, - architectures: [BinaryArchitecture] = [.arm64], - isCarthage: Bool = false, - status: FrameworkStatus = .required - ) -> FrameworkMetadata { - FrameworkMetadata( - path: path, - binaryPath: binaryPath, - dsymPath: dsymPath, - bcsymbolmapPaths: bcsymbolmapPaths, - linking: linking, - architectures: architectures, - isCarthage: isCarthage, - status: status - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Metadata/LibraryMetadata+TestData.swift b/Sources/TuistGraphTesting/Models/Metadata/LibraryMetadata+TestData.swift deleted file mode 100644 index 83aa4287f70..00000000000 --- a/Sources/TuistGraphTesting/Models/Metadata/LibraryMetadata+TestData.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension LibraryMetadata { - public static func test( - path: AbsolutePath = "/Libraries/libTest/libTest.a", - publicHeaders: AbsolutePath = "/Libraries/libTest/include", - swiftModuleMap: AbsolutePath? = "/Libraries/libTest/libTest.swiftmodule", - architectures: [BinaryArchitecture] = [.arm64], - linking: BinaryLinking = .static - ) -> LibraryMetadata { - LibraryMetadata( - path: path, - publicHeaders: publicHeaders, - swiftModuleMap: swiftModuleMap, - architectures: architectures, - linking: linking - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift b/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift deleted file mode 100644 index 80ed8ab943e..00000000000 --- a/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension XCFrameworkMetadata { - public static func test( - path: AbsolutePath = "/XCFrameworks/XCFramework.xcframework", - infoPlist: XCFrameworkInfoPlist = .test(), - primaryBinaryPath: AbsolutePath = "/XCFrameworks/XCFramework.xcframework/ios-arm64/XCFramework", - linking: BinaryLinking = .dynamic, - mergeable: Bool = false, - status: FrameworkStatus = .required, - macroPath: AbsolutePath? = nil - ) -> XCFrameworkMetadata { - XCFrameworkMetadata( - path: path, - infoPlist: infoPlist, - primaryBinaryPath: primaryBinaryPath, - linking: linking, - mergeable: mergeable, - status: status, - macroPath: macroPath - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/PackageSettings+TestData.swift b/Sources/TuistGraphTesting/Models/PackageSettings+TestData.swift deleted file mode 100644 index d2bf60fb758..00000000000 --- a/Sources/TuistGraphTesting/Models/PackageSettings+TestData.swift +++ /dev/null @@ -1,21 +0,0 @@ -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/Plugins+TestData.swift b/Sources/TuistGraphTesting/Models/Plugins+TestData.swift deleted file mode 100644 index 47e269f945c..00000000000 --- a/Sources/TuistGraphTesting/Models/Plugins+TestData.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Plugins { - public static func test( - projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin] = [], - templatePaths: [AbsolutePath] = [], - resourceSynthesizers: [PluginResourceSynthesizer] = [] - ) -> Plugins { - Plugins( - projectDescriptionHelpers: projectDescriptionHelpers, - templatePaths: templatePaths, - resourceSynthesizers: resourceSynthesizers - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/ProfileAction+TestData.swift b/Sources/TuistGraphTesting/Models/ProfileAction+TestData.swift deleted file mode 100644 index 6c25b37cd0b..00000000000 --- a/Sources/TuistGraphTesting/Models/ProfileAction+TestData.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension ProfileAction { - public static func test( - configurationName: String = "Beta Release", - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - executable: TargetReference? = TargetReference(projectPath: "/Project", name: "App"), - arguments: Arguments? = Arguments.test() - ) -> ProfileAction { - ProfileAction( - configurationName: configurationName, - preActions: preActions, - postActions: postActions, - executable: executable, - arguments: arguments - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Project+TestData.swift b/Sources/TuistGraphTesting/Models/Project+TestData.swift deleted file mode 100644 index 4ce743b4ff4..00000000000 --- a/Sources/TuistGraphTesting/Models/Project+TestData.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility -@testable import TuistGraph - -extension Project { - public static func test( - path: AbsolutePath = try! AbsolutePath(validating: "/Project"), // swiftlint:disable:this force_try - sourceRootPath: AbsolutePath = try! AbsolutePath(validating: "/Project"), // swiftlint:disable:this force_try - // swiftlint:disable:next force_try - xcodeProjPath: AbsolutePath = try! AbsolutePath(validating: "/Project/Project.xcodeproj"), - name: String = "Project", - organizationName: String? = nil, - defaultKnownRegions: [String]? = nil, - developmentRegion: String? = nil, - options: Options = .test(automaticSchemesOptions: .disabled), - settings: Settings = Settings.test(), - filesGroup: ProjectGroup = .group(name: "Project"), - targets: [Target] = [Target.test()], - packages: [Package] = [], - schemes: [Scheme] = [], - ideTemplateMacros: IDETemplateMacros? = nil, - additionalFiles: [FileElement] = [], - resourceSynthesizers: [ResourceSynthesizer] = [], - lastUpgradeCheck: Version? = nil, - isExternal: Bool = false - ) -> Project { - Project( - path: path, - sourceRootPath: sourceRootPath, - xcodeProjPath: xcodeProjPath, - name: name, - organizationName: organizationName, - defaultKnownRegions: defaultKnownRegions, - developmentRegion: developmentRegion, - options: options, - settings: settings, - filesGroup: filesGroup, - targets: targets, - packages: packages, - schemes: schemes, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles, - resourceSynthesizers: resourceSynthesizers, - lastUpgradeCheck: lastUpgradeCheck, - isExternal: isExternal - ) - } - - public static func empty( - path: AbsolutePath = try! AbsolutePath(validating: "/test/"), // swiftlint:disable:this force_try - sourceRootPath: AbsolutePath = try! AbsolutePath(validating: "/test/"), // swiftlint:disable:this force_try - xcodeProjPath: AbsolutePath = try! AbsolutePath(validating: "/test/text.xcodeproj"), // swiftlint:disable:this force_try - name: String = "Project", - organizationName: String? = nil, - defaultKnownRegions: [String]? = nil, - developmentRegion: String? = nil, - options: Options = .test(automaticSchemesOptions: .disabled), - settings: Settings = .default, - filesGroup: ProjectGroup = .group(name: "Project"), - targets: [Target] = [], - packages: [Package] = [], - schemes: [Scheme] = [], - ideTemplateMacros: IDETemplateMacros? = nil, - additionalFiles: [FileElement] = [], - resourceSynthesizers: [ResourceSynthesizer] = [], - lastUpgradeCheck: Version? = nil, - isExternal: Bool = false - ) -> Project { - Project( - path: path, - sourceRootPath: sourceRootPath, - xcodeProjPath: xcodeProjPath, - name: name, - organizationName: organizationName, - defaultKnownRegions: defaultKnownRegions, - developmentRegion: developmentRegion, - options: options, - settings: settings, - filesGroup: filesGroup, - targets: targets, - packages: packages, - schemes: schemes, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles, - resourceSynthesizers: resourceSynthesizers, - lastUpgradeCheck: lastUpgradeCheck, - isExternal: isExternal - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/ProjectOptions+TestData.swift b/Sources/TuistGraphTesting/Models/ProjectOptions+TestData.swift deleted file mode 100644 index 8941a036975..00000000000 --- a/Sources/TuistGraphTesting/Models/ProjectOptions+TestData.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -@testable import TuistGraph - -extension Project.Options { - public static func test( - automaticSchemesOptions: AutomaticSchemesOptions = .enabled( - targetSchemesGrouping: .byNameSuffix( - build: ["Implementation", "Interface", "Mocks", "Testing"], - test: ["Tests", "IntegrationTests", "UITests", "SnapshotTests"], - run: ["App", "Demo"] - ), - codeCoverageEnabled: false, - testingOptions: [] - ), - disableBundleAccessors: Bool = false, - disableShowEnvironmentVarsInScriptPhases: Bool = false, - disableSynthesizedResourceAccessors: Bool = false, - textSettings: TextSettings = .init(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ) -> Self { - .init( - automaticSchemesOptions: automaticSchemesOptions, - disableBundleAccessors: disableBundleAccessors, - disableShowEnvironmentVarsInScriptPhases: disableShowEnvironmentVarsInScriptPhases, - disableSynthesizedResourceAccessors: disableSynthesizedResourceAccessors, - textSettings: textSettings - ) - } -} - -extension Project.Options.TextSettings { - public static func test( - usesTabs: Bool? = true, - indentWidth: UInt? = 2, - tabWidth: UInt? = 2, - wrapsLines: Bool? = true - ) -> Self { - .init( - usesTabs: usesTabs, - indentWidth: indentWidth, - tabWidth: tabWidth, - wrapsLines: wrapsLines - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/RawScriptBuildPhase+TestData.swift b/Sources/TuistGraphTesting/Models/RawScriptBuildPhase+TestData.swift deleted file mode 100644 index 440931af3d8..00000000000 --- a/Sources/TuistGraphTesting/Models/RawScriptBuildPhase+TestData.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension RawScriptBuildPhase { - public static func test( - name: String = "Test", - script: String = "", - showEnvVarsInLog: Bool = false, - hashable: Bool = false - ) -> RawScriptBuildPhase { - RawScriptBuildPhase(name: name, script: script, showEnvVarsInLog: showEnvVarsInLog, hashable: hashable) - } -} diff --git a/Sources/TuistGraphTesting/Models/ResourceSynthesizerPlugin+TestData.swift b/Sources/TuistGraphTesting/Models/ResourceSynthesizerPlugin+TestData.swift deleted file mode 100644 index bfabdbe3f03..00000000000 --- a/Sources/TuistGraphTesting/Models/ResourceSynthesizerPlugin+TestData.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension PluginResourceSynthesizer { - public static func test( - name: String = "Plugin", - path: AbsolutePath = try! AbsolutePath(validating: "/test") // swiftlint:disable:this force_try - ) -> Self { - .init( - name: name, - path: path - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/ResourceSynthesizers+TestData.swift b/Sources/TuistGraphTesting/Models/ResourceSynthesizers+TestData.swift deleted file mode 100644 index f054c895c14..00000000000 --- a/Sources/TuistGraphTesting/Models/ResourceSynthesizers+TestData.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -@testable import TuistGraph - -extension TuistGraph.ResourceSynthesizer { - public static func test( - parser: Parser = .assets, - parserOptions: [String: Parser.Option] = [:], - extensions: Set = ["xcassets"], - template: Template = .defaultTemplate("Assets") - ) -> Self { - ResourceSynthesizer(parser: parser, parserOptions: parserOptions, extensions: extensions, template: template) - } -} diff --git a/Sources/TuistGraphTesting/Models/RunAction+TestData.swift b/Sources/TuistGraphTesting/Models/RunAction+TestData.swift deleted file mode 100644 index 2873d14aef9..00000000000 --- a/Sources/TuistGraphTesting/Models/RunAction+TestData.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension RunAction { - public static func test( - configurationName: String = BuildConfiguration.debug.name, - attachDebugger: Bool = true, - customLLDBInitFile: AbsolutePath? = nil, - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - executable: TargetReference? = TargetReference(projectPath: "/Project", name: "App"), - filePath: AbsolutePath? = nil, - arguments: Arguments? = Arguments.test(), - options: RunActionOptions = .init(), - diagnosticsOptions: Set = [.mainThreadChecker, .performanceAntipatternChecker], - expandVariableFromTarget: TargetReference? = nil, - launchStyle: LaunchStyle = .automatically - ) -> RunAction { - RunAction( - configurationName: configurationName, - attachDebugger: attachDebugger, - customLLDBInitFile: customLLDBInitFile, - preActions: preActions, - postActions: postActions, - executable: executable, - filePath: filePath, - arguments: arguments, - options: options, - diagnosticsOptions: diagnosticsOptions, - expandVariableFromTarget: expandVariableFromTarget, - launchStyle: launchStyle - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Scheme+TestData.swift b/Sources/TuistGraphTesting/Models/Scheme+TestData.swift deleted file mode 100644 index 362caaa8d9a..00000000000 --- a/Sources/TuistGraphTesting/Models/Scheme+TestData.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Scheme { - public static func test( - name: String = "Test", - shared: Bool = false, - buildAction: BuildAction? = BuildAction.test(), - testAction: TestAction? = TestAction.test(), - runAction: RunAction? = RunAction.test(), - archiveAction: ArchiveAction? = ArchiveAction.test(), - profileAction: ProfileAction? = ProfileAction.test(), - analyzeAction: AnalyzeAction? = AnalyzeAction.test() - ) -> Scheme { - Scheme( - name: name, - shared: shared, - buildAction: buildAction, - testAction: testAction, - runAction: runAction, - archiveAction: archiveAction, - profileAction: profileAction, - analyzeAction: analyzeAction - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Settings+TestData.swift b/Sources/TuistGraphTesting/Models/Settings+TestData.swift deleted file mode 100644 index dfd95784d11..00000000000 --- a/Sources/TuistGraphTesting/Models/Settings+TestData.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Configuration { - public static func test( - settings: SettingsDictionary = [:], - xcconfig: AbsolutePath? = try! AbsolutePath(validating: "/Config.xcconfig") // swiftlint:disable:this force_try - ) -> Configuration { - Configuration(settings: settings, xcconfig: xcconfig) - } -} - -extension Settings { - public static func test( - base: SettingsDictionary, - // swiftlint:disable:next force_try - debug: Configuration, - // swiftlint:disable:next force_try - release: Configuration - ) -> Settings { - Settings( - base: base, - configurations: [.debug: debug, .release: release] - ) - } - - public static func test( - base: SettingsDictionary = [:], - baseDebug: SettingsDictionary = [:], - configurations: [BuildConfiguration: Configuration?] = [:] - ) -> Settings { - Settings( - base: base, - baseDebug: baseDebug, - configurations: configurations - ) - } - - public static func test(defaultSettings: DefaultSettings) -> Settings { - Settings( - base: [:], - configurations: [ - .debug: Configuration(settings: [:]), - .release: Configuration(settings: [:]), - ], - defaultSettings: defaultSettings - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Target+TestData.swift b/Sources/TuistGraphTesting/Models/Target+TestData.swift deleted file mode 100644 index 675de306dfc..00000000000 --- a/Sources/TuistGraphTesting/Models/Target+TestData.swift +++ /dev/null @@ -1,182 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Target { - /// Creates a Target with test data - /// Note: Referenced paths may not exist - public static func test( - name: String = "Target", - destinations: Destinations = [.iPhone, .iPad], - product: Product = .app, - productName: String? = nil, - bundleId: String? = nil, - deploymentTargets: DeploymentTargets = .iOS("13.1"), - infoPlist: InfoPlist? = nil, - entitlements: Entitlements? = nil, - settings: Settings? = Settings.test(), - sources: [SourceFile] = [], - resources: [ResourceFileElement] = [], - copyFiles: [CopyFilesAction] = [], - coreDataModels: [CoreDataModel] = [], - headers: Headers? = nil, - scripts: [TargetScript] = [], - environmentVariables: [String: EnvironmentVariable] = [:], - filesGroup: ProjectGroup = .group(name: "Project"), - dependencies: [TargetDependency] = [], - rawScriptBuildPhases: [RawScriptBuildPhase] = [], - launchArguments: [LaunchArgument] = [], - playgrounds: [AbsolutePath] = [], - additionalFiles: [FileElement] = [], - prune: Bool = false, - mergedBinaryType: MergedBinaryType = .disabled, - mergeable: Bool = false - ) -> Target { - Target( - name: name, - destinations: destinations, - product: product, - productName: productName, - bundleId: bundleId ?? "io.tuist.\(name)", - deploymentTargets: deploymentTargets, - infoPlist: infoPlist, - entitlements: entitlements, - settings: settings, - sources: sources, - resources: resources, - copyFiles: copyFiles, - headers: headers, - coreDataModels: coreDataModels, - scripts: scripts, - environmentVariables: environmentVariables, - launchArguments: launchArguments, - filesGroup: filesGroup, - dependencies: dependencies, - rawScriptBuildPhases: rawScriptBuildPhases, - playgrounds: playgrounds, - additionalFiles: additionalFiles, - prune: prune, - mergedBinaryType: mergedBinaryType, - mergeable: mergeable - ) - } - - /// Creates a Target with test data - /// Note: Referenced paths may not exist - public static func test( - name: String = "Target", - platform: Platform, - product: Product = .app, - productName: String? = nil, - bundleId: String? = nil, - deploymentTarget: DeploymentTargets = .iOS("13.1"), - infoPlist: InfoPlist? = nil, - entitlements: Entitlements? = nil, - settings: Settings? = Settings.test(), - sources: [SourceFile] = [], - resources: [ResourceFileElement] = [], - copyFiles: [CopyFilesAction] = [], - coreDataModels: [CoreDataModel] = [], - headers: Headers? = nil, - scripts: [TargetScript] = [], - environmentVariables: [String: EnvironmentVariable] = [:], - filesGroup: ProjectGroup = .group(name: "Project"), - dependencies: [TargetDependency] = [], - rawScriptBuildPhases: [RawScriptBuildPhase] = [], - launchArguments: [LaunchArgument] = [], - playgrounds: [AbsolutePath] = [], - additionalFiles: [FileElement] = [], - prune: Bool = false, - mergedBinaryType: MergedBinaryType = .disabled, - mergeable: Bool = false - ) -> Target { - Target( - name: name, - destinations: destinationsFrom(platform), - product: product, - productName: productName, - bundleId: bundleId ?? "io.tuist.\(name)", - deploymentTargets: deploymentTarget, - infoPlist: infoPlist, - entitlements: entitlements, - settings: settings, - sources: sources, - resources: resources, - copyFiles: copyFiles, - headers: headers, - coreDataModels: coreDataModels, - scripts: scripts, - environmentVariables: environmentVariables, - launchArguments: launchArguments, - filesGroup: filesGroup, - dependencies: dependencies, - rawScriptBuildPhases: rawScriptBuildPhases, - playgrounds: playgrounds, - additionalFiles: additionalFiles, - prune: prune, - mergedBinaryType: mergedBinaryType, - mergeable: mergeable - ) - } - - /// Creates a bare bones Target with as little data as possible - public static func empty( - name: String = "Target", - destinations: Destinations = [.iPhone, .iPad], - product: Product = .app, - productName: String? = nil, - bundleId: String? = nil, - deploymentTargets: DeploymentTargets = .init(), - infoPlist: InfoPlist? = nil, - entitlements: Entitlements? = nil, - settings: Settings? = nil, - sources: [SourceFile] = [], - resources: [ResourceFileElement] = [], - copyFiles: [CopyFilesAction] = [], - coreDataModels: [CoreDataModel] = [], - headers: Headers? = nil, - scripts: [TargetScript] = [], - environmentVariables: [String: EnvironmentVariable] = [:], - filesGroup: ProjectGroup = .group(name: "Project"), - dependencies: [TargetDependency] = [], - rawScriptBuildPhases: [RawScriptBuildPhase] = [] - ) -> Target { - Target( - name: name, - destinations: destinations, - product: product, - productName: productName, - bundleId: bundleId ?? "io.tuist.\(name)", - deploymentTargets: deploymentTargets, - infoPlist: infoPlist, - entitlements: entitlements, - settings: settings, - sources: sources, - resources: resources, - copyFiles: copyFiles, - headers: headers, - coreDataModels: coreDataModels, - scripts: scripts, - environmentVariables: environmentVariables, - filesGroup: filesGroup, - dependencies: dependencies, - rawScriptBuildPhases: rawScriptBuildPhases - ) - } - - // Maps a platform to a set of Destinations. For migration purposes - private static func destinationsFrom(_ platform: Platform) -> Destinations { - switch platform { - case .iOS: - return .iOS - case .macOS: - return .macOS - case .tvOS: - return .tvOS - case .watchOS: - return .watchOS - case .visionOS: - return .visionOS - } - } -} diff --git a/Sources/TuistGraphTesting/Models/Template+TestData.swift b/Sources/TuistGraphTesting/Models/Template+TestData.swift deleted file mode 100644 index 1b3f6cdd436..00000000000 --- a/Sources/TuistGraphTesting/Models/Template+TestData.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistGraph - -extension Template { - public static func test( - description: String = "Template", - attributes: [Attribute] = [], - items: [Template.Item] = [] - ) -> Template { - Template( - description: description, - attributes: attributes, - items: items - ) - } -} - -extension Template.Item { - public static func test( - path: RelativePath, - contents: Template.Contents = .string("test content") - ) -> Template.Item { - Template.Item( - path: path, - contents: contents - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/TestAction+TestData.swift b/Sources/TuistGraphTesting/Models/TestAction+TestData.swift deleted file mode 100644 index f5b99df0ff1..00000000000 --- a/Sources/TuistGraphTesting/Models/TestAction+TestData.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -@testable import TuistGraph - -extension TestAction { - public static func test( - targets: [TestableTarget] = [TestableTarget(target: TargetReference(projectPath: "/Project", name: "AppTests"))], - arguments: Arguments? = Arguments.test(), - configurationName: String = BuildConfiguration.debug.name, - attachDebugger: Bool = true, - coverage: Bool = false, - codeCoverageTargets: [TargetReference] = [], - expandVariableFromTarget: TargetReference? = nil, - preActions: [ExecutionAction] = [], - postActions: [ExecutionAction] = [], - diagnosticsOptions: Set = [.mainThreadChecker], - language: String? = nil, - region: String? = nil, - preferredScreenCaptureFormat: ScreenCaptureFormat? = nil, - testPlans: [TestPlan]? = nil, - skippedTests: [String]? = nil - ) -> TestAction { - TestAction( - targets: targets, - arguments: arguments, - configurationName: configurationName, - attachDebugger: attachDebugger, - coverage: coverage, - codeCoverageTargets: codeCoverageTargets, - expandVariableFromTarget: expandVariableFromTarget, - preActions: preActions, - postActions: postActions, - diagnosticsOptions: diagnosticsOptions, - language: language, - region: region, - preferredScreenCaptureFormat: preferredScreenCaptureFormat, - testPlans: testPlans, - skippedTests: skippedTests - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/Workspace+TestData.swift b/Sources/TuistGraphTesting/Models/Workspace+TestData.swift deleted file mode 100644 index 0d103764978..00000000000 --- a/Sources/TuistGraphTesting/Models/Workspace+TestData.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility -@testable import TuistGraph - -extension Workspace { - public static func test( - path: AbsolutePath = try! AbsolutePath(validating: "/"), // swiftlint:disable:this force_try - xcWorkspacePath: AbsolutePath = try! AbsolutePath(validating: "/"), // swiftlint:disable:this force_try - name: String = "test", - projects: [AbsolutePath] = [], - schemes: [Scheme] = [], - ideTemplateMacros: IDETemplateMacros? = nil, - additionalFiles: [FileElement] = [], - generationOptions: GenerationOptions = .test() - ) -> Workspace { - Workspace( - path: path, - xcWorkspacePath: xcWorkspacePath, - name: name, - projects: projects, - schemes: schemes, - generationOptions: generationOptions, - ideTemplateMacros: ideTemplateMacros, - additionalFiles: additionalFiles - ) - } -} - -extension Workspace.GenerationOptions { - public static func test( - enableAutomaticXcodeSchemes: Bool? = false, - autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes = .enabled( - codeCoverageMode: .disabled, - testingOptions: [], - testLanguage: nil, - testRegion: nil, - testScreenCaptureFormat: nil - ), - lastXcodeUpgradeCheck: Version? = nil - ) -> Self { - .init( - enableAutomaticXcodeSchemes: enableAutomaticXcodeSchemes, - autogeneratedWorkspaceSchemes: autogeneratedWorkspaceSchemes, - lastXcodeUpgradeCheck: lastXcodeUpgradeCheck, - renderMarkdownReadme: false - ) - } -} diff --git a/Sources/TuistGraphTesting/Models/XCFrameworkInfoPlist+TestData.swift b/Sources/TuistGraphTesting/Models/XCFrameworkInfoPlist+TestData.swift deleted file mode 100644 index edc7760b1d4..00000000000 --- a/Sources/TuistGraphTesting/Models/XCFrameworkInfoPlist+TestData.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import TSCBasic - -@testable import TuistGraph - -extension XCFrameworkInfoPlist { - public static func test(libraries: [XCFrameworkInfoPlist.Library] = [.test()]) -> XCFrameworkInfoPlist { - XCFrameworkInfoPlist(libraries: libraries) - } -} - -extension XCFrameworkInfoPlist.Library { - public static func test( - identifier: String = "test", - // swiftlint:disable:next force_try - path: RelativePath = try! RelativePath(validating: "relative/to/library"), - mergeable: Bool = false, - architectures: [BinaryArchitecture] = [.i386] - ) -> XCFrameworkInfoPlist.Library { - XCFrameworkInfoPlist.Library( - identifier: identifier, - path: path, - mergeable: mergeable, - architectures: architectures - ) - } -} diff --git a/Sources/TuistHasher/CachedContentHasher.swift b/Sources/TuistHasher/CachedContentHasher.swift new file mode 100644 index 00000000000..0fc36533884 --- /dev/null +++ b/Sources/TuistHasher/CachedContentHasher.swift @@ -0,0 +1,41 @@ +import Foundation +import Path +import TuistCore + +/// `CachedContentHasher` +/// is a wrapper on top of `ContentHasher` that adds an in-memory cache to avoid re-computing the same hashes +public final class CachedContentHasher: ContentHashing { + private let contentHasher: ContentHashing + + // In memory cache for files that have already been hashed + private var hashesCache: [AbsolutePath: String] = [:] + + public init(contentHasher: ContentHashing = ContentHasher()) { + self.contentHasher = contentHasher + } + + public func hash(_ data: Data) throws -> String { + try contentHasher.hash(data) + } + + public func hash(_ string: String) throws -> String { + try contentHasher.hash(string) + } + + public func hash(_ strings: [String]) throws -> String { + try contentHasher.hash(strings) + } + + public func hash(_ dictionary: [String: String]) throws -> String { + try contentHasher.hash(dictionary) + } + + public func hash(path filePath: AbsolutePath) throws -> String { + if let cachedHash = hashesCache[filePath] { + return cachedHash + } + let hash = try contentHasher.hash(path: filePath) + hashesCache[filePath] = hash + return hash + } +} diff --git a/Sources/TuistHasher/CopyFilesContentHasher.swift b/Sources/TuistHasher/CopyFilesContentHasher.swift new file mode 100644 index 00000000000..8b17a595394 --- /dev/null +++ b/Sources/TuistHasher/CopyFilesContentHasher.swift @@ -0,0 +1,30 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol CopyFilesContentHashing { + func hash(copyFiles: [CopyFilesAction]) throws -> String +} + +/// `CopyFilesContentHasher` +/// is responsible for computing a unique hash that identifies a list of CopyFilesAction models +public final class CopyFilesContentHasher: CopyFilesContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - CopyFilesContentHashing + + public func hash(copyFiles: [CopyFilesAction]) throws -> String { + var stringsToHash: [String] = [] + for action in copyFiles { + let fileHashes = try action.files.map { try contentHasher.hash(path: $0.path) } + stringsToHash.append(contentsOf: fileHashes + [action.name, action.destination.rawValue, action.subpath ?? ""]) + } + return try contentHasher.hash(stringsToHash) + } +} diff --git a/Sources/TuistHasher/CoreDataModelsContentHasher.swift b/Sources/TuistHasher/CoreDataModelsContentHasher.swift new file mode 100644 index 00000000000..f61d079f63c --- /dev/null +++ b/Sources/TuistHasher/CoreDataModelsContentHasher.swift @@ -0,0 +1,34 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol CoreDataModelsContentHashing { + func hash(coreDataModels: [CoreDataModel]) throws -> String +} + +/// `CoreDataModelsContentHasher` +/// is responsible for computing a unique hash that identifies a list of CoreData models +public final class CoreDataModelsContentHasher: CoreDataModelsContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - CoreDataModelsContentHashing + + public func hash(coreDataModels: [CoreDataModel]) throws -> String { + var stringsToHash: [String] = [] + for cdModel in coreDataModels { + let contentHash = try contentHasher.hash(path: cdModel.path) + let currentVersionHash = try contentHasher.hash([cdModel.currentVersion]) + let cdModelHash = try contentHasher.hash([contentHash, currentVersionHash]) + let versionsHash = try contentHasher.hash(try cdModel.versions.sorted().map { try contentHasher.hash(path: $0) }) + stringsToHash.append(cdModelHash) + stringsToHash.append(versionsHash) + } + return try contentHasher.hash(stringsToHash) + } +} diff --git a/Sources/TuistHasher/DependenciesContentHasher.swift b/Sources/TuistHasher/DependenciesContentHasher.swift new file mode 100644 index 00000000000..15adcc1d5c1 --- /dev/null +++ b/Sources/TuistHasher/DependenciesContentHasher.swift @@ -0,0 +1,126 @@ +import Foundation +import Path +import TuistCore +import TuistSupport +import XcodeGraph + +public protocol DependenciesContentHashing { + func hash( + graphTarget: GraphTarget, + hashedTargets: inout [GraphHashedTarget: String], + hashedPaths: inout [AbsolutePath: String] + ) throws -> String +} + +enum DependenciesContentHasherError: FatalError, Equatable { + case missingTargetHash( + sourceTargetName: String, + dependencyProjectPath: AbsolutePath, + dependencyTargetName: String + ) + case missingProjectTargetHash( + sourceProjectPath: AbsolutePath, + sourceTargetName: String, + dependencyProjectPath: AbsolutePath, + dependencyTargetName: String + ) + + var description: String { + switch self { + case let .missingTargetHash(sourceTargetName, dependencyProjectPath, dependencyTargetName): + return "The target '\(sourceTargetName)' depends on the target '\(dependencyTargetName)' from the same project at path \(dependencyProjectPath.pathString) whose hash hasn't been previously calculated." + case let .missingProjectTargetHash(sourceProjectPath, sourceTargetName, dependencyProjectPath, dependencyTargetName): + return "The target '\(sourceTargetName)' from project at path \(sourceProjectPath.pathString) depends on the target '\(dependencyTargetName)' from the project at path \(dependencyProjectPath.pathString) whose hash hasn't been previously calculated." + } + } + + var type: ErrorType { + switch self { + case .missingTargetHash: return .bug + case .missingProjectTargetHash: return .bug + } + } +} + +/// `DependencyContentHasher` +/// is responsible for computing a hash that uniquely identifies a target dependency +public final class DependenciesContentHasher: DependenciesContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - DependenciesContentHashing + + public func hash( + graphTarget: GraphTarget, + hashedTargets: inout [GraphHashedTarget: String], + hashedPaths: inout [AbsolutePath: String] + ) throws -> String { + let hashes = try graphTarget.target.dependencies + .map { try hash(graphTarget: graphTarget, dependency: $0, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) } + return hashes.sorted().compactMap { $0 }.joined() + } + + // MARK: - Private + + private func hash( + graphTarget: GraphTarget, + dependency: TargetDependency, + hashedTargets: inout [GraphHashedTarget: String], + hashedPaths: inout [AbsolutePath: String] + ) throws -> String { + switch dependency { + case let .target(targetName, _): + guard let dependencyHash = hashedTargets[GraphHashedTarget(projectPath: graphTarget.path, targetName: targetName)] + else { + throw DependenciesContentHasherError.missingTargetHash( + sourceTargetName: graphTarget.target.name, + dependencyProjectPath: graphTarget.path, + dependencyTargetName: targetName + ) + } + return dependencyHash + case let .project(targetName, projectPath, _): + guard let dependencyHash = hashedTargets[GraphHashedTarget(projectPath: projectPath, targetName: targetName)] else { + throw DependenciesContentHasherError.missingProjectTargetHash( + sourceProjectPath: graphTarget.path, + sourceTargetName: graphTarget.target.name, + dependencyProjectPath: projectPath, + dependencyTargetName: targetName + ) + } + return dependencyHash + case let .framework(path, _, _), let .xcframework(path, _, _): + return try cachedHash(path: path, hashedPaths: &hashedPaths) + case let .library(path, publicHeaders, swiftModuleMap, _): + let libraryHash = try cachedHash(path: path, hashedPaths: &hashedPaths) + let publicHeadersHash = try contentHasher.hash(path: publicHeaders) + if let swiftModuleMap { + let swiftModuleHash = try contentHasher.hash(path: swiftModuleMap) + return try contentHasher.hash("library-\(libraryHash)-\(publicHeadersHash)-\(swiftModuleHash)") + } else { + return try contentHasher.hash("library-\(libraryHash)-\(publicHeadersHash)") + } + case let .package(product, type, _): + return try contentHasher.hash("package-\(product)-\(type.rawValue)") + case let .sdk(name, status, _): + return try contentHasher.hash("sdk-\(name)-\(status)") + case .xctest: + return try contentHasher.hash("xctest") + } + } + + private func cachedHash(path: AbsolutePath, hashedPaths: inout [AbsolutePath: String]) throws -> String { + if let pathHash = hashedPaths[path] { + return pathHash + } else { + let pathHash = try contentHasher.hash(path: path) + hashedPaths[path] = pathHash + return pathHash + } + } +} diff --git a/Sources/TuistHasher/DeploymentTargetsContentHasher.swift b/Sources/TuistHasher/DeploymentTargetsContentHasher.swift new file mode 100644 index 00000000000..855489296c5 --- /dev/null +++ b/Sources/TuistHasher/DeploymentTargetsContentHasher.swift @@ -0,0 +1,29 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol DeploymentTargetsContentHashing { + func hash(deploymentTargets: DeploymentTargets) throws -> String +} + +/// `DeploymentTargetsContentHasher` +/// is responsible for computing a hash that uniquely identifies a `DeploymentTargets` +public final class DeploymentTargetsContentHasher: DeploymentTargetsContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - DeploymentTargetsContentHashing + + public func hash(deploymentTargets: DeploymentTargets) throws -> String { + let stringToHash: String = deploymentTargets.configuredVersions.map { platform, version in + "\(platform.caseValue)-\(version)" + }.joined(separator: ",") + + return try contentHasher.hash(stringToHash) + } +} diff --git a/Sources/TuistHasher/Extensions/Plist+Extras.swift b/Sources/TuistHasher/Extensions/Plist+Extras.swift new file mode 100644 index 00000000000..b96c291f86d --- /dev/null +++ b/Sources/TuistHasher/Extensions/Plist+Extras.swift @@ -0,0 +1,24 @@ +import Foundation +import XcodeGraph + +extension Plist.Value { + /** + Normalizes `Plist.Value` into a type from the Swift standard library + */ + func normalize() -> Any { + switch self { + case let .array(array): + return array.map { $0.normalize() } + case let .boolean(boolean): + return boolean + case let .dictionary(dictionary): + return dictionary.mapValues { $0.normalize() } + case let .integer(integer): + return integer + case let .real(real): + return real + case let .string(string): + return string + } + } +} diff --git a/Sources/TuistHasher/GraphContentHasher.swift b/Sources/TuistHasher/GraphContentHasher.swift new file mode 100644 index 00000000000..faeb217cdb0 --- /dev/null +++ b/Sources/TuistHasher/GraphContentHasher.swift @@ -0,0 +1,112 @@ +import Foundation +import Mockable +import Path +import TuistCore +import TuistSupport +import XcodeGraph + +@Mockable +public protocol GraphContentHashing { + /// Hashes graph + /// - Parameters: + /// - graph: Graph to hash + /// - filter: If `true`, `TargetNode` is hashed, otherwise it is skipped + /// - additionalStrings: Additional strings to be used when hashing graph + func contentHashes( + for graph: Graph, + include: @escaping (GraphTarget) -> Bool, + additionalStrings: [String] + ) throws -> [GraphTarget: String] +} + +/// `GraphContentHasher` +/// is responsible for computing an hash that uniquely identifies a Tuist `Graph`. +/// It considers only targets that are considered cacheable: frameworks without dependencies on XCTest or on non-cacheable targets +public final class GraphContentHasher: GraphContentHashing { + private let targetContentHasher: TargetContentHashing + + // MARK: - Init + + public convenience init(contentHasher: ContentHashing) { + let targetContentHasher = TargetContentHasher(contentHasher: contentHasher) + self.init(targetContentHasher: targetContentHasher) + } + + public init(targetContentHasher: TargetContentHashing) { + self.targetContentHasher = targetContentHasher + } + + // MARK: - GraphContentHashing + + public func contentHashes( + for graph: Graph, + include: (GraphTarget) -> Bool, + additionalStrings: [String] + ) throws -> [GraphTarget: String] { + let graphTraverser = GraphTraverser(graph: graph) + var visitedIsHasheableNodes: [GraphTarget: Bool] = [:] + var hashedTargets: [GraphHashedTarget: String] = [:] + var hashedPaths: [AbsolutePath: String] = [:] + + let sortedCacheableTargets = try graphTraverser.allTargetsTopologicalSorted() + + let hashableTargets = sortedCacheableTargets.compactMap { target -> GraphTarget? in + if isHashable( + target, + graphTraverser: graphTraverser, + visited: &visitedIsHasheableNodes, + include: include + ) { + return target + } else { + return nil + } + } + + let hashes = try hashableTargets.map { (target: GraphTarget) -> String in + let hash = try targetContentHasher.contentHash( + for: target, + hashedTargets: &hashedTargets, + hashedPaths: &hashedPaths, + additionalStrings: additionalStrings + ) + hashedTargets[ + GraphHashedTarget( + projectPath: target.path, + targetName: target.target.name + ) + ] = hash + return hash + } + return Dictionary(uniqueKeysWithValues: zip(hashableTargets, hashes)) + } + + // MARK: - Private + + private func isHashable( + _ target: GraphTarget, + graphTraverser: GraphTraversing, + visited: inout [GraphTarget: Bool], + include: (GraphTarget) -> Bool + ) -> Bool { + guard include(target) else { + visited[target] = false + return false + } + if let visitedValue = visited[target] { return visitedValue } + let allTargetDependenciesAreHashable = graphTraverser.directTargetDependencies( + path: target.path, + name: target.target.name + ) + .allSatisfy { + isHashable( + $0.graphTarget, + graphTraverser: graphTraverser, + visited: &visited, + include: include + ) + } + visited[target] = allTargetDependenciesAreHashable + return allTargetDependenciesAreHashable + } +} diff --git a/Sources/TuistHasher/GraphHashedTarget.swift b/Sources/TuistHasher/GraphHashedTarget.swift new file mode 100644 index 00000000000..61e2009fd4d --- /dev/null +++ b/Sources/TuistHasher/GraphHashedTarget.swift @@ -0,0 +1,12 @@ +import Foundation +import Path +import XcodeGraph + +/// It represents a target that has been hashed. +public struct GraphHashedTarget: Equatable, Hashable { + /// Path to the directory containing the project. + let projectPath: AbsolutePath + + /// Name of the hashed target. + let targetName: String +} diff --git a/Sources/TuistHasher/HeadersContentHasher.swift b/Sources/TuistHasher/HeadersContentHasher.swift new file mode 100644 index 00000000000..f488a8ccbc1 --- /dev/null +++ b/Sources/TuistHasher/HeadersContentHasher.swift @@ -0,0 +1,27 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol HeadersContentHashing { + func hash(headers: Headers) throws -> String +} + +/// `HeadersContentHashing` +/// is responsible for computing a hash that uniquely identifies a list of headers +public final class HeadersContentHasher: HeadersContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - HeadersContentHashing + + public func hash(headers: Headers) throws -> String { + let allHeaders = headers.public + headers.private + headers.project + let headersContent = try allHeaders.map { try contentHasher.hash(path: $0) } + return try contentHasher.hash(headersContent) + } +} diff --git a/Sources/TuistHasher/Log/Logger.swift b/Sources/TuistHasher/Log/Logger.swift new file mode 100644 index 00000000000..a4d4e6e2f0a --- /dev/null +++ b/Sources/TuistHasher/Log/Logger.swift @@ -0,0 +1,3 @@ +import TuistSupport + +let logger = Logger(label: "io.tuist.analytics") diff --git a/Sources/TuistHasher/PlistContentHasher.swift b/Sources/TuistHasher/PlistContentHasher.swift new file mode 100644 index 00000000000..f377931d902 --- /dev/null +++ b/Sources/TuistHasher/PlistContentHasher.swift @@ -0,0 +1,56 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol PlistContentHashing { + func hash(plist: Plist) throws -> String +} + +/// `PlistContentHasher` +/// is responsible for computing a hash that uniquely identifies a property-list file (e.g. `Info.plist` or `.entitlements`) +public final class PlistContentHasher: PlistContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - PlistContentHashing + + public func hash(plist: Plist) throws -> String { + switch plist { + case let .infoPlist(infoPlist): + switch infoPlist { + case let .file(path): + return try contentHasher.hash(path: path) + case let .dictionary(dictionary), let .extendingDefault(dictionary): + var dictionaryString: String = "" + for key in dictionary.keys.sorted() { + let value = dictionary[key, default: "nil"] + dictionaryString += "\(key)=\(value);" + } + return try contentHasher.hash(dictionaryString) + case let .generatedFile(_, data): + return try contentHasher.hash(data) + } + case let .entitlements(entitlements): + switch entitlements { + case let .variable(variable): + return try contentHasher.hash(variable) + case let .file(path): + return try contentHasher.hash(path: path) + case let .dictionary(dictionary): + var dictionaryString: String = "" + for key in dictionary.keys.sorted() { + let value = dictionary[key, default: "nil"] + dictionaryString += "\(key)=\(value);" + } + return try contentHasher.hash(dictionaryString) + case let .generatedFile(_, data): + return try contentHasher.hash(data) + } + } + } +} diff --git a/Sources/TuistHasher/PrivacyManifestContentHasher.swift b/Sources/TuistHasher/PrivacyManifestContentHasher.swift new file mode 100644 index 00000000000..772ee49e949 --- /dev/null +++ b/Sources/TuistHasher/PrivacyManifestContentHasher.swift @@ -0,0 +1,36 @@ +import Foundation +import Path +import TuistCore +import XcodeGraph + +public protocol PrivacyManifestContentHashing { + func hash(_ privacyManifest: PrivacyManifest) throws -> String +} + +public final class PrivacyManifestContentHasher: PrivacyManifestContentHashing { + private let contentHasher: ContentHashing + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + public func hash(_ privacyManifest: PrivacyManifest) throws -> String { + var hashes: [String] = [] + + hashes.append(try contentHasher.hash(privacyManifest.tracking ? "1" : "0")) + hashes.append(try contentHasher.hash(privacyManifest.trackingDomains)) + hashes.append(try contentHasher.hash(privacyManifest.collectedDataTypes.asJSONString())) + hashes.append(try contentHasher.hash(privacyManifest.accessedApiTypes.asJSONString())) + + return try contentHasher.hash(hashes) + } +} + +extension [[String: Plist.Value]] { + fileprivate func asJSONString() throws -> String { + let normalized = map { dictionary in + dictionary.mapValues { $0.normalize() } + } + return String(data: try JSONSerialization.data(withJSONObject: normalized, options: .sortedKeys), encoding: .utf8)! + } +} diff --git a/Sources/TuistHasher/ResourcesContentHasher.swift b/Sources/TuistHasher/ResourcesContentHasher.swift new file mode 100644 index 00000000000..474863747d4 --- /dev/null +++ b/Sources/TuistHasher/ResourcesContentHasher.swift @@ -0,0 +1,45 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol ResourcesContentHashing { + func hash(resources: ResourceFileElements) throws -> String +} + +/// `ResourcesContentHasher` +/// is responsible for computing a unique hash that identifies a list of resources +public final class ResourcesContentHasher: ResourcesContentHashing { + private let contentHasher: ContentHashing + private let privacyManifestContentHasher: PrivacyManifestContentHasher + + // MARK: - Init + + public convenience init(contentHasher: ContentHashing) { + self.init( + contentHasher: contentHasher, + privacyManifestContentHasher: PrivacyManifestContentHasher(contentHasher: contentHasher) + ) + } + + public init( + contentHasher: ContentHashing, + privacyManifestContentHasher: PrivacyManifestContentHasher + ) { + self.contentHasher = contentHasher + self.privacyManifestContentHasher = privacyManifestContentHasher + } + + // MARK: - ResourcesContentHashing + + public func hash(resources: ResourceFileElements) throws -> String { + var hashes = try resources.resources + .sorted(by: { $0.path < $1.path }) + .map { try contentHasher.hash(path: $0.path) } + + if let privacyManifest = resources.privacyManifest { + hashes.append(try privacyManifestContentHasher.hash(privacyManifest)) + } + + return try contentHasher.hash(hashes) + } +} diff --git a/Sources/TuistHasher/SettingsContentHasher.swift b/Sources/TuistHasher/SettingsContentHasher.swift new file mode 100644 index 00000000000..92b5df756b3 --- /dev/null +++ b/Sources/TuistHasher/SettingsContentHasher.swift @@ -0,0 +1,75 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol SettingsContentHashing { + func hash(settings: Settings) throws -> String +} + +/// `SettingsContentHasher` +/// is responsible for computing a hash that uniquely identifies some `Settings` +public final class SettingsContentHasher: SettingsContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - SettingsContentHashing + + public func hash(settings: Settings) throws -> String { + let baseSettingsHash = try hash(settings.base) + let configurationHash = try hash(settings.configurations) + let defaultSettingsHash = try hash(settings.defaultSettings) + return try contentHasher.hash([baseSettingsHash, configurationHash, defaultSettingsHash]) + } + + private func hash(_ configurations: [BuildConfiguration: Configuration?]) throws -> String { + var configurationHashes: [String] = [] + for buildConfiguration in configurations.keys.sorted() { + var configurationHash = buildConfiguration.name + buildConfiguration.variant.rawValue + if let configuration = configurations[buildConfiguration] { + if let configuration { + configurationHash += try hash(configuration) + } + } + configurationHashes.append(configurationHash) + } + return try contentHasher.hash(configurationHashes) + } + + private func hash(_ settingsDictionary: SettingsDictionary) throws -> String { + let sortedAndNormalizedSettings = settingsDictionary + .sorted(by: { $0.0 < $1.0 }) + .map { "\($0):\($1.normalize())" }.joined(separator: "-") + return try contentHasher.hash(sortedAndNormalizedSettings) + } + + private func hash(_ configuration: Configuration) throws -> String { + var configurationHash = try hash(configuration.settings) + if let xcconfigPath = configuration.xcconfig { + let xcconfigHash = try contentHasher.hash(path: xcconfigPath) + configurationHash += xcconfigHash + } + return configurationHash + } + + private func hash(_ defaultSettings: DefaultSettings) throws -> String { + var defaultSettingHash: String + switch defaultSettings { + case let .recommended(excludedKeys): + defaultSettingHash = "recommended" + let excludedKeysHash = try contentHasher.hash(excludedKeys.sorted()) + defaultSettingHash += excludedKeysHash + case let .essential(excludedKeys): + defaultSettingHash = "essential" + let excludedKeysHash = try contentHasher.hash(excludedKeys.sorted()) + defaultSettingHash += excludedKeysHash + case .none: + defaultSettingHash = "none" + } + return defaultSettingHash + } +} diff --git a/Sources/TuistHasher/SourceFilesContentHasher.swift b/Sources/TuistHasher/SourceFilesContentHasher.swift new file mode 100644 index 00000000000..c04f321c10b --- /dev/null +++ b/Sources/TuistHasher/SourceFilesContentHasher.swift @@ -0,0 +1,42 @@ +import Foundation +import TuistCore +import XcodeGraph + +public protocol SourceFilesContentHashing { + func hash(sources: [SourceFile]) throws -> String +} + +/// `SourceFilesContentHasher` +/// is responsible for computing a unique hash that identifies a list of source files, considering their content +public final class SourceFilesContentHasher: SourceFilesContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - SourceFilesContentHashing + + /// Returns a unique hash that identifies an arry of sourceFiles + /// First it hashes the content of every file and append to every hash the compiler flags of the file. It assumes the files + /// are always sorted the same way. + /// Then it hashes again all partial hashes to get a unique identifier that represents a group of source files together with + /// their compiler flags + public func hash(sources: [SourceFile]) throws -> String { + var stringsToHash: [String] = [] + for source in sources.sorted(by: { $0.path < $1.path }) { + if let hash = source.contentHash { + stringsToHash.append(hash) + } else { + var sourceHash = try contentHasher.hash(path: source.path) + if let compilerFlags = source.compilerFlags { + sourceHash += try contentHasher.hash(compilerFlags) + } + stringsToHash.append(sourceHash) + } + } + return try contentHasher.hash(stringsToHash) + } +} diff --git a/Sources/TuistHasher/TargetContentHasher.swift b/Sources/TuistHasher/TargetContentHasher.swift new file mode 100644 index 00000000000..e0153b1fd20 --- /dev/null +++ b/Sources/TuistHasher/TargetContentHasher.swift @@ -0,0 +1,137 @@ +import Foundation +import Path +import TuistCore +import TuistSupport +import XcodeGraph + +public protocol TargetContentHashing { + func contentHash( + for target: GraphTarget, + hashedTargets: inout [GraphHashedTarget: String], + hashedPaths: inout [AbsolutePath: String], + additionalStrings: [String] + ) throws -> String +} + +/// `TargetContentHasher` +/// is responsible for computing a unique hash that identifies a target +public final class TargetContentHasher: TargetContentHashing { + private let contentHasher: ContentHashing + private let coreDataModelsContentHasher: CoreDataModelsContentHashing + private let sourceFilesContentHasher: SourceFilesContentHashing + private let targetScriptsContentHasher: TargetScriptsContentHashing + private let resourcesContentHasher: ResourcesContentHashing + private let copyFilesContentHasher: CopyFilesContentHashing + private let headersContentHasher: HeadersContentHashing + private let deploymentTargetContentHasher: DeploymentTargetsContentHashing + private let plistContentHasher: PlistContentHashing + private let settingsContentHasher: SettingsContentHashing + private let dependenciesContentHasher: DependenciesContentHashing + + // MARK: - Init + + public convenience init(contentHasher: ContentHashing) { + self.init( + contentHasher: contentHasher, + sourceFilesContentHasher: SourceFilesContentHasher(contentHasher: contentHasher), + targetScriptsContentHasher: TargetScriptsContentHasher(contentHasher: contentHasher), + coreDataModelsContentHasher: CoreDataModelsContentHasher(contentHasher: contentHasher), + resourcesContentHasher: ResourcesContentHasher(contentHasher: contentHasher), + copyFilesContentHasher: CopyFilesContentHasher(contentHasher: contentHasher), + headersContentHasher: HeadersContentHasher(contentHasher: contentHasher), + deploymentTargetContentHasher: DeploymentTargetsContentHasher(contentHasher: contentHasher), + plistContentHasher: PlistContentHasher(contentHasher: contentHasher), + settingsContentHasher: SettingsContentHasher(contentHasher: contentHasher), + dependenciesContentHasher: DependenciesContentHasher(contentHasher: contentHasher) + ) + } + + public init( + contentHasher: ContentHashing, + sourceFilesContentHasher: SourceFilesContentHashing, + targetScriptsContentHasher: TargetScriptsContentHashing, + coreDataModelsContentHasher: CoreDataModelsContentHashing, + resourcesContentHasher: ResourcesContentHashing, + copyFilesContentHasher: CopyFilesContentHashing, + headersContentHasher: HeadersContentHashing, + deploymentTargetContentHasher: DeploymentTargetsContentHashing, + plistContentHasher: PlistContentHashing, + settingsContentHasher: SettingsContentHashing, + dependenciesContentHasher: DependenciesContentHashing + ) { + self.contentHasher = contentHasher + self.sourceFilesContentHasher = sourceFilesContentHasher + self.coreDataModelsContentHasher = coreDataModelsContentHasher + self.targetScriptsContentHasher = targetScriptsContentHasher + self.resourcesContentHasher = resourcesContentHasher + self.copyFilesContentHasher = copyFilesContentHasher + self.headersContentHasher = headersContentHasher + self.deploymentTargetContentHasher = deploymentTargetContentHasher + self.plistContentHasher = plistContentHasher + self.settingsContentHasher = settingsContentHasher + self.dependenciesContentHasher = dependenciesContentHasher + } + + // MARK: - TargetContentHashing + + public func contentHash( + for graphTarget: GraphTarget, + hashedTargets: inout [GraphHashedTarget: String], + hashedPaths: inout [AbsolutePath: String], + additionalStrings: [String] = [] + ) throws -> String { + let sourcesHash = try sourceFilesContentHasher.hash(sources: graphTarget.target.sources) + let resourcesHash = try resourcesContentHasher.hash(resources: graphTarget.target.resources) + let copyFilesHash = try copyFilesContentHasher.hash(copyFiles: graphTarget.target.copyFiles) + let coreDataModelHash = try coreDataModelsContentHasher.hash(coreDataModels: graphTarget.target.coreDataModels) + let targetScriptsHash = try targetScriptsContentHasher.hash( + targetScripts: graphTarget.target.scripts, + sourceRootPath: graphTarget.project.sourceRootPath + ) + let dependenciesHash = try dependenciesContentHasher.hash( + graphTarget: graphTarget, + hashedTargets: &hashedTargets, + hashedPaths: &hashedPaths + ) + let environmentHash = try contentHasher.hash(graphTarget.target.environmentVariables.mapValues(\.value)) + var stringsToHash = [ + graphTarget.target.name, + graphTarget.target.product.rawValue, + graphTarget.target.bundleId, + graphTarget.target.productName, + dependenciesHash, + sourcesHash, + resourcesHash, + copyFilesHash, + coreDataModelHash, + targetScriptsHash, + environmentHash, + ] + + stringsToHash.append(contentsOf: graphTarget.target.destinations.map(\.rawValue).sorted()) + + if let headers = graphTarget.target.headers { + let headersHash = try headersContentHasher.hash(headers: headers) + stringsToHash.append(headersHash) + } + + let deploymentTargetHash = try deploymentTargetContentHasher.hash(deploymentTargets: graphTarget.target.deploymentTargets) + stringsToHash.append(deploymentTargetHash) + + if let infoPlist = graphTarget.target.infoPlist { + let infoPlistHash = try plistContentHasher.hash(plist: .infoPlist(infoPlist)) + stringsToHash.append(infoPlistHash) + } + if let entitlements = graphTarget.target.entitlements { + let entitlementsHash = try plistContentHasher.hash(plist: .entitlements(entitlements)) + stringsToHash.append(entitlementsHash) + } + if let settings = graphTarget.target.settings { + let settingsHash = try settingsContentHasher.hash(settings: settings) + stringsToHash.append(settingsHash) + } + stringsToHash += additionalStrings + + return try contentHasher.hash(stringsToHash) + } +} diff --git a/Sources/TuistHasher/TargetScriptsContentHasher.swift b/Sources/TuistHasher/TargetScriptsContentHasher.swift new file mode 100644 index 00000000000..c8d648c5ee4 --- /dev/null +++ b/Sources/TuistHasher/TargetScriptsContentHasher.swift @@ -0,0 +1,61 @@ +import Foundation +import Path +import TuistCore +import XcodeGraph + +public protocol TargetScriptsContentHashing { + func hash(targetScripts: [TargetScript], sourceRootPath: AbsolutePath) throws -> String +} + +/// `TargetScriptsContentHasher` +/// is responsible for computing a unique hash that identifies a list of target scripts +public final class TargetScriptsContentHasher: TargetScriptsContentHashing { + private let contentHasher: ContentHashing + + // MARK: - Init + + public init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + + // MARK: - TargetScriptsContentHashing + + /// Returns the hash that uniquely identifies an array of target scripts + /// The hash takes into consideration the content of the script to execute, the content of input/output files, the name of the + /// tool to execute, the order, the arguments and its name + public func hash(targetScripts: [TargetScript], sourceRootPath: AbsolutePath) throws -> String { + var stringsToHash: [String] = [] + for script in targetScripts { + var pathsToHash: [AbsolutePath] = [] + script.path.map { pathsToHash.append($0) } + + var dynamicPaths = script.inputPaths.compactMap { try? AbsolutePath(validating: $0) } + script.inputFileListPaths + if let dependencyFile = script.dependencyFile { + dynamicPaths += [dependencyFile] + } + + for path in dynamicPaths { + if path.pathString.contains("$") { + stringsToHash.append(path.relative(to: sourceRootPath).pathString) + logger.notice( + "The path of the file \'\(path.url.lastPathComponent)\' is hashed, not the content. Because it has a build variable." + ) + } else { + pathsToHash.append(path) + } + } + stringsToHash.append(contentsOf: try pathsToHash.map { try contentHasher.hash(path: $0) }) + stringsToHash.append( + contentsOf: (script.outputPaths.compactMap { try? AbsolutePath(validating: $0) } + script.outputFileListPaths) + .map { $0.relative(to: sourceRootPath).pathString } + ) + + stringsToHash.append(contentsOf: [ + script.name, + script.tool ?? "", + script.order.rawValue, + ] + script.arguments) + } + return try contentHasher.hash(stringsToHash) + } +} diff --git a/Sources/TuistKit/Commands/AuthCommand.swift b/Sources/TuistKit/Commands/AuthCommand.swift new file mode 100644 index 00000000000..dea4440f516 --- /dev/null +++ b/Sources/TuistKit/Commands/AuthCommand.swift @@ -0,0 +1,40 @@ +import ArgumentParser +import Foundation +import Path + +struct AuthCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "auth", + abstract: "Authenticates the user" + ) + } + + @Option( + help: "Email to authenticate with.", + envKey: .authEmail + ) + var email: String? + + @Option( + help: "Password to authenticate with.", + envKey: .authPassword + ) + var password: String? + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .authPath + ) + var path: String? + + func run() async throws { + try await AuthService().authenticate( + email: email, + password: password, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/BuildCommand.swift b/Sources/TuistKit/Commands/BuildCommand.swift index f83dc09e5b7..97fbe9218e9 100644 --- a/Sources/TuistKit/Commands/BuildCommand.swift +++ b/Sources/TuistKit/Commands/BuildCommand.swift @@ -1,109 +1,201 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TSCUtility +import TuistServer import TuistSupport +import XcodeGraph -/// Command that builds a target from the project in the current directory. -public struct BuildCommand: AsyncParsableCommand { - public init() {} - public static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "build", - abstract: "Builds a project" - ) +enum XcodeBuildPassthroughArgumentError: FatalError, Equatable { + case alreadyHandled(String) + + var description: String { + switch self { + case let .alreadyHandled(argument): + "The argument \(argument) added after the terminator (--) cannot be passed through to xcodebuild because it is handled by Tuist." + } + } + + var type: ErrorType { + switch self { + case .alreadyHandled: + .abort + } } +} + +public struct BuildOptions: ParsableArguments { + public init() {} + + public static var generatorFactory: GeneratorFactorying = GeneratorFactory() + public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory() @Argument( - help: "The scheme to be built. By default it builds all the buildable schemes of the project in the current directory." + help: "The scheme to be built. By default it builds all the buildable schemes of the project in the current directory.", + envKey: .buildOptionsScheme ) - var scheme: String? + public var scheme: String? @Flag( - help: "Force the generation of the project before building." + help: "Force the generation of the project before building.", + envKey: .buildOptionsGenerate ) - var generate: Bool = false + public var generate: Bool = false @Flag( - help: "When passed, it cleans the project before building it" + help: "When passed, it cleans the project before building it", + envKey: .buildOptionsClean ) - var clean: Bool = false + public var clean: Bool = false @Option( name: .shortAndLong, help: "The path to the directory that contains the project to be built.", - completion: .directory + completion: .directory, + envKey: .buildOptionsPath ) - var path: String? + public var path: String? @Option( name: .shortAndLong, - help: "Build on a specific device." + help: "Build on a specific device.", + envKey: .buildOptionsDevice ) - var device: String? + public var device: String? @Option( name: .long, - help: "Build for a specific platform." + help: "Build for a specific platform.", + envKey: .buildOptionsPlatform ) - var platform: String? + public var platform: XcodeGraph.Platform? @Option( name: .shortAndLong, - help: "Build with a specific version of the OS." + help: "Build with a specific version of the OS.", + envKey: .buildOptionsOS ) - var os: String? + public var os: String? @Flag( name: .long, - help: "When passed, append arch=x86_64 to the 'destination' to run simulator in a Rosetta mode." + help: "When passed, append arch=x86_64 to the 'destination' to run simulator in a Rosetta mode.", + envKey: .buildOptionsRosetta ) - var rosetta: Bool = false + public var rosetta: Bool = false @Option( name: [.long, .customShort("C")], - help: "The configuration to be used when building the scheme." + help: "The configuration to be used when building the scheme.", + envKey: .buildOptionsConfiguration ) - var configuration: String? + public var configuration: String? @Option( help: "The directory where build products will be copied to when the project is built.", - completion: .directory + completion: .directory, + envKey: .buildOptionsOutputPath ) - var buildOutputPath: String? + public var buildOutputPath: String? @Option( - help: "Overrides the folder that should be used for derived data when building the project." + help: "[Deprecated] Overrides the folder that should be used for derived data when building the project.", + envKey: .buildOptionsDerivedDataPath ) - var derivedDataPath: String? + public var derivedDataPath: String? @Flag( name: .long, - help: "When passed, it generates the project and skips building. This is useful for debugging purposes." + help: "When passed, it generates the project and skips building. This is useful for debugging purposes.", + envKey: .buildOptionsGenerateOnly + ) + public var generateOnly: Bool = false + + @Argument( + parsing: .postTerminator, + help: "Arguments that will be passed through to xcodebuild", + envKey: .buildOptionsPassthroughXcodeBuildArguments + ) + var passthroughXcodeBuildArguments: [String] = [] +} + +/// Command that builds a target from the project in the current directory. +public struct BuildCommand: AsyncParsableCommand { + public init() {} + public static var generatorFactory: GeneratorFactorying = GeneratorFactory() + public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory() + + public static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "build", + abstract: "Builds a project" + ) + } + + @OptionGroup() + var buildOptions: BuildOptions + + @Flag( + help: "Ignore binary cache and use sources only.", + envKey: .buildBinaryCache ) - var generateOnly: Bool = false + var binaryCache: Bool = true + + private var notAllowedPassthroughXcodeBuildArguments = [ + "-scheme", + "-workspace", + "-project", + ] public func run() async throws { - let absolutePath: AbsolutePath - if let path { - absolutePath = try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + // Check if passthrough arguments are already handled by tuist + try notAllowedPassthroughXcodeBuildArguments.forEach { + if buildOptions.passthroughXcodeBuildArguments.contains($0) { + throw XcodeBuildPassthroughArgumentError.alreadyHandled($0) + } + } + + // Suggest the user to use passthrough arguments if already supported by xcodebuild + if let derivedDataPath = buildOptions.derivedDataPath { + logger + .warning( + "--derivedDataPath is deprecated please use -derivedDataPath \(derivedDataPath) after the terminator (--) instead to passthrough parameters to xcodebuild" + ) + } + + let absolutePath = if let path = buildOptions.path { + try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) } else { - absolutePath = FileHandler.shared.currentPath + FileHandler.shared.currentPath } - try await BuildService().run( - schemeName: scheme, - generate: generate, - clean: clean, - configuration: configuration, - buildOutputPath: buildOutputPath.map { try AbsolutePath(validating: $0, relativeTo: FileHandler.shared.currentPath) }, - derivedDataPath: derivedDataPath, + try await BuildService( + generatorFactory: Self.generatorFactory, + cacheStorageFactory: Self.cacheStorageFactory + ).run( + schemeName: buildOptions.scheme, + generate: buildOptions.generate, + clean: buildOptions.clean, + configuration: buildOptions.configuration, + ignoreBinaryCache: !binaryCache, + buildOutputPath: buildOptions.buildOutputPath.map { try AbsolutePath( + validating: $0, + relativeTo: FileHandler.shared.currentPath + ) }, + derivedDataPath: buildOptions.derivedDataPath, path: absolutePath, - device: device, - platform: platform, - osVersion: os, - rosetta: rosetta, - generateOnly: generateOnly + device: buildOptions.device, + platform: buildOptions.platform, + osVersion: buildOptions.os, + rosetta: buildOptions.rosetta, + generateOnly: buildOptions.generateOnly, + passthroughXcodeBuildArguments: buildOptions.passthroughXcodeBuildArguments ) } } + +extension XcodeGraph.Platform: ExpressibleByArgument { + public init?(argument: String) { + self.init(commandLineValue: argument) + } +} diff --git a/Sources/TuistKit/Commands/Cache/CachePrintHashesService.swift b/Sources/TuistKit/Commands/Cache/CachePrintHashesService.swift new file mode 100644 index 00000000000..89bde79c5ad --- /dev/null +++ b/Sources/TuistKit/Commands/Cache/CachePrintHashesService.swift @@ -0,0 +1,75 @@ +import Foundation +import Path +import TuistAutomation +import TuistCache +import TuistCore +import TuistHasher +import TuistLoader +import TuistSupport + +final class CachePrintHashesService { + private let generatorFactory: GeneratorFactorying + private let cacheGraphContentHasher: CacheGraphContentHashing + private let clock: Clock + private let configLoader: ConfigLoading + + convenience init( + contentHasher: ContentHashing = CachedContentHasher(), + generatorFactory: GeneratorFactorying + ) { + self.init( + generatorFactory: generatorFactory, + cacheGraphContentHasher: CacheGraphContentHasher(contentHasher: contentHasher), + clock: WallClock(), + configLoader: ConfigLoader(manifestLoader: ManifestLoader()) + ) + } + + init( + generatorFactory: GeneratorFactorying, + cacheGraphContentHasher: CacheGraphContentHashing, + clock: Clock, + configLoader: ConfigLoading + ) { + self.generatorFactory = generatorFactory + self.cacheGraphContentHasher = cacheGraphContentHasher + self.clock = clock + self.configLoader = configLoader + } + + private func absolutePath(_ path: String?) throws -> AbsolutePath { + if let path { + return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + } else { + return FileHandler.shared.currentPath + } + } + + func run( + path: String?, + configuration: String? + ) async throws { + let absolutePath = try absolutePath(path) + let timer = clock.startTimer() + let config = try await configLoader.loadConfig(path: absolutePath) + let generator = generatorFactory.defaultGenerator(config: config) + let graph = try await generator.load(path: absolutePath) + let hashes = try cacheGraphContentHasher.contentHashes( + for: graph, + configuration: configuration, + config: config, + excludedTargets: [] + ) + let duration = timer.stop() + let time = String(format: "%.3f", duration) + guard hashes.count > 0 else { + logger.notice("No cacheable targets were found") + return + } + let sortedHashes = hashes.sorted { $0.key.target.name < $1.key.target.name } + for (target, hash) in sortedHashes { + logger.info("\(target.target.name) - \(hash)") + } + logger.notice("Total time taken: \(time)s") + } +} diff --git a/Sources/TuistKit/Commands/Cache/CacheService.swift b/Sources/TuistKit/Commands/Cache/CacheService.swift new file mode 100644 index 00000000000..65f6871db51 --- /dev/null +++ b/Sources/TuistKit/Commands/Cache/CacheService.swift @@ -0,0 +1,26 @@ +import Foundation + +public protocol CacheServicing { + func run( + path directory: String?, + configuration: String?, + targetsToBinaryCache: Set, + externalOnly: Bool, + generateOnly: Bool + ) async throws +} + +final class EmptyCacheService: CacheServicing { + func run( + path _: String?, + configuration _: String?, + targetsToBinaryCache _: Set, + externalOnly _: Bool, + generateOnly _: Bool + ) async throws { + logger + .notice( + "Caching is currently not opensourced. Please, report issues with caching on GitHub and the Tuist team will take a look." + ) + } +} diff --git a/Sources/TuistKit/Commands/CacheCommand.swift b/Sources/TuistKit/Commands/CacheCommand.swift new file mode 100644 index 00000000000..48dbd28b6af --- /dev/null +++ b/Sources/TuistKit/Commands/CacheCommand.swift @@ -0,0 +1,91 @@ +import AnyCodable +import ArgumentParser +import Foundation +import Path +import TuistServer +import TuistSupport + +/// Command to cache targets as `.(xc)framework`s and speed up your and your peers' build times. +public struct CacheCommand: AsyncParsableCommand, HasTrackableParameters { + public init() {} + + public var runId = "" + public static var generatorFactory: GeneratorFactorying = GeneratorFactory() + public static var analyticsDelegate: TrackableParametersDelegate? + public static var cacheService: CacheServicing = EmptyCacheService() + + public static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "cache", + abstract: "Warms the local and remote cache." + ) + } + + @Option( + name: .shortAndLong, + help: "The path to the directory that contains the project whose targets will be cached.", + completion: .directory + ) + var path: String? + + @Option( + name: .shortAndLong, + help: "Configuration to use for binary caching." + ) + var configuration: String? + + @Argument(help: """ + A list of targets to cache. \ + Those and their dependant targets will be cached. \ + If no target is specified, all the project targets (excluding the external ones) and their dependencies will be cached. + """) + var targets: [String] = [] + + @Flag( + help: "If passed, the command doesn't cache the targets passed in the `--targets` argument, but only their dependencies" + ) + var externalOnly: Bool = false + + @Flag( + name: .long, + help: "When passed, it generates the project and skips warming the cache. This is useful for debugging purposes." + ) + var generateOnly: Bool = false + + @Flag( + name: .long, + help: "When passed, the hashes of the cacheable frameworks in the given project are printed." + ) + var printHashes: Bool = false + + public func run() async throws { + if printHashes { + try await CachePrintHashesService( + generatorFactory: Self.generatorFactory + ).run( + path: path, + configuration: configuration + ) + return + } + + try await Self.cacheService.run( + path: path, + configuration: configuration, + targetsToBinaryCache: Set(targets), + externalOnly: externalOnly, + generateOnly: generateOnly + ) + CacheCommand.analyticsDelegate?.addParameters( + [ + "n_targets": AnyCodable(targets.count), + "cacheable_targets": AnyCodable(CacheAnalyticsStore.shared.cacheableTargets), + "local_cache_target_hits": AnyCodable(CacheAnalyticsStore.shared.localCacheTargetsHits), + "remote_cache_target_hits": AnyCodable(CacheAnalyticsStore.shared.remoteCacheTargetsHits), + "test_targets": AnyCodable(CacheAnalyticsStore.shared.testTargets), + "local_test_target_hits": AnyCodable(CacheAnalyticsStore.shared.localTestTargetHits), + "remote_test_target_hits": AnyCodable(CacheAnalyticsStore.shared.remoteTestTargetHits), + ] + ) + } +} diff --git a/Sources/TuistKit/Commands/CleanCommand.swift b/Sources/TuistKit/Commands/CleanCommand.swift index 5f9771ca2e1..9f668386131 100644 --- a/Sources/TuistKit/Commands/CleanCommand.swift +++ b/Sources/TuistKit/Commands/CleanCommand.swift @@ -2,37 +2,7 @@ import ArgumentParser import Foundation import TuistCore -/// Category that can be cleaned -enum CleanCategory: ExpressibleByArgument { - static let allCases = CacheCategory.allCases.map { .global($0) } + [Self.dependencies] - - /// The global cache - case global(CacheCategory) - - /// The local dependencies cache - case dependencies - - var defaultValueDescription: String { - switch self { - case let .global(cacheCategory): - return cacheCategory.rawValue - case .dependencies: - return "dependencies" - } - } - - init?(argument: String) { - if let cacheCategory = CacheCategory(rawValue: argument) { - self = .global(cacheCategory) - } else if argument == "dependencies" { - self = .dependencies - } else { - return nil - } - } -} - -public struct CleanCommand: ParsableCommand { +public struct CleanCommand: AsyncParsableCommand { public init() {} public static var configuration: CommandConfiguration { @@ -42,19 +12,30 @@ public struct CleanCommand: ParsableCommand { ) } - @Argument(help: "The cache and artifact categories to be cleaned. If no category is specified, everything is cleaned.") - var cleanCategories: [CleanCategory] = CleanCategory.allCases + @Argument( + help: "The cache and artifact categories to be cleaned. If no category is specified, everything is cleaned.", + envKey: .cleanCleanCategories + ) + var cleanCategories: [TuistCleanCategory] = TuistCleanCategory.allCases.map { $0 } + + @Flag( + help: "Clean the remote cache", + envKey: .cleanRemote + ) + var remote: Bool = false @Option( name: .shortAndLong, help: "The path to the directory that contains the project that should be cleaned.", - completion: .directory + completion: .directory, + envKey: .cleanPath ) var path: String? - public func run() throws { - try CleanService().run( + public func run() async throws { + try await CleanService().run( categories: cleanCategories, + remote: remote, path: path ) } diff --git a/Sources/TuistKit/Commands/DumpCommand.swift b/Sources/TuistKit/Commands/DumpCommand.swift index 093b4b84a7f..cee25521cf4 100644 --- a/Sources/TuistKit/Commands/DumpCommand.swift +++ b/Sources/TuistKit/Commands/DumpCommand.swift @@ -1,11 +1,13 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TuistLoader import TuistSupport -struct DumpCommand: AsyncParsableCommand { - static var configuration: CommandConfiguration { +public struct DumpCommand: AsyncParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "dump", abstract: "Outputs the manifest as a JSON" @@ -17,14 +19,15 @@ struct DumpCommand: AsyncParsableCommand { @Option( name: .shortAndLong, help: "The path to the folder where the manifest is", - completion: .directory + completion: .directory, + envKey: .dumpPath ) var path: String? - @Argument(help: "The manifest to be dumped") + @Argument(help: "The manifest to be dumped", envKey: .dumpManifest) var manifest: DumpableManifest = .project - func run() async throws { + public func run() async throws { try await DumpService().run(path: path, manifest: manifest) } } diff --git a/Sources/TuistKit/Commands/EditCommand.swift b/Sources/TuistKit/Commands/EditCommand.swift index 649836850c9..f03edf85359 100644 --- a/Sources/TuistKit/Commands/EditCommand.swift +++ b/Sources/TuistKit/Commands/EditCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TuistGenerator import TuistSupport @@ -17,19 +17,22 @@ public struct EditCommand: AsyncParsableCommand { @Option( name: .shortAndLong, help: "The path to the directory whose project will be edited", - completion: .directory + completion: .directory, + envKey: .editPath ) var path: String? @Flag( name: [.long, .customShort("P")], - help: "It creates the project in the current directory or the one indicated by -p and doesn't block the process" + help: "It creates the project in the current directory or the one indicated by -p and doesn't block the process", + envKey: .editPermanent ) var permanent: Bool = false @Flag( name: [.long, .customShort("o")], - help: "It only includes the manifest in the current directory." + help: "It only includes the manifest in the current directory.", + envKey: .editOnlyCurrentDirectory ) var onlyCurrentDirectory: Bool = false diff --git a/Sources/TuistKit/Commands/EnvKey/EnvKey.swift b/Sources/TuistKit/Commands/EnvKey/EnvKey.swift new file mode 100644 index 00000000000..524aeef0742 --- /dev/null +++ b/Sources/TuistKit/Commands/EnvKey/EnvKey.swift @@ -0,0 +1,242 @@ +import ArgumentParser +import Foundation +import TuistSupport + +public enum EnvKey: String, CaseIterable { + // BUILD + case buildBinaryCache = "TUIST_BUILD_BINARY_CACHE" + + // BUILD OPTIONS + case buildOptionsScheme = "TUIST_BUILD_OPTIONS_SCHEME" + case buildOptionsGenerate = "TUIST_BUILD_OPTIONS_GENERATE" + case buildOptionsClean = "TUIST_BUILD_OPTIONS_CLEAN" + case buildOptionsPath = "TUIST_BUILD_OPTIONS_PATH" + case buildOptionsDevice = "TUIST_BUILD_OPTIONS_DEVICE" + case buildOptionsPlatform = "TUIST_BUILD_OPTIONS_PLATFORM" + case buildOptionsOS = "TUIST_BUILD_OPTIONS_OS" + case buildOptionsRosetta = "TUIST_BUILD_OPTIONS_ROSETTA" + case buildOptionsConfiguration = "TUIST_BUILD_OPTIONS_CONFIGURATION" + case buildOptionsOutputPath = "TUIST_BUILD_OPTIONS_BUILD_OUTPUT_PATH" + case buildOptionsDerivedDataPath = "TUIST_BUILD_OPTIONS_DERIVED_DATA_PATH" + case buildOptionsGenerateOnly = "TUIST_BUILD_OPTIONS_GENERATE_ONLY" + case buildOptionsPassthroughXcodeBuildArguments = "TUIST_BUILD_OPTIONS_PASSTHROUGH_XCODE_BUILD_ARGUMENTS" + + // CLEAN + case cleanCleanCategories = "TUIST_CLEAN_CLEAN_CATEGORIES" + case cleanPath = "TUIST_CLEAN_PATH" + case cleanRemote = "TUIST_CLEAN_REMOTE" + + // DUMP + case dumpPath = "TUIST_DUMP_PATH" + case dumpManifest = "TUIST_DUMP_MANIFEST" + + // EDIT + case editPath = "TUIST_EDIT_PATH" + case editPermanent = "TUIST_EDIT_PERMANENT" + case editOnlyCurrentDirectory = "TUIST_EDIT_ONLY_CURRENT_DIRECTORY" + + // INSTALL + case installPath = "TUIST_INSTALL_PATH" + case installUpdate = "TUIST_INSTALL_UPDATE" + + // GENERATE + case generatePath = "TUIST_GENERATE_PATH" + case generateOpen = "TUIST_GENERATE_OPEN" + case generateBinaryCache = "TUIST_GENERATE_BINARY_CACHE" + + // GRAPH + case graphSkipTestTargets = "TUIST_GRAPH_SKIP_TEST_TARGETS" + case graphSkipExternalDependencies = "TUIST_GRAPH_SKIP_EXTERNAL_DEPENDENCIES" + case graphPlatform = "TUIST_GRAPH_PLATFORM" + case graphFormat = "TUIST_GRAPH_FORMAT" + case graphOpen = "TUIST_GRAPH_OPEN" + case graphLayoutAlgorithm = "TUIST_GRAPH_LAYOUT_ALGORITHM" + case graphTargets = "TUIST_GRAPH_TARGETS" + case graphPath = "TUIST_GRAPH_PATH" + case graphOutputPath = "TUIST_GRAPH_OUTPUT_PATH" + + // INIT + case initPlatform = "TUIST_INIT_PLATFORM" + case initName = "TUIST_INIT_NAME" + case initTemplate = "TUIST_INIT_TEMPLATE" + case initPath = "TUIST_INIT_PATH" + + // MIGRATION + case migrationSettingsToXcconfigXcodeprojPath = "TUIST_MIGRATION_SETTINGS_TO_XCCONFIG_XCODEPROJ_PATH" + case migrationSettingsToXcconfigXcconfigPath = "TUIST_MIGRATION_SETTINGS_TO_XCCONFIG_XCCONFIG_PATH" + case migrationSettingsToXcconfigTarget = "TUIST_MIGRATION_SETTINGS_TO_XCCONFIG_TARGET" + case migrationCheckEmptySettingsXcodeprojPath = "TUIST_MIGRATION_CHECK_EMPTY_SETTINGS_XCODEPROJ_PATH" + case migrationCheckEmptySettingsTarget = "TUIST_MIGRATION_CHECK_EMPTY_SETTINGS_TARGET" + case migrationListTargetsXcodeprojPath = "TUIST_MIGRATION_LIST_TARGETS_XCODEPROJ_PATH" + + // PLUGIN + case pluginArchivePath = "TUIST_PLUGIN_ARCHIVE_PATH" + case pluginBuildBuildTests = "TUIST_PLUGIN_BUILD_BUILD_TESTS" + case pluginBuildShowBinPath = "TUIST_PLUGIN_BUILD_SHOW_BIN_PATH" + case pluginBuildTargets = "TUIST_PLUGIN_BUILD_TARGETS" + case pluginBuildProducts = "TUIST_PLUGIN_BUILD_PRODUCTS" + case pluginRunBuildTests = "TUIST_PLUGIN_RUN_BUILD_TESTS" + case pluginRunSkipBuild = "TUIST_PLUGIN_RUN_SKIP_BUILD" + case pluginRunTask = "TUIST_PLUGIN_RUN_TASK" + case pluginRunArguments = "TUIST_PLUGIN_RUN_ARGUMENTS" + case pluginTestBuildTests = "TUIST_PLUGIN_TEST_BUILD_TESTS" + case pluginTestTestProducts = "TUIST_PLUGIN_TEST_TEST_PRODUCTS" + + // PLUGIN OPTIONS + case pluginOptionsConfiguration = "TUIST_PLUGIN_OPTIONS_CONFIGURATION" + case pluginOptionsPath = "TUIST_PLUGIN_OPTIONS_PATH" + + // RUN + case runBuildTests = "TUIST_RUN_BUILD_TESTS" + case runSkipBuild = "TUIST_RUN_SKIP_BUILD" + case runTask = "TUIST_RUN_TASK" + case runArguments = "TUIST_RUN_ARGUMENTS" + case runGenerate = "TUIST_RUN_GENERATE" + case runClean = "TUIST_RUN_CLEAN" + case runPath = "TUIST_RUN_PATH" + case runConfiguration = "TUIST_RUN_CONFIGURATION" + case runDevice = "TUIST_RUN_DEVICE" + case runOS = "TUIST_RUN_OS" + case runRosetta = "TUIST_RUN_ROSETTA" + case runScheme = "TUIST_RUN_SCHEME" + + // SCAFFOLD + case scaffoldTemplate = "TUIST_SCAFFOLD_TEMPLATE" + case scaffoldJson = "TUIST_SCAFFOLD_JSON" + case scaffoldPath = "TUIST_SCAFFOLD_PATH" + case scaffoldListJson = "TUIST_SCAFFOLD_LIST_JSON" + case scaffoldListPath = "TUIST_SCAFFOLD_LIST_PATH" + + // TEST + case testScheme = "TUIST_TEST_SCHEME" + case testClean = "TUIST_TEST_CLEAN" + case testPath = "TUIST_TEST_PATH" + case testDevice = "TUIST_TEST_DEVICE" + case testPlatform = "TUIST_TEST_PLATFORM" + case testOS = "TUIST_TEST_OS" + case testRosetta = "TUIST_TEST_ROSETTA" + case testConfiguration = "TUIST_TEST_CONFIGURATION" + case testSkipUITests = "TUIST_TEST_SKIP_UITESTS" + case testResultBundlePath = "TUIST_TEST_RESULT_BUNDLE_PATH" + case testDerivedDataPath = "TUIST_TEST_DERIVED_DATA_PATH" + case testRetryCount = "TUIST_TEST_RETRY_COUNT" + case testTestPlan = "TUIST_TEST_TEST_PLAN" + case testTestTargets = "TUIST_TEST_TEST_TARGETS" + case testSkipTestTargets = "TUIST_TEST_SKIP_TEST_TARGETS" + case testConfigurations = "TUIST_TEST_CONFIGURATIONS" + case testSkipConfigurations = "TUIST_TEST_SKIP_CONFIGURATIONS" + case testGenerateOnly = "TUIST_TEST_GENERATE_ONLY" + case testBinaryCache = "TUIST_TEST_BINARY_CACHE" + case testSelectiveTesting = "TUIST_TEST_SELECTIVE_TESTING" + + // ORGANIZATION BILLING + case organizationBillingOrganizationName = "TUIST_ORGANIZATION_BILLING_ORGANIZATION_NAME" + case organizationBillingPath = "TUIST_ORGANIZATION_BILLING_PATH" + + // ORGANIZATION CREATE + case organizationCreateOrganizationName = "TUIST_ORGANIZATION_CREATE_ORGANIZATION_NAME" + case organizationCreatePath = "TUIST_ORGANIZATION_CREATE_PATH" + + // ORGANIZATION DELETE + case organizationDeleteOrganizationName = "TUIST_ORGANIZATION_DELETE_ORGANIZATION_NAME" + case organizationDeletePath = "TUIST_ORGANIZATION_DELETE_PATH" + + // PROJECT TOKEN + case projectTokenFullHandle = "TUIST_PROJECT_TOKEN_FULL_HANDLE" + case projectTokenPath = "TUIST_PROJECT_TOKEN_PATH" + case projectTokenId = "TUIST_PROJECT_TOKEN_ID" + + // ORGANIZATION LIST + case organizationListJson = "TUIST_ORGANIZATION_LIST_JSON" + case organizationListPath = "TUIST_ORGANIZATION_LIST_PATH" + + // ORGANIZATION REMOVE INVITE + case organizationRemoveInviteOrganizationName = "TUIST_ORGANIZATION_REMOVE_INVITE_ORGANIZATION_NAME" + case organizationRemoveInviteEmail = "TUIST_ORGANIZATION_REMOVE_INVITE_EMAIL" + case organizationRemoveInvitePath = "TUIST_ORGANIZATION_REMOVE_INVITE_PATH" + + // ORGANIZATION REMOVE MEMBER + case organizationRemoveMemberOrganizationName = "TUIST_ORGANIZATION_REMOVE_MEMBER_ORGANIZATION_NAME" + case organizationRemoveMemberUsername = "TUIST_ORGANIZATION_REMOVE_MEMBER_USERNAME" + case organizationRemoveMemberPath = "TUIST_ORGANIZATION_REMOVE_MEMBER_PATH" + + // ORGANIZATION REMOVE SSO + case organizationRemoveSSOOrganizationName = "TUIST_ORGANIZATION_REMOVE_SSO_ORGANIZATION_NAME" + case organizationRemoveSSOPath = "TUIST_ORGANIZATION_REMOVE_SSO_PATH" + + // ORGANIZATION UPDATE SSO + case organizationUpdateSSOOrganizationName = "TUIST_ORGANIZATION_UPDATE_SSO_ORGANIZATION_NAME" + case organizationUpdateSSOProvider = "TUIST_ORGANIZATION_UPDATE_SSO_PROVIDER" + case organizationUpdateSSOOrganizationId = "TUIST_ORGANIZATION_UPDATE_SSO_ORGANIZATION_ID" + case organizationUpdateSSOPath = "TUIST_ORGANIZATION_UPDATE_SSO_PATH" + + // PROJECT DELETE + case projectDeleteFullHandle = "TUIST_PROJECT_DELETE_FULL_HANDLE" + case projectDeletePath = "TUIST_PROJECT_DELETE_PATH" + + // PROJECT CREATE + case projectCreateFullHandle = "TUIST_PROJECT_CREATE_FULL_HANDLE" + case projectCreatePath = "TUIST_PROJECT_CREATE_PATH" + + // PROJECT SHOW + case projectShowFullHandle = "TUIST_PROJECT_SHOW_FULL_HANDLE" + case projectShowPath = "TUIST_PROJECT_SHOW_PATH" + case projectShowWeb = "TUIST_PROJECT_SHOW_WEB" + + // ORGANIZATION INVITE + case organizationInviteOrganizationName = "TUIST_ORGANIZATION_INVITE_ORGANIZATION_NAME" + case organizationInviteEmail = "TUIST_ORGANIZATION_INVITE_EMAIL" + case organizationInvitePath = "TUIST_ORGANIZATION_INVITE_PATH" + + // ORGANIZATION SHOW + case organizationShowOrganizationName = "TUIST_ORGANIZATION_SHOW_ORGANIZATION_NAME" + case organizationShowJson = "TUIST_ORGANIZATION_SHOW_JSON" + case organizationShowPath = "TUIST_ORGANIZATION_SHOW_PATH" + + // PROJECT LIST + case projectListJson = "TUIST_PROJECT_LIST_JSON" + case projectListPath = "TUIST_PROJECT_LIST_PATH" + + // ORGANIZATION UPDATE MEMBER + case organizationUpdateMemberOrganizationName = "TUIST_ORGANIZATION_UPDATE_MEMBER_ORGANIZATION_NAME" + case organizationUpdateMemberUsername = "TUIST_ORGANIZATION_UPDATE_MEMBER_USERNAME" + case organizationUpdateMemberRole = "TUIST_ORGANIZATION_UPDATE_MEMBER_ROLE" + case organizationUpdateMemberPath = "TUIST_ORGANIZATION_UPDATE_MEMBER_PATH" + + // AUTH + case authPath = "TUIST_AUTH_PATH" + case authEmail = "TUIST_AUTH_EMAIL" + case authPassword = "TUIST_AUTH_PASSWORD" + + // SESSION + case sessionPath = "TUIST_SESSION_PATH" + + // LOGOUT + case logoutPath = "TUIST_LOGOUT_PATH" + + // ANALYTICS + case analyticsPath = "TUIST_ANALYTICS_PATH" + + // SHARE + case shareApp = "TUIST_SHARE_APP" + case shareConfiguration = "TUIST_SHARE_CONFIGURATION" + case sharePlatform = "TUIST_SHARE_PLATFORM" + case shareDerivedDataPath = "TUIST_SHARE_DERIVED_DATA_PATH" +} + +extension EnvKey { + var envValueString: String? { + Environment.shared.tuistVariables[rawValue] + } + + func envValue() -> T? { + guard let envValueString else { + return nil + } + return T(argument: envValueString) + } + + func envValue() -> [T]? { + return envValueString?.split(separator: ",").compactMap { T(argument: String($0)) } + } +} diff --git a/Sources/TuistKit/Commands/EnvKey/ParseableArgument+Extension.swift b/Sources/TuistKit/Commands/EnvKey/ParseableArgument+Extension.swift new file mode 100644 index 00000000000..5faf69b7801 --- /dev/null +++ b/Sources/TuistKit/Commands/EnvKey/ParseableArgument+Extension.swift @@ -0,0 +1,221 @@ +import ArgumentParser +import Foundation +import TuistSupport + +extension Option { + public init( + wrappedValue value: [T] = [], + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where T: ExpressibleByArgument, Value == [T] { + let envValue: Value? = envKey.envValue() + if let envValue { + self.init( + wrappedValue: envValue, + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } else { + self.init( + wrappedValue: value, + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } + } + + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where T: ExpressibleByArgument, Value == T? { + if let value: T = envKey.envValue() { + self.init( + wrappedValue: value, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) { argument in + T(argument: argument) + } + } else { + self.init( + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } + } + + init( + wrappedValue value: Value, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where Value: ExpressibleByArgument { + let envValue: Value? = envKey.envValue() + if let envValue { + self.init( + wrappedValue: envValue, + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } else { + self.init( + wrappedValue: value, + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } + } + + init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where Value: ExpressibleByArgument { + let envValue: Value? = envKey.envValue() + if let envValue { + self.init( + wrappedValue: envValue, + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } else { + self.init( + name: name, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } + } +} + +extension Flag where Value == Bool { + public init( + wrappedValue: Bool, + name: NameSpecification = .long, + help: ArgumentHelp? = nil, + envKey: EnvKey + ) { + if let envValue: Value = envKey.envValue() { + self.init( + wrappedValue: envValue, + name: name, + inversion: .prefixedNo, + help: help?.withEnvKey(envKey) + ) + } else { + self.init( + wrappedValue: wrappedValue, + name: name, + inversion: .prefixedNo, + help: help?.withEnvKey(envKey) + ) + } + } +} + +// Argument Extensions +extension Argument { + init( + wrappedValue value: [T] = [], + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where T: ExpressibleByArgument, Value == [T] { + let envValue: Value? = envKey.envValue() + if let envValue { + self.init( + wrappedValue: envValue, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } else { + self.init( + wrappedValue: value, + parsing: parsingStrategy, + help: help?.withEnvKey(envKey), + completion: completion + ) + } + } + + init( + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where Value: ExpressibleByArgument { + let envValue: Value? = envKey.envValue() + if let envValue { + self.init(wrappedValue: envValue, help: help?.withEnvKey(envKey), completion: completion) + } else { + self.init(help: help?.withEnvKey(envKey), completion: completion) + } + } + + init( + wrappedValue value: Value, + help: ArgumentHelp? = nil, + envKey: EnvKey + ) where Value: ExpressibleByArgument { + let envValue: Value? = envKey.envValue() + if let envValue { + self.init(wrappedValue: envValue, help: help?.withEnvKey(envKey)) + } else { + self.init(wrappedValue: value, help: help?.withEnvKey(envKey)) + } + } + + public init( + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + envKey: EnvKey + ) where T: ExpressibleByArgument, Value == T? { + if let value: T = envKey.envValue() { + self.init( + wrappedValue: value, + help: help?.withEnvKey(envKey), + completion: completion + ) { argument in + T(argument: argument) + } + } else { + self.init( + help: help?.withEnvKey(envKey), + completion: completion + ) + } + } +} + +extension ArgumentHelp { + func withEnvKey(_ envKey: EnvKey) -> ArgumentHelp { + var help = self + help.abstract += " (env: \(envKey.rawValue))" + return help + } +} diff --git a/Sources/TuistKit/Commands/FetchCommand.swift b/Sources/TuistKit/Commands/FetchCommand.swift deleted file mode 100644 index c2cfc65bab0..00000000000 --- a/Sources/TuistKit/Commands/FetchCommand.swift +++ /dev/null @@ -1,39 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic - -enum FetchCategory: String, CaseIterable, RawRepresentable, ExpressibleByArgument { - case dependencies - case plugins -} - -/// A command to fetch any remote content necessary to interact with the project. -public struct FetchCommand: AsyncParsableCommand { - public init() {} - public static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "fetch", - abstract: "Fetches any remote content necessary to interact with the project." - ) - } - - @Option( - name: .shortAndLong, - help: "The path to the directory or a subdirectory of the project.", - completion: .directory - ) - var path: String? - - @Flag( - name: .shortAndLong, - help: "Instead of simple fetch, update external content when available." - ) - var update: Bool = false - - public func run() async throws { - try await FetchService().run( - path: path, - update: update - ) - } -} diff --git a/Sources/TuistKit/Commands/GenerateCommand.swift b/Sources/TuistKit/Commands/GenerateCommand.swift index 9a5993b2e59..f2a9e5eacca 100644 --- a/Sources/TuistKit/Commands/GenerateCommand.swift +++ b/Sources/TuistKit/Commands/GenerateCommand.swift @@ -2,11 +2,16 @@ import AnyCodable import ArgumentParser import Foundation import TuistCore +import TuistServer import TuistSupport public struct GenerateCommand: AsyncParsableCommand, HasTrackableParameters { public init() {} + public static var analyticsDelegate: TrackableParametersDelegate? + public static var generatorFactory: GeneratorFactorying = GeneratorFactory() + public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory() + public var runId = UUID().uuidString public static var configuration: CommandConfiguration { CommandConfiguration( @@ -19,20 +24,62 @@ public struct GenerateCommand: AsyncParsableCommand, HasTrackableParameters { @Option( name: .shortAndLong, help: "The path to the directory or a subdirectory of the project.", - completion: .directory + completion: .directory, + envKey: .generatePath ) var path: String? + @Argument(help: """ + A list of targets to focus on. \ + Other targets will be linked as binaries if possible. \ + If no target is specified, all the project targets will be generated (except external ones, such as Swift packages). + """) + var sources: [String] = [] + @Flag( name: .shortAndLong, - help: "Don't open the project after generating it." + help: "Don't open the project after generating it.", + envKey: .generateOpen + ) + var open: Bool = !CIChecker().isCI() + + @Flag( + help: "Ignore binary cache and use sources only.", + envKey: .generateBinaryCache + ) + var binaryCache: Bool = true + + @Option( + name: .shortAndLong, + help: "Configuration to generate for." ) - var noOpen: Bool = false + var configuration: String? public func run() async throws { - try await GenerateService().run( + defer { + GenerateCommand.analyticsDelegate?.addParameters( + [ + "no_open": AnyCodable(!open), + "no_binary_cache": AnyCodable(!binaryCache), + "n_targets": AnyCodable(sources.count), + "cacheable_targets": AnyCodable(CacheAnalyticsStore.shared.cacheableTargets), + "local_cache_target_hits": AnyCodable(CacheAnalyticsStore.shared.localCacheTargetsHits), + "remote_cache_target_hits": AnyCodable(CacheAnalyticsStore.shared.remoteCacheTargetsHits), + "test_targets": AnyCodable(CacheAnalyticsStore.shared.testTargets), + "local_test_target_hits": AnyCodable(CacheAnalyticsStore.shared.localTestTargetHits), + "remote_test_target_hits": AnyCodable(CacheAnalyticsStore.shared.remoteTestTargetHits), + ] + ) + } + try await GenerateService( + cacheStorageFactory: Self.cacheStorageFactory, + generatorFactory: Self.generatorFactory + ).run( path: path, - noOpen: noOpen + sources: Set(sources), + noOpen: !open, + configuration: configuration, + ignoreBinaryCache: !binaryCache ) } } diff --git a/Sources/TuistKit/Commands/GraphCommand.swift b/Sources/TuistKit/Commands/GraphCommand.swift index 4b96ec6e764..619d7c51d9e 100644 --- a/Sources/TuistKit/Commands/GraphCommand.swift +++ b/Sources/TuistKit/Commands/GraphCommand.swift @@ -2,17 +2,18 @@ import AnyCodable import ArgumentParser import Foundation import GraphViz -import TSCBasic +import Path import TuistGenerator -import TuistGraph import TuistLoader import TuistSupport +import XcodeGraph /// Command that generates and exports a dot graph from the workspace or project in the current directory. public struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { public init() {} public static var analyticsDelegate: TrackableParametersDelegate? + public var runId = UUID().uuidString public static var configuration: CommandConfiguration { CommandConfiguration( @@ -23,53 +24,64 @@ public struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { @Flag( name: [.customShort("t"), .long], - help: "Skip Test targets during graph rendering." + help: "Skip Test targets during graph rendering.", + envKey: .graphSkipTestTargets ) var skipTestTargets: Bool = false @Flag( name: [.customShort("d"), .long], - help: "Skip external dependencies." + help: "Skip external dependencies.", + envKey: .graphSkipExternalDependencies ) var skipExternalDependencies: Bool = false @Option( name: [.customShort("l"), .long], - help: "A platform to filter. Only targets for this platform will be showed in the graph. Available platforms: ios, macos, tvos, watchos" + help: "A platform to filter. Only targets for this platform will be showed in the graph. Available platforms: ios, macos, tvos, watchos", + envKey: .graphPlatform ) var platform: Platform? @Option( name: [.customShort("f"), .long], - help: "Available formats: dot, json, png, svg" + help: "Available formats: dot, json, png, svg", + envKey: .graphFormat ) var format: GraphFormat = .png @Flag( - name: .shortAndLong, - help: "Don't open the file after generating it." + name: .long, + help: "Don't open the file after generating it.", + envKey: .graphOpen ) - var noOpen: Bool = false + var open: Bool = true @Option( name: [.customShort("a"), .customLong("algorithm")], - help: "Available formats: dot, neato, twopi, circo, fdp, sfdp, patchwork" + help: "Available formats: dot, neato, twopi, circo, fdp, sfdp, patchwork", + envKey: .graphLayoutAlgorithm ) var layoutAlgorithm: GraphViz.LayoutAlgorithm = .dot - @Argument(help: "A list of targets to filter. Those and their dependent targets will be showed in the graph.") + @Argument( + help: "A list of targets to filter. Those and their dependent targets will be showed in the graph.", + envKey: .graphTargets + ) var targets: [String] = [] @Option( name: .shortAndLong, help: "The path to the directory that contains the project whose targets will be cached.", - completion: .directory + completion: .directory, + envKey: .graphPath ) var path: String? @Option( name: .shortAndLong, - help: "The path where the graph will be generated." + help: "The path where the graph will be generated.", + envKey: .graphOutputPath ) var outputPath: String? @@ -87,7 +99,7 @@ public struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { layoutAlgorithm: layoutAlgorithm, skipTestTargets: skipTestTargets, skipExternalDependencies: skipExternalDependencies, - open: !noOpen, + open: open, platformToFilter: platform, targetsToFilter: targets, path: path.map { try AbsolutePath(validating: $0) } ?? FileHandler.shared.currentPath, @@ -98,10 +110,20 @@ public struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { } } -enum GraphFormat: String, ExpressibleByArgument { +enum GraphFormat: String, ExpressibleByArgument, CaseIterable { case dot, json, png, svg } -extension GraphViz.LayoutAlgorithm: ExpressibleByArgument {} - -extension TuistGraph.Platform: ExpressibleByArgument {} +extension GraphViz.LayoutAlgorithm: ExpressibleByArgument { + public static var allValueStrings: [String] { + [ + LayoutAlgorithm.dot.rawValue, + LayoutAlgorithm.neato.rawValue, + LayoutAlgorithm.twopi.rawValue, + LayoutAlgorithm.circo.rawValue, + LayoutAlgorithm.fdp.rawValue, + LayoutAlgorithm.sfdp.rawValue, + LayoutAlgorithm.patchwork.rawValue, + ] + } +} diff --git a/Sources/TuistKit/Commands/InitCommand.swift b/Sources/TuistKit/Commands/InitCommand.swift index dfe9b4cd607..d4d25485131 100644 --- a/Sources/TuistKit/Commands/InitCommand.swift +++ b/Sources/TuistKit/Commands/InitCommand.swift @@ -1,18 +1,18 @@ import AnyCodable import ArgumentParser import Foundation -import TSCBasic +import Path import TuistCore import TuistGenerator -import TuistGraph import TuistLoader import TuistScaffold import TuistSupport +import XcodeGraph -private typealias Platform = TuistGraph.Platform -private typealias Product = TuistGraph.Product +private typealias Platform = XcodeGraph.Platform +private typealias Product = XcodeGraph.Product -public struct InitCommand: ParsableCommand, HasTrackableParameters { +public struct InitCommand: AsyncParsableCommand, HasTrackableParameters { public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "init", @@ -21,29 +21,34 @@ public struct InitCommand: ParsableCommand, HasTrackableParameters { } public static var analyticsDelegate: TrackableParametersDelegate? + public var runId = UUID().uuidString @Option( - help: "The platform (ios, tvos, visionos, watchos or macos) the product will be for (Default: ios)", - completion: .list(["ios", "tvos", "macos", "visionos", "watchos"]) + help: "The platform (iOS, tvOS, visionOS, watchOS or macOS) the product will be for (Default: iOS)", + completion: .list(["iOS", "tvOS", "macOS", "visionOS", "watchOS"]), + envKey: .initPlatform ) var platform: String? @Option( name: .shortAndLong, - help: "The path to the folder where the project will be generated (Default: Current directory)", - completion: .directory + help: "The path to the folder where the project will be generated. (Default: Current directory)", + completion: .directory, + envKey: .initPath ) var path: String? @Option( name: .shortAndLong, - help: "The name of the project. If it's not passed (Default: Name of the directory)" + help: "The name of the project. (Default: Name of the current directory)", + envKey: .initName ) var name: String? @Option( name: .shortAndLong, - help: "The name of the template to use (you can list available templates with tuist scaffold list)" + help: "The name of the template to use (you can list available templates with tuist scaffold list)", + envKey: .initTemplate ) var template: String? @@ -73,13 +78,13 @@ public struct InitCommand: ParsableCommand, HasTrackableParameters { } } - public func run() throws { + public func run() async throws { InitCommand.analyticsDelegate?.addParameters( [ "platform": AnyCodable(platform ?? "unknown"), ] ) - try InitService().run( + try await InitService().run( name: name, platform: platform, path: path, @@ -97,7 +102,7 @@ extension InitCommand { 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) throws { + static func preprocess(_ arguments: [String]? = nil) async throws { guard let arguments, arguments.contains("--template") || arguments.contains("-t") @@ -121,7 +126,7 @@ extension InitCommand { templateName != "default" else { return } - let (required, optional) = try InitService().loadTemplateOptions( + let (required, optional) = try await InitService().loadTemplateOptions( name: name, templateName: templateName, path: command.path diff --git a/Sources/TuistKit/Commands/InstallCommand.swift b/Sources/TuistKit/Commands/InstallCommand.swift new file mode 100644 index 00000000000..c921d8129f7 --- /dev/null +++ b/Sources/TuistKit/Commands/InstallCommand.swift @@ -0,0 +1,36 @@ +import ArgumentParser +import Foundation +import Path + +/// A command to install the remote content the project depends on. +public struct InstallCommand: AsyncParsableCommand { + public init() {} + public static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "install", + abstract: "Installs any remote content (e.g. dependencies) necessary to interact with the project." + ) + } + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .installPath + ) + var path: String? + + @Flag( + name: .shortAndLong, + help: "Instead of simple install, update external content when available.", + envKey: .installUpdate + ) + var update: Bool = false + + public func run() async throws { + try await InstallService().run( + path: path, + update: update + ) + } +} diff --git a/Sources/TuistKit/Commands/ListCommand.swift b/Sources/TuistKit/Commands/ListCommand.swift index 4f59861ca74..e560f27f0ce 100644 --- a/Sources/TuistKit/Commands/ListCommand.swift +++ b/Sources/TuistKit/Commands/ListCommand.swift @@ -14,14 +14,16 @@ public struct ListCommand: AsyncParsableCommand { } @Flag( - help: "The output in JSON format" + help: "The output in JSON format", + envKey: .scaffoldListJson ) var json: Bool = false @Option( name: .shortAndLong, help: "The path where you want to list templates from", - completion: .directory + completion: .directory, + envKey: .scaffoldListPath ) var path: String? diff --git a/Sources/TuistKit/Commands/LogoutCommand.swift b/Sources/TuistKit/Commands/LogoutCommand.swift new file mode 100644 index 00000000000..541ca5328da --- /dev/null +++ b/Sources/TuistKit/Commands/LogoutCommand.swift @@ -0,0 +1,26 @@ +import ArgumentParser +import Foundation +import Path + +struct LogoutCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "logout", + abstract: "Removes an existing Tuist session." + ) + } + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .logoutPath + ) + var path: String? + + func run() async throws { + try await LogoutService().logout( + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Migration/MigrationCheckEmptyBuildSettingsCommand.swift b/Sources/TuistKit/Commands/Migration/MigrationCheckEmptyBuildSettingsCommand.swift index 5528451346e..e520ffbfecb 100644 --- a/Sources/TuistKit/Commands/Migration/MigrationCheckEmptyBuildSettingsCommand.swift +++ b/Sources/TuistKit/Commands/Migration/MigrationCheckEmptyBuildSettingsCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TuistSupport struct MigrationCheckEmptyBuildSettingsCommand: ParsableCommand { @@ -15,13 +15,15 @@ struct MigrationCheckEmptyBuildSettingsCommand: ParsableCommand { @Option( name: [.customShort("p"), .long], help: "The path to the Xcode project", - completion: .directory + completion: .directory, + envKey: .migrationCheckEmptySettingsXcodeprojPath ) var xcodeprojPath: String @Option( name: .shortAndLong, - help: "The name of the target whose build settings will be checked. When not passed, it checks the build settings of the project." + help: "The name of the target whose build settings will be checked. When not passed, it checks the build settings of the project.", + envKey: .migrationCheckEmptySettingsTarget ) var target: String? diff --git a/Sources/TuistKit/Commands/Migration/MigrationSettingsToXCConfigCommand.swift b/Sources/TuistKit/Commands/Migration/MigrationSettingsToXCConfigCommand.swift index 8fd1e20ccd6..f8ccb3e1d8f 100644 --- a/Sources/TuistKit/Commands/Migration/MigrationSettingsToXCConfigCommand.swift +++ b/Sources/TuistKit/Commands/Migration/MigrationSettingsToXCConfigCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TuistSupport struct MigrationSettingsToXCConfigCommand: ParsableCommand { @@ -15,21 +15,24 @@ struct MigrationSettingsToXCConfigCommand: ParsableCommand { @Option( name: [.customShort("p"), .long], help: "The path to the Xcode project", - completion: .directory + completion: .directory, + envKey: .migrationSettingsToXcconfigXcodeprojPath ) var xcodeprojPath: String @Option( name: [.customShort("x"), .long], help: "The path to the .xcconfig file where build settings will be extracted.", - completion: .directory + completion: .directory, + envKey: .migrationSettingsToXcconfigXcconfigPath ) var xcconfigPath: String @Option( name: .shortAndLong, help: "The name of the target whose build settings will be extracted. When not passed, it extracts the build settings of the project.", - completion: .default + completion: .default, + envKey: .migrationSettingsToXcconfigTarget ) var target: String? diff --git a/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift b/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift index 243d9cef8c0..b2e966857d1 100644 --- a/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift +++ b/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TuistSupport public struct MigrationTargetsByDependenciesCommand: ParsableCommand { @@ -17,7 +17,8 @@ public struct MigrationTargetsByDependenciesCommand: ParsableCommand { @Option( name: [.customShort("p"), .long], help: "The path to the Xcode project", - completion: .directory + completion: .directory, + envKey: .migrationListTargetsXcodeprojPath ) var xcodeprojPath: String diff --git a/Sources/TuistKit/Commands/MigrationCommand.swift b/Sources/TuistKit/Commands/MigrationCommand.swift index b450e003e72..59adc3fc69f 100644 --- a/Sources/TuistKit/Commands/MigrationCommand.swift +++ b/Sources/TuistKit/Commands/MigrationCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path struct MigrationCommand: ParsableCommand { static var configuration: CommandConfiguration { diff --git a/Sources/TuistKit/Commands/Organization/OrganizationBillingCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationBillingCommand.swift new file mode 100644 index 00000000000..a3ca6391bcb --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationBillingCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationBillingCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "billing", + _superCommandName: "organization", + abstract: "Open billing dashboard for the specified organization." + ) + } + + @Argument( + help: "The name of the organization to show billing dashboard for.", + envKey: .organizationBillingOrganizationName + ) + var organizationName: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationBillingPath + ) + var path: String? + + func run() async throws { + try await OrganizationBillingService().run( + organizationName: organizationName, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationCreateCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationCreateCommand.swift new file mode 100644 index 00000000000..7fbf6dc1971 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationCreateCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationCreateCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "create", + _superCommandName: "organization", + abstract: "Create a new organization." + ) + } + + @Argument( + help: "The name of the organization to create.", + envKey: .organizationCreateOrganizationName + ) + var organizationName: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationCreatePath + ) + var path: String? + + func run() async throws { + try await OrganizationCreateService().run( + organizationName: organizationName, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationDeleteCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationDeleteCommand.swift new file mode 100644 index 00000000000..5dfdde20253 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationDeleteCommand.swift @@ -0,0 +1,36 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationDeleteCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "delete", + _superCommandName: "organization", + abstract: "Delete a new organization." + ) + } + + @Argument( + help: "The name of the organization to delete.", + completion: .directory, + envKey: .organizationDeleteOrganizationName + ) + var organizationName: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationDeletePath + ) + var path: String? + + func run() async throws { + try await OrganizationDeleteService().run( + organizationName: organizationName, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationInviteCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationInviteCommand.swift new file mode 100644 index 00000000000..1503a1ec440 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationInviteCommand.swift @@ -0,0 +1,42 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationInviteCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "invite", + _superCommandName: "organization", + abstract: "Invite a new member to your organization." + ) + } + + @Argument( + help: "The name of the organization to invite the user to.", + envKey: .organizationInviteOrganizationName + ) + var organizationName: String + + @Argument( + help: "The email of the user to invite.", + envKey: .organizationInviteEmail + ) + var email: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationInvitePath + ) + var path: String? + + func run() async throws { + try await OrganizationInviteService().run( + organizationName: organizationName, + email: email, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationListCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationListCommand.swift new file mode 100644 index 00000000000..dc8aebda770 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationListCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationListCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "list", + _superCommandName: "organization", + abstract: "List your organizations." + ) + } + + @Flag( + help: "The output in JSON format.", + envKey: .organizationListJson + ) + var json: Bool = false + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationListPath + ) + var path: String? + + func run() async throws { + try await OrganizationListService().run( + json: json, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationRemoveCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationRemoveCommand.swift new file mode 100644 index 00000000000..a346595eb2c --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationRemoveCommand.swift @@ -0,0 +1,18 @@ +import ArgumentParser +import Foundation +import Path + +struct OrganizationRemoveCommand: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "remove", + _superCommandName: "organization", + abstract: "A set of commands to remove members or cancel pending invitations.", + subcommands: [ + OrganizationRemoveInviteCommand.self, + OrganizationRemoveMemberCommand.self, + OrganizationRemoveSSOCommand.self, + ] + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationRemoveInviteCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationRemoveInviteCommand.swift new file mode 100644 index 00000000000..d8ccb7cae59 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationRemoveInviteCommand.swift @@ -0,0 +1,42 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationRemoveInviteCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "invite", + _superCommandName: "remove", + abstract: "Cancel pending invitation." + ) + } + + @Argument( + help: "The name of the organization to cancel the invitation for.", + envKey: .organizationRemoveInviteOrganizationName + ) + var organizationName: String + + @Argument( + help: "The email of the user to cancel the invitation for.", + envKey: .organizationRemoveInviteEmail + ) + var email: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationRemoveInvitePath + ) + var path: String? + + func run() async throws { + try await OrganizationRemoveInviteService().run( + organizationName: organizationName, + email: email, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationRemoveMemberCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationRemoveMemberCommand.swift new file mode 100644 index 00000000000..23b7ea35744 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationRemoveMemberCommand.swift @@ -0,0 +1,42 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationRemoveMemberCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "member", + _superCommandName: "remove", + abstract: "Remove a member from your organization." + ) + } + + @Argument( + help: "The name of the organization to remove the organization member from.", + envKey: .organizationRemoveMemberOrganizationName + ) + var organizationName: String + + @Argument( + help: "The username of the member you want to remove from the organization.", + envKey: .organizationRemoveMemberUsername + ) + var username: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationRemoveMemberPath + ) + var path: String? + + func run() async throws { + try await OrganizationRemoveMemberService().run( + organizationName: organizationName, + username: username, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationRemoveSSOCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationRemoveSSOCommand.swift new file mode 100644 index 00000000000..08a20a4be83 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationRemoveSSOCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationRemoveSSOCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "sso", + _superCommandName: "remove", + abstract: "Remove the SSO provider for your organization." + ) + } + + @Argument( + help: "The name of the organization for which you want to update the SSO provider for.", + envKey: .organizationRemoveSSOOrganizationName + ) + var organizationName: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationRemoveSSOPath + ) + var path: String? + + func run() async throws { + try await OrganizationRemoveSSOService().run( + organizationName: organizationName, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationShowCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationShowCommand.swift new file mode 100644 index 00000000000..c1570748778 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationShowCommand.swift @@ -0,0 +1,42 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationShowCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "show", + _superCommandName: "organization", + abstract: "Show information about the specified organization." + ) + } + + @Argument( + help: "The name of the organization to show.", + envKey: .organizationShowOrganizationName + ) + var organizationName: String + + @Flag( + help: "The output in JSON format.", + envKey: .organizationShowJson + ) + var json: Bool = false + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationShowPath + ) + var path: String? + + func run() async throws { + try await OrganizationShowService().run( + organizationName: organizationName, + json: json, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationUpdateCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationUpdateCommand.swift new file mode 100644 index 00000000000..8e59b032ec4 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationUpdateCommand.swift @@ -0,0 +1,17 @@ +import ArgumentParser +import Foundation +import Path + +struct OrganizationUpdateCommand: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "update", + _superCommandName: "organization", + abstract: "A set of commands to update the organization.", + subcommands: [ + OrganizationUpdateMemberCommand.self, + OrganizationUpdateSSOCommand.self, + ] + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationUpdateMemberCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationUpdateMemberCommand.swift new file mode 100644 index 00000000000..fac57670cd3 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationUpdateMemberCommand.swift @@ -0,0 +1,50 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct OrganizationUpdateMemberCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "member", + _superCommandName: "update", + abstract: "Update a member from your organization." + ) + } + + @Argument( + help: "The name of the organization for which you want to update the member for.", + envKey: .organizationUpdateMemberOrganizationName + ) + var organizationName: String + + @Argument( + help: "The username of the member you want to update.", + envKey: .organizationUpdateMemberUsername + ) + var username: String + + @Option( + help: "The new member role", + completion: .list(["admin", "user"]), + envKey: .organizationUpdateMemberRole + ) + var role: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationUpdateMemberPath + ) + var path: String? + + func run() async throws { + try await OrganizationUpdateMemberService().run( + organizationName: organizationName, + username: username, + role: role, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Organization/OrganizationUpdateSSOCommand.swift b/Sources/TuistKit/Commands/Organization/OrganizationUpdateSSOCommand.swift new file mode 100644 index 00000000000..69ed284a1f1 --- /dev/null +++ b/Sources/TuistKit/Commands/Organization/OrganizationUpdateSSOCommand.swift @@ -0,0 +1,54 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +enum SSOProvider: String, ExpressibleByArgument, CaseIterable { + case google +} + +struct OrganizationUpdateSSOCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "sso", + _superCommandName: "update", + abstract: "Update the SSO provider for your organization." + ) + } + + @Argument( + help: "The name of the organization for which you want to update the SSO provider for.", + envKey: .organizationUpdateSSOOrganizationName + ) + var organizationName: String + + @Option( + help: "The SSO provider to use.", + envKey: .organizationUpdateSSOProvider + ) + var provider: SSOProvider + + @Option( + name: .shortAndLong, + help: "Organization ID for your SSO provider. For Google, this is your Google domain (for example, if your email is tuist@tuist.io, the domain would be tuist.io)", + envKey: .organizationUpdateSSOOrganizationId + ) + var organizationId: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .organizationUpdateSSOPath + ) + var path: String? + + func run() async throws { + try await OrganizationUpdateSSOService().run( + organizationName: organizationName, + provider: provider, + organizationId: organizationId, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/OrganizationCommand.swift b/Sources/TuistKit/Commands/OrganizationCommand.swift new file mode 100644 index 00000000000..a446ec92d5c --- /dev/null +++ b/Sources/TuistKit/Commands/OrganizationCommand.swift @@ -0,0 +1,22 @@ +import ArgumentParser +import Foundation +import Path + +struct OrganizationCommand: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "organization", + abstract: "A set of commands to manage your Tuist organizations.", + subcommands: [ + OrganizationCreateCommand.self, + OrganizationListCommand.self, + OrganizationDeleteCommand.self, + OrganizationShowCommand.self, + OrganizationInviteCommand.self, + OrganizationRemoveCommand.self, + OrganizationUpdateCommand.self, + OrganizationBillingCommand.self, + ] + ) + } +} diff --git a/Sources/TuistKit/Commands/Plugin/PluginArchiveCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginArchiveCommand.swift index dd111705a97..8614ea91aca 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginArchiveCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginArchiveCommand.swift @@ -1,8 +1,8 @@ import ArgumentParser import Foundation -import TSCBasic +import Path -struct PluginArchiveCommannd: ParsableCommand { +struct PluginArchiveCommand: AsyncParsableCommand { static var configuration: CommandConfiguration { CommandConfiguration( commandName: "archive", @@ -13,12 +13,13 @@ struct PluginArchiveCommannd: ParsableCommand { @Option( name: .shortAndLong, help: "The path to the directory that contains the definition of the plugin.", - completion: .directory + completion: .directory, + envKey: .pluginArchivePath ) var path: String? - func run() throws { - try PluginArchiveService().run( + func run() async throws { + try await PluginArchiveService().run( path: path ) } diff --git a/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift index 108d8aa80f5..91096b8a33f 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path public struct PluginBuildCommand: ParsableCommand { public init() {} @@ -16,22 +16,26 @@ public struct PluginBuildCommand: ParsableCommand { var pluginOptions: PluginCommand.PluginOptions @Flag( - help: "Build both source and test targets." + help: "Build both source and test targets.", + envKey: .pluginBuildBuildTests ) var buildTests = false @Flag( - help: "Print the binary output path." + help: "Print the binary output path.", + envKey: .pluginBuildShowBinPath ) var showBinPath = false @Option( - help: "Build the specified targets." + help: "Build the specified targets.", + envKey: .pluginBuildTargets ) var targets: [String] = [] @Option( - help: "Build the specified products." + help: "Build the specified products.", + envKey: .pluginBuildProducts ) var products: [String] = [] diff --git a/Sources/TuistKit/Commands/Plugin/PluginCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginCommand.swift index 05075aa87d4..0790e590977 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path struct PluginCommand: ParsableCommand { static var configuration: CommandConfiguration { @@ -8,7 +8,7 @@ struct PluginCommand: ParsableCommand { commandName: "plugin", abstract: "A set of commands for plugin's management.", subcommands: [ - PluginArchiveCommannd.self, + PluginArchiveCommand.self, PluginBuildCommand.self, PluginRunCommand.self, PluginTestCommand.self, @@ -16,21 +16,23 @@ struct PluginCommand: ParsableCommand { ) } - enum PackageConfiguration: String, ExpressibleByArgument, RawRepresentable { + enum PackageConfiguration: String, ExpressibleByArgument, RawRepresentable, EnumerableFlag { case debug, release } struct PluginOptions: ParsableArguments { @Option( name: .shortAndLong, - help: "Choose configuration (default: debug)." + help: "Choose configuration (default: debug).", + envKey: .pluginOptionsConfiguration ) var configuration: PackageConfiguration = .debug @Option( name: .shortAndLong, help: "The path to the directory that contains the definition of the plugin.", - completion: .directory + completion: .directory, + envKey: .pluginOptionsPath ) var path: String? } diff --git a/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift index 34c27eebc4a..9bb6454036d 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path public struct PluginRunCommand: ParsableCommand { public init() {} @@ -16,22 +16,26 @@ public struct PluginRunCommand: ParsableCommand { var pluginOptions: PluginCommand.PluginOptions @Flag( - help: "Build both source and test targets." + help: "Build both source and test targets.", + envKey: .pluginRunBuildTests ) - var buildTests = false + var buildTests: Bool = false @Flag( - help: "Skip building the plugin." + help: "Skip building the plugin.", + envKey: .pluginRunSkipBuild ) - var skipBuild = false + var skipBuild: Bool = false @Argument( - help: "The plugin task to run." + help: "The plugin task to run.", + envKey: .pluginRunTask ) var task: String @Argument( - help: "The arguments to pass to the plugin task." + help: "The arguments to pass to the plugin task.", + envKey: .pluginRunArguments ) var arguments: [String] = [] diff --git a/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift index 76da4a1cdf5..96420c8024d 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path public struct PluginTestCommand: ParsableCommand { public init() {} @@ -8,6 +8,7 @@ public struct PluginTestCommand: ParsableCommand { public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "test", + abstract: "Tests a plugin." ) } @@ -16,12 +17,14 @@ public struct PluginTestCommand: ParsableCommand { var pluginOptions: PluginCommand.PluginOptions @Flag( - help: "Build both source and test targets." + help: "Build both source and test targets.", + envKey: .pluginTestBuildTests ) var buildTests = false @Option( - help: "Test the specified products." + help: "Test the specified products.", + envKey: .pluginTestTestProducts ) var testProducts: [String] = [] diff --git a/Sources/TuistKit/Commands/Project/ProjectCreateCommand.swift b/Sources/TuistKit/Commands/Project/ProjectCreateCommand.swift new file mode 100644 index 00000000000..eda00c3cef0 --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectCreateCommand.swift @@ -0,0 +1,36 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectCreateCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "create", + _superCommandName: "project", + abstract: "Create a new project." + ) + } + + @Argument( + help: "The project to create. The full handle must be in the format of account-handle/project-handle.", + completion: .directory, + envKey: .projectCreateFullHandle + ) + var fullHandle: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .projectCreatePath + ) + var path: String? + + func run() async throws { + try await ProjectCreateService().run( + fullHandle: fullHandle, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectDeleteCommand.swift b/Sources/TuistKit/Commands/Project/ProjectDeleteCommand.swift new file mode 100644 index 00000000000..77a1980ec8e --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectDeleteCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import TuistSupport + +struct ProjectDeleteCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "delete", + _superCommandName: "project", + abstract: "Delete a Tuist project." + ) + } + + @Argument( + help: "The project to delete. Must be in the format of account-handle/project-handle.", + completion: .directory, + envKey: .projectDeleteFullHandle + ) + var fullHandle: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .projectDeletePath + ) + var path: String? + + func run() async throws { + try await ProjectDeleteService().run( + fullHandle: fullHandle, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectListCommand.swift b/Sources/TuistKit/Commands/Project/ProjectListCommand.swift new file mode 100644 index 00000000000..e41c788a606 --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectListCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectListCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "list", + _superCommandName: "project", + abstract: "List projects you have access to." + ) + } + + @Flag( + help: "The output in JSON format.", + envKey: .projectListJson + ) + var json: Bool = false + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .projectListPath + ) + var path: String? + + func run() async throws { + try await ProjectListService().run( + json: json, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectShowCommand.swift b/Sources/TuistKit/Commands/Project/ProjectShowCommand.swift new file mode 100644 index 00000000000..486bf58338b --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectShowCommand.swift @@ -0,0 +1,43 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectShowCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "show", + _superCommandName: "project", + abstract: "Show information about the specified project. Use --web flag to open the project in the browser." + ) + } + + @Argument( + help: "The project to show. The full handle must be in the format of account-handle/project-handle.", + completion: .directory, + envKey: .projectShowFullHandle + ) + var fullHandle: String? + + @Flag( + help: "Open a project in the browser.", + envKey: .projectShowWeb + ) + var web: Bool = false + + @Option( + name: .shortAndLong, + help: "The path to the Tuist project.", + completion: .directory, + envKey: .projectShowPath + ) + var path: String? + + func run() async throws { + try await ProjectShowService().run( + fullHandle: fullHandle, + web: web, + path: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectTokenCommand.swift b/Sources/TuistKit/Commands/Project/ProjectTokenCommand.swift new file mode 100644 index 00000000000..29197a12cbe --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectTokenCommand.swift @@ -0,0 +1,19 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectTokensCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "tokens", + _superCommandName: "project", + abstract: "Manage Tuist project tokens.", + subcommands: [ + ProjectTokensCreateCommand.self, + ProjectTokensListCommand.self, + ProjectTokensRevokeCommand.self, + ] + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectTokensCreateCommand.swift b/Sources/TuistKit/Commands/Project/ProjectTokensCreateCommand.swift new file mode 100644 index 00000000000..531b6934ac3 --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectTokensCreateCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectTokensCreateCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "create", + _superCommandName: "tokens", + abstract: "Create a new Tuist project token. You can save this token in the `TUIST_CONFIG_TOKEN` environment variable to authenticate requests against the Tuist API." + ) + } + + @Argument( + help: "The project to create the token for. Must be in the format of account-handle/project-handle.", + envKey: .projectTokenFullHandle + ) + var fullHandle: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .projectTokenPath + ) + var path: String? + + func run() async throws { + try await ProjectTokensCreateService().run( + fullHandle: fullHandle, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectTokensListCommand.swift b/Sources/TuistKit/Commands/Project/ProjectTokensListCommand.swift new file mode 100644 index 00000000000..0a1f09bb8c5 --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectTokensListCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectTokensListCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "list", + _superCommandName: "tokens", + abstract: "List Tuist project tokens." + ) + } + + @Argument( + help: "The project to list the tokens for. Must be in the format of account-handle/project-handle.", + envKey: .projectTokenFullHandle + ) + var fullHandle: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .projectTokenPath + ) + var path: String? + + func run() async throws { + try await ProjectTokensListService().run( + fullHandle: fullHandle, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectTokensRevokeCommand.swift b/Sources/TuistKit/Commands/Project/ProjectTokensRevokeCommand.swift new file mode 100644 index 00000000000..882a5ddefeb --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectTokensRevokeCommand.swift @@ -0,0 +1,42 @@ +import ArgumentParser +import Foundation +import Path +import TuistSupport + +struct ProjectTokensRevokeCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "revoke", + _superCommandName: "tokens", + abstract: "Revoke Tuist project tokens." + ) + } + + @Argument( + help: "The ID of the project token to revoke.", + envKey: .projectTokenId + ) + var projectTokenId: String + + @Argument( + help: "The project to revoke the token for. Must be in the format of account-handle/project-handle.", + envKey: .projectTokenFullHandle + ) + var fullHandle: String + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .projectTokenPath + ) + var path: String? + + func run() async throws { + try await ProjectTokensRevokeService().run( + projectTokenId: projectTokenId, + fullHandle: fullHandle, + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/Project/ProjectUpdateCommand.swift b/Sources/TuistKit/Commands/Project/ProjectUpdateCommand.swift new file mode 100644 index 00000000000..73f7f95e022 --- /dev/null +++ b/Sources/TuistKit/Commands/Project/ProjectUpdateCommand.swift @@ -0,0 +1,39 @@ +import ArgumentParser +import Foundation +import Path + +struct ProjectUpdateCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "update", + _superCommandName: "project", + abstract: "Update project settings" + ) + } + + @Argument( + help: "The full handle of the project to update. Must be in the format of account-handle/project-handle." + ) + var fullHandle: String? + + @Option( + help: "Set the default branch name for the repository linked to the project." + ) + var defaultBranch: String? + + @Option( + name: .shortAndLong, + help: "The path to the Tuist project.", + completion: .directory + ) + var path: String? + + func run() async throws { + try await ProjectUpdateService() + .run( + fullHandle: fullHandle, + defaultBranch: defaultBranch, + path: path + ) + } +} diff --git a/Sources/TuistKit/Commands/ProjectCommand.swift b/Sources/TuistKit/Commands/ProjectCommand.swift new file mode 100644 index 00000000000..b4bccd50f86 --- /dev/null +++ b/Sources/TuistKit/Commands/ProjectCommand.swift @@ -0,0 +1,20 @@ +import ArgumentParser +import Foundation +import Path + +struct ProjectCommand: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "project", + abstract: "A set of commands to manage your Tuist projects.", + subcommands: [ + ProjectShowCommand.self, + ProjectCreateCommand.self, + ProjectListCommand.self, + ProjectDeleteCommand.self, + ProjectTokensCommand.self, + ProjectUpdateCommand.self, + ] + ) + } +} diff --git a/Sources/TuistKit/Commands/RunCommand.swift b/Sources/TuistKit/Commands/RunCommand.swift index 44c72e4ecd4..14014ce6d9b 100644 --- a/Sources/TuistKit/Commands/RunCommand.swift +++ b/Sources/TuistKit/Commands/RunCommand.swift @@ -1,9 +1,24 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TSCUtility import TuistSupport +enum Runnable: ExpressibleByArgument, Equatable { + init?(argument: String) { + if argument.starts(with: "http://") || argument.starts(with: "https://"), + let previewLink = URL(string: argument) + { + self = .url(previewLink) + } else { + self = .scheme(argument) + } + } + + case url(Foundation.URL) + case scheme(String) +} + public struct RunCommand: AsyncParsableCommand { public init() {} @@ -24,10 +39,22 @@ public struct RunCommand: AsyncParsableCommand { ) } - @Flag(help: "Force the generation of the project before running.") + @Argument( + help: "Runnable project scheme or a preview URL.", + envKey: .runScheme + ) + var runnable: Runnable + + @Flag( + help: "Force the generation of the project before running.", + envKey: .runGenerate + ) var generate: Bool = false - @Flag(help: "When passed, it cleans the project before running.") + @Flag( + help: "When passed, it cleans the project before running.", + envKey: .runClean + ) var clean: Bool = false @Option( @@ -48,7 +75,8 @@ public struct RunCommand: AsyncParsableCommand { @Option( name: .shortAndLong, - help: "The OS version of the simulator." + help: "The OS version of the simulator.", + envKey: .runOS ) var os: String? @@ -58,24 +86,22 @@ public struct RunCommand: AsyncParsableCommand { ) var rosetta: Bool = false - @Argument(help: "The scheme to be run.") - var scheme: String - @Argument( parsing: .captureForPassthrough, - help: "The arguments to pass to the runnable target during execution." + help: "The arguments to pass to the runnable target during execution.", + envKey: .runArguments ) var arguments: [String] = [] public func run() async throws { try await RunService().run( path: path, - schemeName: scheme, + runnable: runnable, generate: generate, clean: clean, configuration: configuration, device: device, - version: os, + osVersion: os, rosetta: rosetta, arguments: arguments ) diff --git a/Sources/TuistKit/Commands/ScaffoldCommand.swift b/Sources/TuistKit/Commands/ScaffoldCommand.swift index be00c8f8ca9..abb96d71433 100644 --- a/Sources/TuistKit/Commands/ScaffoldCommand.swift +++ b/Sources/TuistKit/Commands/ScaffoldCommand.swift @@ -1,11 +1,11 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader import TuistPlugin import TuistSupport +import XcodeGraph enum ScaffoldCommandError: FatalError, Equatable { var type: ErrorType { @@ -35,14 +35,16 @@ public struct ScaffoldCommand: AsyncParsableCommand { } @Flag( - help: "The output in JSON format" + help: "The output in JSON format", + envKey: .scaffoldJson ) var json: Bool = false @Option( name: .shortAndLong, help: "The path to the folder where the template will be generated (Default: Current directory)", - completion: .directory + completion: .directory, + envKey: .scaffoldPath ) var path: String? @@ -53,7 +55,8 @@ public struct ScaffoldCommand: AsyncParsableCommand { var url: String? @Argument( - help: "Name of template you want to use" + help: "Name of template you want to use", + envKey: .scaffoldTemplate ) var template: String diff --git a/Sources/TuistKit/Commands/SessionCommand.swift b/Sources/TuistKit/Commands/SessionCommand.swift new file mode 100644 index 00000000000..e69f5a08a32 --- /dev/null +++ b/Sources/TuistKit/Commands/SessionCommand.swift @@ -0,0 +1,26 @@ +import ArgumentParser +import Foundation +import Path + +struct SessionCommand: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "session", + abstract: "Prints the current Tuist session" + ) + } + + @Option( + name: .shortAndLong, + help: "The path to the directory or a subdirectory of the project.", + completion: .directory, + envKey: .sessionPath + ) + var path: String? + + func run() async throws { + try await SessionService().printSession( + directory: path + ) + } +} diff --git a/Sources/TuistKit/Commands/ShareCommand.swift b/Sources/TuistKit/Commands/ShareCommand.swift new file mode 100644 index 00000000000..4a5263b157b --- /dev/null +++ b/Sources/TuistKit/Commands/ShareCommand.swift @@ -0,0 +1,58 @@ +import ArgumentParser +import Foundation +import XcodeGraph + +public struct ShareCommand: AsyncParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "share", + abstract: "Generate a link to share your app. Only simulator builds supported." + ) + } + + @Option( + name: .shortAndLong, + help: "The path to the directory that contains a Tuist or Xcode project with a buildable scheme that can output runnable artifacts.", + completion: .directory + ) + var path: String? + + @Argument( + help: "The app names to be looked up in the built products directory or the paths to the app bundles.", + envKey: .shareApp + ) + var apps: [String] = [] + + @Option( + name: [.long, .customShort("C")], + help: "The configuration of the app to share. Ignored when the app paths are passed directly.", + envKey: .shareConfiguration + ) + var configuration: String? + + @Option( + help: "The platforms (iOS, tvOS, visionOS, watchOS or macOS) to share the app for. Ignored when the app paths are passed directly.", + completion: .list(["iOS", "tvOS", "macOS", "visionOS", "watchOS"]), + envKey: .sharePlatform + ) + var platforms: [XcodeGraph.Platform] = [] + + @Option( + help: "The derived data path to find the apps in. When absent, the system-configured one.", + completion: .directory, + envKey: .shareDerivedDataPath + ) + var derivedDataPath: String? + + public func run() async throws { + try await ShareService().run( + path: path, + apps: apps, + configuration: configuration, + platforms: platforms, + derivedDataPath: derivedDataPath + ) + } +} diff --git a/Sources/TuistKit/Commands/Signing/DecryptCommand.swift b/Sources/TuistKit/Commands/Signing/DecryptCommand.swift deleted file mode 100644 index 1b48126f7c6..00000000000 --- a/Sources/TuistKit/Commands/Signing/DecryptCommand.swift +++ /dev/null @@ -1,27 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic -import TuistCore -import TuistSigning -import TuistSupport - -struct DecryptCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "decrypt", - _superCommandName: "signing", - abstract: "Decrypts all files in Tuist/Signing directory" - ) - } - - @Option( - name: .shortAndLong, - help: "The path to the folder containing the encrypted certificates", - completion: .directory - ) - var path: String? - - func run() throws { - try DecryptService().run(path: path) - } -} diff --git a/Sources/TuistKit/Commands/Signing/EncryptCommand.swift b/Sources/TuistKit/Commands/Signing/EncryptCommand.swift deleted file mode 100644 index 703445c6890..00000000000 --- a/Sources/TuistKit/Commands/Signing/EncryptCommand.swift +++ /dev/null @@ -1,24 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic - -struct EncryptCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "encrypt", - _superCommandName: "signing", - abstract: "Encrypts all files in Tuist/Signing directory" - ) - } - - @Option( - name: .shortAndLong, - help: "The path to the folder containing the certificates you would like to encrypt", - completion: .directory - ) - var path: String? - - func run() throws { - try EncryptService().run(path: path) - } -} diff --git a/Sources/TuistKit/Commands/SigningCommand.swift b/Sources/TuistKit/Commands/SigningCommand.swift deleted file mode 100644 index 305fbae89a4..00000000000 --- a/Sources/TuistKit/Commands/SigningCommand.swift +++ /dev/null @@ -1,16 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic - -struct SigningCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "signing", - abstract: "A set of commands for signing-related operations", - subcommands: [ - EncryptCommand.self, - DecryptCommand.self, - ] - ) - } -} diff --git a/Sources/TuistKit/Commands/TestCommand.swift b/Sources/TuistKit/Commands/TestCommand.swift index eb8a0f1af17..552986cf777 100644 --- a/Sources/TuistKit/Commands/TestCommand.swift +++ b/Sources/TuistKit/Commands/TestCommand.swift @@ -1,15 +1,20 @@ import AnyCodable import ArgumentParser import Foundation -import TSCBasic +import Path import TuistCore +import TuistServer import TuistSupport +import XcodeGraph /// Command that tests a target from the project in the current directory. public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { public init() {} public static var analyticsDelegate: TrackableParametersDelegate? + public static var generatorFactory: GeneratorFactorying = GeneratorFactory() + public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory() + public var runId = UUID().uuidString public static var configuration: CommandConfiguration { CommandConfiguration( @@ -19,78 +24,94 @@ public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { } @Argument( - help: "The scheme to be tested. By default it tests all the testable targets of the project in the current directory." + help: "The scheme to be tested. By default it tests all the testable targets of the project in the current directory.", + envKey: .testScheme ) var scheme: String? @Flag( name: .shortAndLong, - help: "When passed, it cleans the project before testing it." + help: "When passed, it cleans the project before testing it.", + envKey: .testClean ) var clean: Bool = false @Option( name: .shortAndLong, - help: "The path to the directory that contains the project to be tested." + help: "The path to the directory that contains the project to be tested.", + completion: .directory, + envKey: .testPath ) var path: String? @Option( name: .shortAndLong, - help: "Test on a specific device." + help: "Test on a specific device.", + envKey: .testDevice ) var device: String? @Option( name: .long, - help: "Test on a specific platform." + help: "Test on a specific platform.", + envKey: .testPlatform ) var platform: String? @Option( name: .shortAndLong, - help: "Test with a specific version of the OS." + help: "Test with a specific version of the OS.", + envKey: .testOS ) var os: String? @Flag( name: .long, - help: "When passed, append arch=x86_64 to the 'destination' to run simulator in a Rosetta mode." + help: "When passed, append arch=x86_64 to the 'destination' to run simulator in a Rosetta mode.", + envKey: .testRosetta ) var rosetta: Bool = false @Option( name: [.long, .customShort("C")], - help: "The configuration to be used when testing the scheme." + help: "The configuration to be used when testing the scheme.", + envKey: .testConfiguration ) var configuration: String? @Flag( name: .long, - help: "When passed, it skips testing UI Tests targets." + help: "When passed, it skips testing UI Tests targets.", + envKey: .testSkipUITests ) var skipUITests: Bool = false @Option( name: [.long, .customShort("T")], - help: "Path where test result bundle will be saved." + help: "Path where test result bundle will be saved.", + completion: .directory, + envKey: .testResultBundlePath ) var resultBundlePath: String? @Option( - help: "Overrides the folder that should be used for derived data when testing a project." + help: "[Deprecated] Overrides the folder that should be used for derived data when testing a project.", + completion: .directory, + envKey: .testDerivedDataPath ) var derivedDataPath: String? @Option( name: .long, - help: "Tests will retry of times until success. Example: if 1 is specified, the test will be retried at most once, hence it will run up to 2 times." + help: "[Deprecated] Tests will retry of times until success. Example: if 1 is specified, the test will be retried at most once, hence it will run up to 2 times.", + envKey: .testRetryCount ) var retryCount: Int = 0 @Option( name: .long, - help: "The test plan to run." + help: "The test plan to run.", + envKey: .testTestPlan ) var testPlan: String? @@ -98,7 +119,7 @@ public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { name: .long, parsing: .upToNextOption, help: "The list of test identifiers you want to test. Expected format is TestTarget[/TestClass[/TestMethod]]. It is applied before --skip-testing", - transform: TestIdentifier.init(string:) + envKey: .testTestTargets ) var testTargets: [TestIdentifier] = [] @@ -106,47 +127,121 @@ public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { name: .long, parsing: .upToNextOption, help: "The list of test identifiers you want to skip testing. Expected format is TestTarget[/TestClass[/TestMethod]].", - transform: TestIdentifier.init(string:) + envKey: .testSkipTestTargets ) var skipTestTargets: [TestIdentifier] = [] @Option( name: .customLong("filter-configurations"), parsing: .upToNextOption, - help: "The list of configurations you want to test. It is applied before --skip-configuration" + help: "The list of configurations you want to test. It is applied before --skip-configuration", + envKey: .testConfigurations ) var configurations: [String] = [] @Option( name: .long, parsing: .upToNextOption, - help: "The list of configurations you want to skip testing." + help: "The list of configurations you want to skip testing.", + envKey: .testSkipConfigurations ) var skipConfigurations: [String] = [] + @Flag( + help: "Ignore binary cache and use sources only.", + envKey: .testBinaryCache + ) + var binaryCache: Bool = true + + @Flag( + help: "Run all tests instead of selectively test only those that have changed since the last successful test run.", + envKey: .testSelectiveTesting + ) + var selectiveTesting: Bool = true + @Flag( name: .long, - help: "When passed, it generates the project and skips testing. This is useful for debugging purposes." + help: "When passed, it generates the project and skips testing. This is useful for debugging purposes.", + envKey: .testGenerateOnly ) var generateOnly: Bool = false + @Argument( + parsing: .postTerminator, + help: "xcodebuild arguments that will be passthrough" + ) + var passthroughXcodeBuildArguments: [String] = [] + public func validate() throws { - try TestService().validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ) } + private var notAllowedPassthroughXcodeBuildArguments = [ + "-scheme", + "-workspace", + "-project", + "-testPlan", + "-skip-test-configuration", + "-only-test-configuration", + "-only-testing", + "-skip-testing", + ] + public func run() async throws { - let absolutePath: AbsolutePath + // Check if passthrough arguments are already handled by tuist + try notAllowedPassthroughXcodeBuildArguments.forEach { + if passthroughXcodeBuildArguments.contains($0) { + throw XcodeBuildPassthroughArgumentError.alreadyHandled($0) + } + } - if let path { - absolutePath = try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + // Suggest the user to use passthrough arguments if already supported by xcodebuild + if let derivedDataPath { + logger + .warning( + "--derivedDataPath is deprecated please use -derivedDataPath \(derivedDataPath) after the terminator (--) instead to passthrough parameters to xcodebuild" + ) + } + if retryCount > 0 { + logger + .warning( + "--retryCount is deprecated please use -retry-tests-on-failure -test-iterations \(retryCount + 1) after the terminator (--) instead to passthrough parameters to xcodebuild" + ) + } + + let absolutePath = if let path { + try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) } else { - absolutePath = FileHandler.shared.currentPath + FileHandler.shared.currentPath } - try await TestService().run( + defer { + var parameters: [String: AnyCodable] = [ + "no_binary_cache": AnyCodable(!binaryCache), + "no_selective_testing": AnyCodable(!selectiveTesting), + ] + parameters["cacheable_targets"] = AnyCodable(CacheAnalyticsStore.shared.cacheableTargets) + parameters["local_cache_target_hits"] = AnyCodable(CacheAnalyticsStore.shared.localCacheTargetsHits) + parameters["remote_cache_target_hits"] = AnyCodable(CacheAnalyticsStore.shared.remoteCacheTargetsHits) + parameters["test_targets"] = AnyCodable(CacheAnalyticsStore.shared.testTargets) + parameters["local_test_target_hits"] = AnyCodable(CacheAnalyticsStore.shared.localTestTargetHits) + parameters["remote_test_target_hits"] = AnyCodable(CacheAnalyticsStore.shared.remoteTestTargetHits) + parameters["target_hashes"] = AnyCodable(CacheAnalyticsStore.shared.targetHashes) + parameters["graph_path"] = AnyCodable(CacheAnalyticsStore.shared.graphPath) + + TestCommand.analyticsDelegate?.addParameters( + parameters + ) + } + + try await TestService( + generatorFactory: Self.generatorFactory, + cacheStorageFactory: Self.cacheStorageFactory + ).run( + runId: runId, schemeName: scheme, clean: clean, configuration: configuration, @@ -174,7 +269,20 @@ public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { ) }, validateTestTargetsParameters: false, - generateOnly: generateOnly + ignoreBinaryCache: !binaryCache, + ignoreSelectiveTesting: !selectiveTesting, + generateOnly: generateOnly, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) } } + +extension TestIdentifier: ExpressibleByArgument { + public init?(argument: String) { + do { + try self.init(string: argument) + } catch { + return nil + } + } +} diff --git a/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift b/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift index dc6a91bb0cf..cbc45837c83 100644 --- a/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift +++ b/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift @@ -1,6 +1,7 @@ import Foundation import TuistAnalytics import TuistAsyncQueue +import TuistCore import TuistSupport /// `CommandEventTagger` builds a `CommandEvent` by grouping information @@ -17,6 +18,7 @@ public final class CommandEventFactory { public func make(from info: TrackableCommandInfo) -> CommandEvent { let commandEvent = CommandEvent( + runId: info.runId, name: info.name, subcommand: info.subcommand, params: info.parameters, @@ -27,7 +29,8 @@ public final class CommandEventFactory { swiftVersion: machineEnvironment.swiftVersion, macOSVersion: machineEnvironment.macOSVersion, machineHardwareName: machineEnvironment.hardwareName, - isCI: machineEnvironment.isCI + isCI: machineEnvironment.isCI, + status: info.status ) return commandEvent } diff --git a/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift b/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift index 0323b0a443e..caa8b181484 100644 --- a/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift +++ b/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift @@ -1,16 +1,20 @@ import AnyCodable import ArgumentParser import Foundation +import TuistAnalytics import TuistAsyncQueue +import TuistCore import TuistSupport /// `TrackableCommandInfo` contains the information to report the execution of a command public struct TrackableCommandInfo { + let runId: String let name: String let subcommand: String? let parameters: [String: AnyCodable] let commandArguments: [String] let durationInMs: Int + let status: CommandEvent.Status } /// A `TrackableCommand` wraps a `ParsableCommand` and reports its execution to an analytics provider @@ -37,25 +41,45 @@ public class TrackableCommand: TrackableParametersDelegate { } public func run() async throws { + let runId: String let timer = clock.startTimer() - if let command = command as? HasTrackableParameters { + if let command = command as? HasTrackableParameters & ParsableCommand { type(of: command).analyticsDelegate = self - } - if var asyncCommand = command as? AsyncParsableCommand { - try await asyncCommand.run() + runId = command.runId + self.command = command } else { - try command.run() + runId = UUID().uuidString + } + do { + if var asyncCommand = command as? AsyncParsableCommand { + try await asyncCommand.run() + } else { + try command.run() + } + try dispatchCommandEvent(timer: timer, status: .success, runId: runId) + } catch { + try dispatchCommandEvent(timer: timer, status: .failure("\(error)"), runId: runId) + throw error } + } + + private func dispatchCommandEvent( + timer: any ClockTimer, + status: CommandEvent.Status, + runId: String + ) throws { let durationInSeconds = timer.stop() let durationInMs = Int(durationInSeconds * 1000) let configuration = type(of: command).configuration let (name, subcommand) = extractCommandName(from: configuration) let info = TrackableCommandInfo( + runId: runId, name: name, subcommand: subcommand, parameters: trackedParameters, commandArguments: commandArguments, - durationInMs: durationInMs + durationInMs: durationInMs, + status: status ) let commandEvent = commandEventFactory.make(from: info) try asyncQueue.dispatch(event: commandEvent) diff --git a/Sources/TuistKit/Commands/TrackableCommand/TrackableParametersDelegate.swift b/Sources/TuistKit/Commands/TrackableCommand/TrackableParametersDelegate.swift index 1f1bdf0c9af..356f3ae8ac6 100644 --- a/Sources/TuistKit/Commands/TrackableCommand/TrackableParametersDelegate.swift +++ b/Sources/TuistKit/Commands/TrackableCommand/TrackableParametersDelegate.swift @@ -4,6 +4,8 @@ import Foundation /// Commands that conform to `HasTrackableParameters` can report extra parameters that are only known at runtime public protocol HasTrackableParameters { static var analyticsDelegate: TrackableParametersDelegate? { get set } + /// ID that uniquely identifies the command run + var runId: String { get set } } /// `TrackableParametersDelegate` contains the callback that should be called diff --git a/Sources/TuistKit/Commands/TuistCommand.swift b/Sources/TuistKit/Commands/TuistCommand.swift index 79e13507190..689b9e83bf6 100644 --- a/Sources/TuistKit/Commands/TuistCommand.swift +++ b/Sources/TuistKit/Commands/TuistCommand.swift @@ -1,5 +1,10 @@ @_exported import ArgumentParser import Foundation +import OpenAPIRuntime +import Path +import TuistAnalytics +import TuistLoader +import TuistServer import TuistSupport public struct TuistCommand: AsyncParsableCommand { @@ -9,38 +14,76 @@ public struct TuistCommand: AsyncParsableCommand { CommandConfiguration( commandName: "tuist", abstract: "Generate, build and test your Xcode projects.", - subcommands: [ - BuildCommand.self, - CleanCommand.self, - DumpCommand.self, - EditCommand.self, - FetchCommand.self, - GenerateCommand.self, - GraphCommand.self, - InitCommand.self, - MigrationCommand.self, - PluginCommand.self, - RunCommand.self, - ScaffoldCommand.self, - SigningCommand.self, - TestCommand.self, - VersionCommand.self, + subcommands: [], + groupedSubcommands: [ + CommandGroup( + name: "Start", + subcommands: [ + InitCommand.self, + ] + ), + CommandGroup( + name: "Develop", + subcommands: [ + BuildCommand.self, + CacheCommand.self, + CleanCommand.self, + DumpCommand.self, + EditCommand.self, + GenerateCommand.self, + GraphCommand.self, + InstallCommand.self, + MigrationCommand.self, + PluginCommand.self, + RunCommand.self, + ScaffoldCommand.self, + TestCommand.self, + ] + ), + CommandGroup( + name: "Share", + subcommands: [ + ShareCommand.self, + ] + ), + CommandGroup( + name: "Account", + subcommands: [ + ProjectCommand.self, + OrganizationCommand.self, + AuthCommand.self, + SessionCommand.self, + LogoutCommand.self, + ] + ), ] ) } - @Flag( - name: [.customLong("help-env")], - help: "Display subcommands to manage the environment tuist versions." - ) - var isTuistEnvHelp: Bool = false - public static func main( _ arguments: [String]? = nil, - parseAsRoot: ((_ arguments: [String]?) throws -> ParsableCommand) = Self.parseAsRoot, - execute: ((_ command: ParsableCommand, _ commandArguments: [String]) async throws -> Void)? = nil - ) async { - let execute = execute ?? Self.execute + parseAsRoot: ((_ arguments: [String]?) throws -> ParsableCommand) = Self.parseAsRoot + ) async throws { + let path: AbsolutePath + if let argumentIndex = CommandLine.arguments.firstIndex(of: "--path") { + path = try AbsolutePath(validating: CommandLine.arguments[argumentIndex + 1], relativeTo: .current) + } else { + path = .current + } + + let backend: TuistAnalyticsBackend? + let config = try await ConfigLoader().loadConfig(path: path) + if let fullHandle = config.fullHandle { + backend = TuistAnalyticsServerBackend( + fullHandle: fullHandle, + url: config.url + ) + } else { + backend = nil + } + let dispatcher = TuistAnalyticsDispatcher(backend: backend) + try TuistAnalytics.bootstrap(dispatcher: dispatcher) + let errorHandler = ErrorHandler() let executeCommand: () async throws -> Void let processedArguments = Array(processArguments(arguments)?.dropFirst() ?? []) @@ -50,19 +93,20 @@ public struct TuistCommand: AsyncParsableCommand { try await ScaffoldCommand.preprocess(processedArguments) } if processedArguments.first == InitCommand.configuration.commandName { - try InitCommand.preprocess(processedArguments) + try await InitCommand.preprocess(processedArguments) } let command = try parseAsRoot(processedArguments) executeCommand = { - try await execute( - command, - processedArguments + let trackableCommand = TrackableCommand( + command: command, + commandArguments: processedArguments ) + try await trackableCommand.run() } } catch { parsedError = error executeCommand = { - try executeTask(with: processedArguments) + try await executeTask(with: processedArguments) } } @@ -73,6 +117,11 @@ public struct TuistCommand: AsyncParsableCommand { WarningController.shared.flush() errorHandler.fatal(error: error) _exit(exitCode(for: error).rawValue) + } catch let error as ClientError where error.underlyingError is ServerClientAuthenticationError { + WarningController.shared.flush() + // swiftlint:disable:next force_cast + logger.error("\((error.underlyingError as! ServerClientAuthenticationError).description)") + _exit(exitCode(for: error).rawValue) } catch { WarningController.shared.flush() if let parsedError { @@ -88,8 +137,8 @@ public struct TuistCommand: AsyncParsableCommand { } } - private static func executeTask(with processedArguments: [String]) throws { - try TuistService().run( + private static func executeTask(with processedArguments: [String]) async throws { + try await TuistService().run( arguments: processedArguments, tuistBinaryPath: processArguments()!.first! ) @@ -98,25 +147,13 @@ public struct TuistCommand: AsyncParsableCommand { private static func handleParseError(_ error: Error) -> Never { let exitCode = exitCode(for: error).rawValue if exitCode == 0 { - logger.info("\(fullMessage(for: error))") + logger.notice("\(fullMessage(for: error))") } else { logger.error("\(fullMessage(for: error))") } _exit(exitCode) } - private static func execute( - command: ParsableCommand, - commandArguments _: [String] - ) async throws { - var command = command - if var asyncCommand = command as? AsyncParsableCommand { - try await asyncCommand.run() - } else { - try command.run() - } - } - // MARK: - Helpers static func processArguments(_ arguments: [String]? = nil) -> [String]? { diff --git a/Sources/TuistKit/Commands/VersionCommand.swift b/Sources/TuistKit/Commands/VersionCommand.swift deleted file mode 100644 index 9e2b8a714f0..00000000000 --- a/Sources/TuistKit/Commands/VersionCommand.swift +++ /dev/null @@ -1,16 +0,0 @@ -import ArgumentParser -import Foundation -import TSCBasic - -struct VersionCommand: ParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "version", - abstract: "Outputs the current version of tuist" - ) - } - - func run() throws { - try VersionService().run() - } -} diff --git a/Sources/TuistKit/Generator/Generator.swift b/Sources/TuistKit/Generator/Generator.swift index d85d75579fd..7c9b243b964 100644 --- a/Sources/TuistKit/Generator/Generator.swift +++ b/Sources/TuistKit/Generator/Generator.swift @@ -1,20 +1,21 @@ import Foundation +import Mockable +import Path import ProjectDescription -import TSCBasic import TuistCore import TuistDependencies import TuistGenerator -import TuistGraph import TuistLoader import TuistPlugin -import TuistSigning import TuistSupport +import XcodeGraph +@Mockable public protocol Generating { @discardableResult func load(path: AbsolutePath) async throws -> Graph func generate(path: AbsolutePath) async throws -> AbsolutePath - func generateWithGraph(path: AbsolutePath) async throws -> (AbsolutePath, Graph) + func generateWithGraph(path: AbsolutePath) async throws -> (AbsolutePath, Graph, MapperEnvironment) } public class Generator: Generating { @@ -24,7 +25,6 @@ public class Generator: Generating { private let writer: XcodeProjWriting = XcodeProjWriter() private let swiftPackageManagerInteractor: TuistGenerator.SwiftPackageManagerInteracting = TuistGenerator .SwiftPackageManagerInteractor() - private let signingInteractor: SigningInteracting = SigningInteractor() private let sideEffectDescriptorExecutor: SideEffectDescriptorExecuting private let configLoader: ConfigLoading private let manifestGraphLoader: ManifestGraphLoading @@ -44,27 +44,27 @@ public class Generator: Generating { } public func generate(path: AbsolutePath) async throws -> AbsolutePath { - let (generatedPath, _) = try await generateWithGraph(path: path) + let (generatedPath, _, _) = try await generateWithGraph(path: path) return generatedPath } - public func generateWithGraph(path: AbsolutePath) async throws -> (AbsolutePath, Graph) { - let (graph, sideEffects) = try await load(path: path) + public func generateWithGraph(path: AbsolutePath) async throws -> (AbsolutePath, Graph, MapperEnvironment) { + let (graph, sideEffects, environment) = try await load(path: path) // Load let graphTraverser = GraphTraverser(graph: graph) // Lint - try lint(graphTraverser: graphTraverser) + try await lint(graphTraverser: graphTraverser) // Generate let workspaceDescriptor = try generator.generateWorkspace(graphTraverser: graphTraverser) // Write - try writer.write(workspace: workspaceDescriptor) + try await writer.write(workspace: workspaceDescriptor) // Mapper side effects - try sideEffectDescriptorExecutor.execute(sideEffects: sideEffects) + try await sideEffectDescriptorExecutor.execute(sideEffects: sideEffects) // Post Generate Actions try await postGenerationActions( @@ -74,21 +74,25 @@ public class Generator: Generating { printAndFlushPendingLintWarnings() - return (workspaceDescriptor.xcworkspacePath, graph) + return (workspaceDescriptor.xcworkspacePath, graph, environment) } public func load(path: AbsolutePath) async throws -> Graph { try await load(path: path).0 } - func load(path: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor]) { - let (graph, sideEffectDescriptors, issues) = try await manifestGraphLoader.load(path: path) + func load(path: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { + logger.notice("Loading and constructing the graph", metadata: .section) + logger.notice("It might take a while if the cache is empty") + + let (graph, sideEffectDescriptors, environment, issues) = try await manifestGraphLoader.load(path: path) + lintingIssues.append(contentsOf: issues) - return (graph, sideEffectDescriptors) + return (graph, sideEffectDescriptors, environment) } - private func lint(graphTraverser: GraphTraversing) throws { - let config = try configLoader.loadConfig(path: graphTraverser.path) + private func lint(graphTraverser: GraphTraversing) async throws { + let config = try await configLoader.loadConfig(path: graphTraverser.path) let environmentIssues = try environmentLinter.lint(config: config) try environmentIssues.printAndThrowErrorsIfNeeded() @@ -100,9 +104,8 @@ public class Generator: Generating { } private func postGenerationActions(graphTraverser: GraphTraversing, workspaceName: String) async throws { - let config = try configLoader.loadConfig(path: graphTraverser.path) + let config = try await configLoader.loadConfig(path: graphTraverser.path) - lintingIssues.append(contentsOf: try signingInteractor.install(graphTraverser: graphTraverser)) try await swiftPackageManagerInteractor.install( graphTraverser: graphTraverser, workspaceName: workspaceName, diff --git a/Sources/TuistKit/Generator/GeneratorFactory.swift b/Sources/TuistKit/Generator/GeneratorFactory.swift index aafedc9e247..2b6951f4766 100644 --- a/Sources/TuistKit/Generator/GeneratorFactory.swift +++ b/Sources/TuistKit/Generator/GeneratorFactory.swift @@ -1,32 +1,67 @@ import Foundation -import TSCBasic +import Mockable +import Path +import TuistAutomation import TuistCore import TuistGenerator -import TuistGraph import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph /// The protocol describes the interface of a factory that instantiates /// generators for different commands -protocol GeneratorFactorying { +@Mockable +public protocol GeneratorFactorying { /// Returns the generator to generate a project to run tests on. /// - Parameter config: The project configuration - /// - Parameter testsCacheDirectory: The cache directory used for tests. /// - Parameter skipUITests: Whether UI tests should be skipped. + /// - Parameter ignoreBinaryCache: True to not include binaries from the cache. + /// - Parameter ignoreSelectiveTesting: True to run all tests + /// - Parameter cacheStorage: The cache storage instance. /// - Returns: A Generator instance. - func test( + func testing( config: Config, - testsCacheDirectory: AbsolutePath, testPlan: String?, includedTargets: Set, excludedTargets: Set, - skipUITests: Bool + skipUITests: Bool, + configuration: String?, + ignoreBinaryCache: Bool, + ignoreSelectiveTesting: Bool, + cacheStorage: CacheStoring + ) -> Generating + + /// Returns the generator for focused projects. + /// - Parameter config: The project configuration. + /// - Parameter sources: The list of targets whose sources should be included. + /// - Parameter configuration: The configuration to generate for. + /// - Parameter ignoreBinaryCache: True to not include binaries from the cache. + /// - Parameter cacheStorage: The cache storage instance. + /// - Returns: The generator for focused projects. + func generation( + config: Config, + sources: Set, + configuration: String?, + ignoreBinaryCache: Bool, + cacheStorage: CacheStoring + ) -> Generating + + /// Returns a generator for building a project. + /// - Parameters: + /// - config: The project configuration + /// - Returns: A Generator instance + func building( + config: Config, + configuration: String?, + ignoreBinaryCache: Bool, + cacheStorage: CacheStoring ) -> Generating /// Returns the default generator. /// - Parameter config: The project configuration. /// - Returns: A Generator instance. - func `default`( + func defaultGenerator( config: Config ) -> Generating } @@ -38,13 +73,16 @@ public class GeneratorFactory: GeneratorFactorying { self.contentHasher = contentHasher } - func test( + public func testing( config: Config, - testsCacheDirectory: AbsolutePath, testPlan: String?, includedTargets: Set, excludedTargets: Set, - skipUITests: Bool + skipUITests: Bool, + configuration _: String?, + ignoreBinaryCache _: Bool, + ignoreSelectiveTesting _: Bool, + cacheStorage _: CacheStoring ) -> Generating { let contentHasher = ContentHasher() let projectMapperFactory = ProjectMapperFactory(contentHasher: contentHasher) @@ -54,7 +92,6 @@ public class GeneratorFactory: GeneratorFactorying { let graphMappers = graphMapperFactory.automation( config: config, - testsCacheDirectory: testsCacheDirectory, testPlan: testPlan, includedTargets: includedTargets, excludedTargets: excludedTargets @@ -71,7 +108,26 @@ public class GeneratorFactory: GeneratorFactorying { ) } - public func `default`(config: Config) -> Generating { + public func generation( + config: Config, + sources _: Set, + configuration _: String?, + ignoreBinaryCache _: Bool, + cacheStorage _: CacheStoring + ) -> Generating { + defaultGenerator(config: config) + } + + public func building( + config: Config, + configuration _: String?, + ignoreBinaryCache _: Bool, + cacheStorage _: CacheStoring + ) -> Generating { + defaultGenerator(config: config) + } + + public func defaultGenerator(config: Config) -> Generating { let contentHasher = ContentHasher() let projectMapperFactory = ProjectMapperFactory(contentHasher: contentHasher) let projectMappers = projectMapperFactory.default() diff --git a/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift b/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift index ae61d37184c..579db9b3a2d 100644 --- a/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift +++ b/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistDependencies import TuistGenerator -import TuistGraph -import TuistSigning +import XcodeGraph /// The GraphMapperFactorying describes the interface of a factory of graph mappers. /// Methods in the interface map with workflows exposed to the user. @@ -13,7 +12,6 @@ protocol GraphMapperFactorying { /// - Returns: A graph mapper. func automation( config: Config, - testsCacheDirectory: AbsolutePath, testPlan: String?, includedTargets: Set, excludedTargets: Set @@ -31,7 +29,6 @@ public final class GraphMapperFactory: GraphMapperFactorying { public func automation( config: Config, - testsCacheDirectory _: AbsolutePath, testPlan: String?, includedTargets: Set, excludedTargets: Set @@ -54,12 +51,14 @@ public final class GraphMapperFactory: GraphMapperFactorying { config: Config ) -> [GraphMapping] { var mappers: [GraphMapping] = [] + mappers.append(ModuleMapMapper()) mappers.append(UpdateWorkspaceProjectsGraphMapper()) - mappers.append(PruneOrphanExternalTargetsGraphMapper()) mappers.append(ExternalProjectsPlatformNarrowerGraphMapper()) + mappers.append(PruneOrphanExternalTargetsGraphMapper()) if config.generationOptions.enforceExplicitDependencies { mappers.append(ExplicitDependencyGraphMapper()) } + mappers.append(TreeShakePrunedTargetsGraphMapper()) return mappers } } diff --git a/Sources/TuistKit/Mappers/Factories/ProjectMapperFactory.swift b/Sources/TuistKit/Mappers/Factories/ProjectMapperFactory.swift index 73ad8f7b4da..abf69ecc5a3 100644 --- a/Sources/TuistKit/Mappers/Factories/ProjectMapperFactory.swift +++ b/Sources/TuistKit/Mappers/Factories/ProjectMapperFactory.swift @@ -1,11 +1,11 @@ import Foundation import TuistAutomation import TuistCore +import TuistDependencies import TuistGenerator -import TuistGraph import TuistLoader -import TuistSigning import TuistSupport +import XcodeGraph /// The protocol describes an interface for getting project mappers. protocol ProjectMapperFactorying { @@ -74,12 +74,12 @@ public final class ProjectMapperFactory: ProjectMapperFactorying { // Entitlements mappers.append(GenerateEntitlementsProjectMapper()) + // Privacy Manifest + mappers.append(GeneratePrivacyManifestProjectMapper()) + // Template macros mappers.append(IDETemplateMacrosMapper()) - // Signing - mappers.append(SigningMapper()) - return mappers } } diff --git a/Sources/TuistKit/Mappers/Factories/WorkspaceMapperFactory.swift b/Sources/TuistKit/Mappers/Factories/WorkspaceMapperFactory.swift index 0cd9ceea44e..371f14bf1e3 100644 --- a/Sources/TuistKit/Mappers/Factories/WorkspaceMapperFactory.swift +++ b/Sources/TuistKit/Mappers/Factories/WorkspaceMapperFactory.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic +import Path import TSCUtility import TuistAutomation import TuistCore +import TuistDependencies import TuistGenerator -import TuistGraph +import XcodeGraph protocol WorkspaceMapperFactorying { /// Returns the default workspace mapper. @@ -58,14 +59,12 @@ public final class WorkspaceMapperFactory: WorkspaceMapperFactorying { AutogeneratedWorkspaceSchemeWorkspaceMapper(forceWorkspaceSchemes: forceWorkspaceSchemes) ) - mappers.append( - ModuleMapMapper() - ) - mappers.append( LastUpgradeVersionWorkspaceMapper() ) + mappers.append(ExternalDependencyPathWorkspaceMapper()) + return mappers } } diff --git a/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift b/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift index 5cc20700aac..d45305515c3 100644 --- a/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift +++ b/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift @@ -1,7 +1,27 @@ import Foundation +import Path import TSCBasic import TuistCore -import TuistGraph +import TuistSupport +import XcodeGraph + +public enum FocusTargetsGraphMappersError: FatalError, Equatable { + case targetsNotFound([String]) + + public var type: ErrorType { + switch self { + case .targetsNotFound: + return .abort + } + } + + public var description: String { + switch self { + case let .targetsNotFound(targets): + return "The following targets were not found: \(targets.joined(separator: ", ")). Please, make sure they exist." + } + } +} /// `FocusTargetsGraphMappers` is used to filter out some targets and their dependencies and tests targets. public final class FocusTargetsGraphMappers: GraphMapping { @@ -21,7 +41,7 @@ public final class FocusTargetsGraphMappers: GraphMapping { self.excludedTargets = excludedTargets } - public func map(graph: Graph) throws -> (Graph, [SideEffectDescriptor]) { + public func map(graph: Graph, environment: MapperEnvironment) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { let graphTraverser = GraphTraverser(graph: graph) var graph = graph let userSpecifiedSourceTargets = graphTraverser.filterIncludedTargets( @@ -32,18 +52,29 @@ public final class FocusTargetsGraphMappers: GraphMapping { excludingExternalTargets: true ) + let unavailableIncludedTargets = Set(includedTargets).subtracting(userSpecifiedSourceTargets.map(\.target.name)) + if !unavailableIncludedTargets.isEmpty { + throw FocusTargetsGraphMappersError.targetsNotFound(Array(unavailableIncludedTargets)) + } + let filteredTargets = Set(try topologicalSort( Array(userSpecifiedSourceTargets), successors: { Array(graphTraverser.directTargetDependencies(path: $0.path, name: $0.target.name)).map(\.graphTarget) } )) - for graphTarget in graphTraverser.allTargets() { - if !filteredTargets.contains(graphTarget) { - var target = graphTarget.target - target.prune = true - graph.targets[graphTarget.path]?[target.name] = target + graph.projects = graph.projects.mapValues { project in + var project = project + project.targets = project.targets.mapValues { target in + var target = target + if !filteredTargets.contains(GraphTarget(path: project.path, target: target, project: project)) { + target.prune = true + } + return target } + + return project } - return (graph, []) + + return (graph, [], environment) } } diff --git a/Sources/TuistKit/Mappers/Graph/UpdateWorkspaceProjectsGraphMapper.swift b/Sources/TuistKit/Mappers/Graph/UpdateWorkspaceProjectsGraphMapper.swift index 11d8f6bd09f..3949adb0f51 100644 --- a/Sources/TuistKit/Mappers/Graph/UpdateWorkspaceProjectsGraphMapper.swift +++ b/Sources/TuistKit/Mappers/Graph/UpdateWorkspaceProjectsGraphMapper.swift @@ -1,19 +1,21 @@ import Foundation import TuistCore -import TuistGraph +import XcodeGraph /// A mapper that ensures that the list of projects of the workspace is in sync /// with the projects available in the graph. public final class UpdateWorkspaceProjectsGraphMapper: GraphMapping { public init() {} - public func map(graph: Graph) throws -> (Graph, [SideEffectDescriptor]) { + public func map(graph: Graph, environment: MapperEnvironment) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { + logger.debug("Transforming graph \(graph.name): Aligning workspace projects with the graph's") + var graph = graph let graphProjects = Set(graph.projects.map(\.key)) let workspaceProjects = Set(graph.workspace.projects).intersection(graphProjects) var workspace = graph.workspace workspace.projects = Array(workspaceProjects.union(graphProjects)) graph.workspace = workspace - return (graph, []) + return (graph, [], environment) } } diff --git a/Sources/TuistKit/Mappers/TreeShakePrunedTargetsGraphMapper.swift b/Sources/TuistKit/Mappers/TreeShakePrunedTargetsGraphMapper.swift index 8c1f8b118e8..911a4546bcd 100644 --- a/Sources/TuistKit/Mappers/TreeShakePrunedTargetsGraphMapper.swift +++ b/Sources/TuistKit/Mappers/TreeShakePrunedTargetsGraphMapper.swift @@ -1,26 +1,26 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph +import XcodeGraph public final class TreeShakePrunedTargetsGraphMapper: GraphMapping { public init() {} - public func map(graph: Graph) throws -> (Graph, [SideEffectDescriptor]) { - let sourceTargets: Set = Set(graph.targets.flatMap { projectPath, targets -> [TargetReference] in - guard graph.projects[projectPath] != nil else { return [] } - return targets.compactMap { _, target -> TargetReference? in + public func map(graph: Graph, environment: MapperEnvironment) throws -> (Graph, [SideEffectDescriptor], MapperEnvironment) { + logger.debug("Transforming graph \(graph.name): Tree-shaking nodes") + let sourceTargets: Set = Set(graph.projects.flatMap { projectPath, project -> [TargetReference] in + return project.targets.compactMap { _, target -> TargetReference? in if target.prune { return nil } return TargetReference(projectPath: projectPath, name: target.name) } }) // If the number of source targets matches the number of targets in the graph there's nothing to be pruned. - if sourceTargets.count == graph.targets.flatMap(\.value.values).count { return (graph, []) } + if sourceTargets.count == graph.projects.values.flatMap(\.targets.values).count { return (graph, [], environment) } let projects = graph.projects.reduce(into: [AbsolutePath: Project]()) { acc, next in let targets = self.treeShake( - targets: next.value.targets, + targets: Array(next.value.targets.values), path: next.key, graph: graph, sourceTargets: sourceTargets @@ -32,7 +32,10 @@ public final class TreeShakePrunedTargetsGraphMapper: GraphMapping { schemes: next.value.schemes, sourceTargets: sourceTargets ) - acc[next.key] = next.value.with(targets: targets).with(schemes: schemes) + var project = next.value + project.schemes = schemes + project.targets = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) }) + acc[next.key] = project } } @@ -45,14 +48,7 @@ public final class TreeShakePrunedTargetsGraphMapper: GraphMapping { var graph = graph graph.workspace = workspace graph.projects = projects - graph.targets = sourceTargets.reduce(into: [AbsolutePath: [String: Target]]()) { acc, targetReference in - var targets = acc[targetReference.projectPath, default: [:]] - if let target = graph.targets[targetReference.projectPath, default: [:]][targetReference.name] { - targets[target.name] = target - } - acc[targetReference.projectPath] = targets - } - return (graph, []) + return (graph, [], environment) } fileprivate func treeShake(workspace: Workspace, projects: [Project], sourceTargets: Set) -> Workspace { @@ -71,7 +67,7 @@ public final class TreeShakePrunedTargetsGraphMapper: GraphMapping { sourceTargets: Set ) -> [Target] { targets.compactMap { target -> Target? in - guard let target = graph.targets[path, default: [:]][target.name] else { return nil } + guard let target = graph.projects[path]?.targets[target.name] else { return nil } let targetReference = TargetReference(projectPath: path, name: target.name) guard sourceTargets.contains(targetReference) else { return nil } return target @@ -98,6 +94,18 @@ public final class TreeShakePrunedTargetsGraphMapper: GraphMapping { return nil } + if let expandVariableFromTarget = scheme.runAction?.expandVariableFromTarget, + !sourceTargets.contains(expandVariableFromTarget) + { + return nil + } + + if let expandVariableFromTarget = scheme.testAction?.expandVariableFromTarget, + !sourceTargets.contains(expandVariableFromTarget) + { + return nil + } + return scheme } } diff --git a/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceIdentifierMapper.swift b/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceIdentifierMapper.swift index d3373ad2e4a..49be12e0e40 100644 --- a/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceIdentifierMapper.swift +++ b/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceIdentifierMapper.swift @@ -8,6 +8,8 @@ import TuistSupport /// This is used to help identify the workspace as one that has been generated by tuist. final class TuistWorkspaceIdentifierMapper: WorkspaceMapping { func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) { + logger.debug("Transforming workspace \(workspace.workspace.name): Signing the workspace") + let tuistGeneratedFileDescriptor = FileDescriptor( path: workspace .workspace diff --git a/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceRenderMarkdownMapper.swift b/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceRenderMarkdownMapper.swift index cf674b93cc2..d8c20bd8c35 100644 --- a/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceRenderMarkdownMapper.swift +++ b/Sources/TuistKit/Mappers/Workspace/TuistWorkspaceRenderMarkdownMapper.swift @@ -8,6 +8,8 @@ import TuistSupport /// This is used to render markdown inside the workspace. final class TuistWorkspaceRenderMarkdownReadmeMapper: WorkspaceMapping { func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) { + logger.debug("Transforming workspace \(workspace.workspace.name): Including .xcodesample.plist") + let tuistGeneratedFileDescriptor = FileDescriptor( path: workspace .workspace diff --git a/Sources/TuistKit/ProjectEditor/EditablePluginManifest.swift b/Sources/TuistKit/ProjectEditor/EditablePluginManifest.swift index 911316abe85..dd0473733f0 100644 --- a/Sources/TuistKit/ProjectEditor/EditablePluginManifest.swift +++ b/Sources/TuistKit/ProjectEditor/EditablePluginManifest.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// A plugin which is loaded & editable as part of the `tuist edit` command. struct EditablePluginManifest { diff --git a/Sources/TuistKit/ProjectEditor/ProjectEditor.swift b/Sources/TuistKit/ProjectEditor/ProjectEditor.swift index 8275697272d..021f651c731 100644 --- a/Sources/TuistKit/ProjectEditor/ProjectEditor.swift +++ b/Sources/TuistKit/ProjectEditor/ProjectEditor.swift @@ -1,11 +1,12 @@ import Foundation -import TSCBasic +import Mockable +import Path import TuistCore import TuistGenerator -import TuistGraph import TuistLoader import TuistScaffold import TuistSupport +import XcodeGraph enum ProjectEditorError: FatalError, Equatable { /// This error is thrown when we try to edit in a project in a directory that has no editable files. @@ -25,6 +26,7 @@ enum ProjectEditorError: FatalError, Equatable { } } +@Mockable protocol ProjectEditing: AnyObject { /// Generates an Xcode project to edit the Project defined in the given directory. /// - Parameters: @@ -38,7 +40,7 @@ protocol ProjectEditing: AnyObject { in destinationDirectory: AbsolutePath, onlyCurrentDirectory: Bool, plugins: Plugins - ) throws -> AbsolutePath + ) async throws -> AbsolutePath } final class ProjectEditor: ProjectEditing { @@ -105,7 +107,7 @@ final class ProjectEditor: ProjectEditing { in destinationDirectory: AbsolutePath, onlyCurrentDirectory: Bool, plugins: Plugins - ) throws -> AbsolutePath { + ) async throws -> AbsolutePath { let tuistIgnoreContent = (try? FileHandler.shared.readTextFile(editingPath.appending(component: ".tuistignore"))) ?? "" let tuistIgnoreEntries = try tuistIgnoreContent .split(separator: "\n") @@ -121,8 +123,7 @@ final class ProjectEditor: ProjectEditing { } let pathsToExclude = [ - "**/\(Constants.tuistDirectoryName)/\(Constants.DependenciesDirectory.name)/**", - "**/\(Constants.DependenciesDirectory.packageBuildDirectoryName)/**", + "**/\(Constants.SwiftPackageManager.packageBuildDirectoryName)/**", ] + tuistIgnoreEntries let projectDescriptionPath = try resourceLocator.projectDescription() @@ -132,11 +133,10 @@ final class ProjectEditor: ProjectEditing { onlyCurrentDirectory: onlyCurrentDirectory ) let configPath = manifestFilesLocator.locateConfig(at: editingPath) - let cacheDirectory = try cacheDirectoryProviderFactory.cacheDirectories(config: nil) + let cacheDirectory = try cacheDirectoryProviderFactory.cacheDirectories() let projectDescriptionHelpersBuilder = projectDescriptionHelpersBuilderFactory.projectDescriptionHelpersBuilder( - cacheDirectory: cacheDirectory.cacheDirectory(for: .projectDescriptionHelpers) + cacheDirectory: try cacheDirectory.cacheDirectory(for: .projectDescriptionHelpers) ) - let dependenciesPath = manifestFilesLocator.locateDependencies(at: editingPath) let packageManifestPath = manifestFilesLocator.locatePackageManifest(at: editingPath) let helpers = helpersDirectoryLocator.locate(at: editingPath).map { @@ -168,7 +168,7 @@ final class ProjectEditor: ProjectEditing { plugins: plugins, onlyCurrentDirectory: onlyCurrentDirectory ) - let builtPluginHelperModules = try buildRemotePluginModules( + let builtPluginHelperModules = try await buildRemotePluginModules( in: editingPath, projectDescriptionPath: projectDescriptionPath, plugins: plugins, @@ -177,7 +177,7 @@ final class ProjectEditor: ProjectEditing { /// We error if the user tries to edit a project in a directory where there are no editable files. if projectManifests.isEmpty, editablePluginManifests.isEmpty, helpers.isEmpty, templateSources.isEmpty, - resourceSynthesizers.isEmpty, stencils.isEmpty + resourceSynthesizers.isEmpty, stencils.isEmpty, packageManifestPath == nil { throw ProjectEditorError.noEditableFiles(editingPath) } @@ -192,7 +192,6 @@ final class ProjectEditor: ProjectEditing { sourceRootPath: editingPath, destinationDirectory: destinationDirectory, configPath: configPath, - dependenciesPath: dependenciesPath, packageManifestPath: packageManifestPath, projectManifests: projectManifests.map(\.path), editablePluginManifests: editablePluginManifests, @@ -207,7 +206,7 @@ final class ProjectEditor: ProjectEditing { let graphTraverser = GraphTraverser(graph: graph) let descriptor = try generator.generateWorkspace(graphTraverser: graphTraverser) - try writer.write(workspace: descriptor) + try await writer.write(workspace: descriptor) return descriptor.xcworkspacePath } @@ -238,9 +237,9 @@ final class ProjectEditor: ProjectEditing { projectDescriptionPath: AbsolutePath, plugins: Plugins, projectDescriptionHelpersBuilder: ProjectDescriptionHelpersBuilding - ) throws -> [ProjectDescriptionHelpersModule] { + ) async throws -> [ProjectDescriptionHelpersModule] { let loadedPluginHelpers = plugins.projectDescriptionHelpers.filter { $0.location == .remote } - return try projectDescriptionHelpersBuilder.buildPlugins( + return try await projectDescriptionHelpersBuilder.buildPlugins( at: path, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths.paths(for: projectDescriptionPath), projectDescriptionHelperPlugins: loadedPluginHelpers diff --git a/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift b/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift index 341f597c266..24dd48d0752 100644 --- a/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift +++ b/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader import TuistSupport +import XcodeGraph protocol ProjectEditorMapping: AnyObject { func map( @@ -12,7 +12,6 @@ protocol ProjectEditorMapping: AnyObject { sourceRootPath: AbsolutePath, destinationDirectory: AbsolutePath, configPath: AbsolutePath?, - dependenciesPath: AbsolutePath?, packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], @@ -31,7 +30,10 @@ final class ProjectEditorMapper: ProjectEditorMapping { private let swiftPackageManagerController: SwiftPackageManagerControlling init( - swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController() + swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController( + system: System.shared, + fileHandler: FileHandler.shared + ) ) { self.swiftPackageManagerController = swiftPackageManagerController } @@ -43,7 +45,6 @@ final class ProjectEditorMapper: ProjectEditorMapping { sourceRootPath: AbsolutePath, destinationDirectory: AbsolutePath, configPath: AbsolutePath?, - dependenciesPath: AbsolutePath?, packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], @@ -55,7 +56,8 @@ final class ProjectEditorMapper: ProjectEditorMapping { stencils: [AbsolutePath], projectDescriptionSearchPath: AbsolutePath ) throws -> Graph { - let swiftVersion = try System.shared.swiftVersion() + logger.notice("Building the editable project graph") + let swiftVersion = try SwiftVersionProvider.shared.swiftVersion() let pluginsProject = mapPluginsProject( pluginManifests: editablePluginManifests, @@ -79,7 +81,6 @@ final class ProjectEditorMapper: ProjectEditorMapping { resourceSynthesizers: resourceSynthesizers, stencils: stencils, configPath: configPath, - dependenciesPath: dependenciesPath, packageManifestPath: packageManifestPath, editablePluginTargets: editablePluginManifests.map(\.name), pluginProjectDescriptionHelpersModule: pluginProjectDescriptionHelpersModule @@ -102,15 +103,10 @@ final class ProjectEditorMapper: ProjectEditorMapping { let graphProjects = Dictionary(uniqueKeysWithValues: projects.map { ($0.path, $0) }) - let graphTargets = projects - .lazy - .map { ($0.path, $0.targets) } - .map { path, targets in (path, Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) })) } - let graphDependencies = projects .lazy .flatMap { project -> [(GraphDependency, Set)] in - let graphDependencies = project.targets.map(\.dependencies).lazy.map { dependencies in + let graphDependencies = project.targets.values.map(\.dependencies).lazy.map { dependencies in dependencies.lazy.compactMap { dependency -> GraphDependency? in switch dependency { case let .target(name, _): @@ -125,7 +121,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { } } - return zip(project.targets, graphDependencies).map { target, dependencies in + return zip(project.targets.values, graphDependencies).map { target, dependencies in (GraphDependency.target(name: target.name, path: project.path), Set(dependencies)) } } @@ -136,7 +132,6 @@ final class ProjectEditorMapper: ProjectEditorMapping { workspace: workspace, projects: graphProjects, packages: [:], - targets: Dictionary(uniqueKeysWithValues: graphTargets), dependencies: Dictionary(uniqueKeysWithValues: graphDependencies), dependencyConditions: [:] ) @@ -156,12 +151,11 @@ final class ProjectEditorMapper: ProjectEditorMapping { resourceSynthesizers: [AbsolutePath], stencils: [AbsolutePath], configPath: AbsolutePath?, - dependenciesPath: AbsolutePath?, packageManifestPath: AbsolutePath?, editablePluginTargets: [String], pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule] ) throws -> Project? { - guard !projectManifests.isEmpty else { return nil } + guard !projectManifests.isEmpty || packageManifestPath != nil else { return nil } let projectName = "Manifests" let projectPath = sourceRootPath.appending(component: projectName) @@ -247,17 +241,6 @@ final class ProjectEditorMapper: ProjectEditorMapping { let helperTargetDependencies = helpersTarget.map { [TargetDependency.target(name: $0.name)] } ?? [] let helperAndPluginDependencies = helperTargetDependencies + editablePluginTargetDependencies - let dependenciesTarget: Target? = { - guard let dependenciesPath else { return nil } - return editorHelperTarget( - name: "Dependencies", - filesGroup: manifestsFilesGroup, - targetSettings: targetWithLinkedPluginsSettings, - sourcePaths: [dependenciesPath], - dependencies: helperAndPluginDependencies - ) - }() - let packagesTarget: Target? = try { guard let packageManifestPath, let xcode = try XcodeController.shared.selected() @@ -280,7 +263,14 @@ final class ProjectEditorMapper: ProjectEditorMapping { "\(xcode.path.pathString)/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/ManifestAPI", ]), ], - uniquingKeysWith: { $1 } + uniquingKeysWith: { + switch ($0, $1) { + case let (.array(leftArray), .array(rightArray)): + return SettingValue.array(leftArray + rightArray) + default: + return $1 + } + } ) let dependencies: [TargetDependency] = helpersTarget == nil ? [] : [ @@ -316,7 +306,6 @@ final class ProjectEditorMapper: ProjectEditorMapping { resourceSynthesizersTarget, stencilsTarget, configTarget, - dependenciesTarget, packagesTarget, ] .compactMap { $0 } @@ -331,7 +320,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { executable: nil, filePath: tuistPath, arguments: arguments, - diagnosticsOptions: [] + diagnosticsOptions: SchemeDiagnosticsOptions() ) let scheme = Scheme(name: projectName, shared: true, buildAction: buildAction, runAction: runAction) let projectSettings = Settings( @@ -346,6 +335,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { xcodeProjPath: destinationDirectory.appending(component: "\(projectName).xcodeproj"), name: projectName, organizationName: nil, + classPrefix: nil, defaultKnownRegions: nil, developmentRegion: nil, options: .init( @@ -437,6 +427,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { xcodeProjPath: destinationDirectory.appending(component: "\(projectName).xcodeproj"), name: projectName, organizationName: nil, + classPrefix: nil, defaultKnownRegions: nil, developmentRegion: nil, options: .init( diff --git a/Sources/TuistKit/Services/AuthService.swift b/Sources/TuistKit/Services/AuthService.swift new file mode 100644 index 00000000000..1ba927cbb0e --- /dev/null +++ b/Sources/TuistKit/Services/AuthService.swift @@ -0,0 +1,99 @@ +import Foundation +import Mockable +import Path +import TuistCore +import TuistLoader +import TuistServer +import TuistSupport + +@Mockable +protocol AuthServicing: AnyObject { + func authenticate( + email: String?, + password: String?, + directory: String? + ) async throws +} + +final class AuthService: AuthServicing { + private let serverSessionController: ServerSessionControlling + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + private let userInputReader: UserInputReading + private let authenticateService: AuthenticateServicing + private let serverCredentialsStore: ServerCredentialsStoring + + init( + serverSessionController: ServerSessionControlling = ServerSessionController(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader(), + userInputReader: UserInputReading = UserInputReader(), + authenticateService: AuthenticateServicing = AuthenticateService(), + serverCredentialsStore: ServerCredentialsStoring = ServerCredentialsStore() + ) { + self.serverSessionController = serverSessionController + self.serverURLService = serverURLService + self.configLoader = configLoader + self.userInputReader = userInputReader + self.authenticateService = authenticateService + self.serverCredentialsStore = serverCredentialsStore + } + + // MARK: - AuthServicing + + func authenticate( + email: String?, + password: String?, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + if email != nil || password != nil { + try await authenticateWithEmailAndPassword( + email: email, + password: password, + serverURL: serverURL + ) + } else { + try await authenticateWithBrowserLogin(serverURL: serverURL) + } + } + + private func authenticateWithEmailAndPassword( + email: String?, + password: String?, + serverURL: URL + ) async throws { + let email = email ?? userInputReader.readString(asking: "Email:") + let password = password ?? userInputReader.readString(asking: "Password:") + + let authenticationTokens = try await authenticateService.authenticate( + email: email, + password: password, + serverURL: serverURL + ) + + try serverCredentialsStore.store( + credentials: ServerCredentials( + token: nil, + accessToken: authenticationTokens.accessToken, + refreshToken: authenticationTokens.refreshToken + ), + serverURL: serverURL + ) + logger.notice("Credentials stored successfully.", metadata: .success) + } + + private func authenticateWithBrowserLogin( + serverURL: URL + ) async throws { + try await serverSessionController.authenticate(serverURL: serverURL) + } +} diff --git a/Sources/TuistKit/Services/BuildService.swift b/Sources/TuistKit/Services/BuildService.swift index 0de0083b515..2587eeaeea3 100644 --- a/Sources/TuistKit/Services/BuildService.swift +++ b/Sources/TuistKit/Services/BuildService.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic +import Path import TuistAutomation import TuistCore -import TuistGraph import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph enum BuildServiceError: FatalError { case workspaceNotFound(path: String) @@ -33,42 +34,54 @@ enum BuildServiceError: FatalError { } } -final class BuildService { +public final class BuildService { private let generatorFactory: GeneratorFactorying + private let cacheStorageFactory: CacheStorageFactorying private let buildGraphInspector: BuildGraphInspecting private let targetBuilder: TargetBuilding private let configLoader: ConfigLoading - init( - generatorFactory: GeneratorFactorying = GeneratorFactory(), + public init( + generatorFactory: GeneratorFactorying, + cacheStorageFactory: CacheStorageFactorying, buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), targetBuilder: TargetBuilding = TargetBuilder(), configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()) ) { self.generatorFactory = generatorFactory + self.cacheStorageFactory = cacheStorageFactory self.buildGraphInspector = buildGraphInspector self.targetBuilder = targetBuilder self.configLoader = configLoader } // swiftlint:disable:next function_body_length - func run( + public func run( schemeName: String?, generate: Bool, clean: Bool, configuration: String?, + ignoreBinaryCache: Bool, buildOutputPath: AbsolutePath?, derivedDataPath: String?, path: AbsolutePath, device: String?, - platform: String?, + platform: Platform?, osVersion: String?, rosetta: Bool, - generateOnly: Bool + generateOnly: Bool, + generator _: ((Config) throws -> Generating)? = nil, + passthroughXcodeBuildArguments: [String] ) async throws { let graph: Graph - let config = try configLoader.loadConfig(path: path) - let generator = generatorFactory.default(config: config) + let config = try await configLoader.loadConfig(path: path) + let cacheStorage = try cacheStorageFactory.cacheStorage(config: config) + let generator = generatorFactory.building( + config: config, + configuration: configuration, + ignoreBinaryCache: ignoreBinaryCache, + cacheStorage: cacheStorage + ) if try (generate || buildGraphInspector.workspacePath(directory: path) == nil) { graph = try await generator.generateWithGraph(path: path).1 } else { @@ -107,10 +120,10 @@ final class BuildService { throw TargetBuilderError.schemeWithoutBuildableTargets(scheme: scheme.name) } - let buildPlatform: TuistGraph.Platform + let buildPlatform: XcodeGraph.Platform - if let platform, let inputPlatform = TuistGraph.Platform(rawValue: platform) { - buildPlatform = inputPlatform + if let platform { + buildPlatform = platform } else { buildPlatform = try graphTarget.target.servicePlatform } @@ -125,9 +138,10 @@ final class BuildService { buildOutputPath: buildOutputPath, derivedDataPath: derivedDataPath, device: device, - osVersion: osVersion?.version(), + osVersion: osVersion?.version().map { .init(stringLiteral: $0.description) }, rosetta: rosetta, - graphTraverser: graphTraverser + graphTraverser: graphTraverser, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) } else { var cleaned = false @@ -138,10 +152,10 @@ final class BuildService { throw TargetBuilderError.schemeWithoutBuildableTargets(scheme: scheme.name) } - let buildPlatform: TuistGraph.Platform + let buildPlatform: XcodeGraph.Platform - if let platform, let inputPlatform = TuistGraph.Platform(rawValue: platform) { - buildPlatform = inputPlatform + if let platform { + buildPlatform = platform } else { buildPlatform = try graphTarget.target.servicePlatform } @@ -156,9 +170,10 @@ final class BuildService { buildOutputPath: buildOutputPath, derivedDataPath: derivedDataPath, device: device, - osVersion: osVersion?.version(), + osVersion: osVersion?.version().map { .init(stringLiteral: $0.description) }, rosetta: rosetta, - graphTraverser: graphTraverser + graphTraverser: graphTraverser, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) cleaned = true } diff --git a/Sources/TuistKit/Services/CleanService.swift b/Sources/TuistKit/Services/CleanService.swift index 860dcbe9b9b..01113df7e3d 100644 --- a/Sources/TuistKit/Services/CleanService.swift +++ b/Sources/TuistKit/Services/CleanService.swift @@ -1,76 +1,145 @@ +import FileSystem import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph -final class CleanService { - private let cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring - init( - cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring = CacheDirectoriesProviderFactory() - ) { - self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory +enum TuistCleanCategory: ExpressibleByArgument, CaseIterable, Equatable { + static let allCases = CacheCategory.allCases + .map { .global($0) } + [Self.dependencies] + + static var allValueStrings: [String] { + TuistCleanCategory.allCases.map(\.defaultValueDescription) } - func run( - categories: [CleanCategory], - path: String? - ) throws { - let path: AbsolutePath = try self.path(path) - let manifestLoaderFactory = ManifestLoaderFactory() - let manifestLoader = manifestLoaderFactory.createManifestLoader() - let configLoader = ConfigLoader(manifestLoader: manifestLoader) - let config = try configLoader.loadConfig(path: path) - let cacheDirectoryProvider = try cacheDirectoryProviderFactory.cacheDirectories(config: config) + /// The local global cache + case global(CacheCategory) - for category in categories { - switch category { - case let .global(cacheCategory): - try cleanCacheCategory( - cacheCategory, - cacheDirectoryProvider: cacheDirectoryProvider - ) - case .dependencies: - try cleanDependencies(at: path) - } + /// The local dependencies cache + case dependencies + + var defaultValueDescription: String { + switch self { + case let .global(cacheCategory): + return cacheCategory.rawValue + case .dependencies: + return "dependencies" } } - // MARK: - Helpers - - private func path(_ path: String?) throws -> AbsolutePath { - if let path { - return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + init?(argument: String) { + if let cacheCategory = CacheCategory(rawValue: argument) { + self = .global(cacheCategory) + } else if argument == "dependencies" { + self = .dependencies } else { - return FileHandler.shared.currentPath + return nil } } - private func cleanCacheCategory( - _ cacheCategory: CacheCategory, - cacheDirectoryProvider: CacheDirectoriesProviding - ) throws { - let directory = cacheDirectoryProvider.cacheDirectory(for: cacheCategory) - if FileHandler.shared.exists(directory) { - try FileHandler.shared.delete(directory) - logger.info("Successfully cleaned artifacts at path \(directory.pathString)", metadata: .success) + func directory( + packageDirectory: AbsolutePath? + ) throws -> Path.AbsolutePath? { + switch self { + case let .global(category): + return try CacheDirectoriesProvider().cacheDirectory(for: category) + case .dependencies: + return packageDirectory?.appending( + component: Constants.SwiftPackageManager.packageBuildDirectoryName + ) } } +} - private func cleanDependencies(at path: AbsolutePath) throws { - let dependenciesPath = path.appending(components: [Constants.tuistDirectoryName, Constants.DependenciesDirectory.name]) - if FileHandler.shared.exists(dependenciesPath) { - let carthagePath = dependenciesPath.appending(component: Constants.DependenciesDirectory.carthageDirectoryName) - if FileHandler.shared.exists(carthagePath) { - try FileHandler.shared.delete(carthagePath) - } +final class CleanService { + private let fileHandler: FileHandling + private let rootDirectoryLocator: RootDirectoryLocating + private let cacheDirectoriesProvider: CacheDirectoriesProviding + private let manifestFilesLocator: ManifestFilesLocating + private let configLoader: ConfigLoading + private let serverURLService: ServerURLServicing + private let cleanCacheService: CleanCacheServicing + private let fileSystem: FileSystem + + init( + fileHandler: FileHandling, + rootDirectoryLocator: RootDirectoryLocating, + cacheDirectoriesProvider: CacheDirectoriesProviding, + manifestFilesLocator: ManifestFilesLocating, + configLoader: ConfigLoading, + serverURLService: ServerURLServicing, + cleanCacheService: CleanCacheServicing, + fileSystem: FileSystem + ) { + self.fileHandler = fileHandler + self.rootDirectoryLocator = rootDirectoryLocator + self.cacheDirectoriesProvider = cacheDirectoriesProvider + self.manifestFilesLocator = manifestFilesLocator + self.configLoader = configLoader + self.serverURLService = serverURLService + self.cleanCacheService = cleanCacheService + self.fileSystem = fileSystem + } + + public convenience init() { + self.init( + fileHandler: FileHandler.shared, + rootDirectoryLocator: RootDirectoryLocator(), + cacheDirectoriesProvider: CacheDirectoriesProvider(), + manifestFilesLocator: ManifestFilesLocator(), + configLoader: ConfigLoader(), + serverURLService: ServerURLService(), + cleanCacheService: CleanCacheService(), + fileSystem: FileSystem() + ) + } + + func run( + categories: [TuistCleanCategory], + remote: Bool, + path: String? + ) async throws { + let resolvedPath = if let path { + try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + } else { + FileHandler.shared.currentPath + } - let spmPath = dependenciesPath.appending(component: Constants.DependenciesDirectory.swiftPackageManagerDirectoryName) - if FileHandler.shared.exists(spmPath) { - try FileHandler.shared.delete(spmPath) + let packageDirectory = manifestFilesLocator.locatePackageManifest(at: resolvedPath)?.parentDirectory + + for category in categories { + let directory: AbsolutePath? + switch category { + case let .global(category): + directory = try cacheDirectoriesProvider.cacheDirectory(for: category) + case .dependencies: + directory = packageDirectory?.appending( + component: Constants.SwiftPackageManager.packageBuildDirectoryName + ) } + if let directory, + fileHandler.exists(directory) + { + try await fileSystem.remove(directory) + logger.notice("Successfully cleaned artifacts at path \(directory.pathString)", metadata: .success) + } else { + logger.notice("There's nothing to clean for \(category.defaultValueDescription)") + } + } + + if remote { + let config = try await configLoader.loadConfig(path: resolvedPath) + guard let fullHandle = config.fullHandle else { return } + let serverURL = try serverURLService.url(configServerURL: config.url) + try await cleanCacheService.cleanCache( + serverURL: serverURL, + fullHandle: fullHandle + ) + + logger.notice("Successfully cleaned the remote storage.") } - logger.info("Successfully cleaned dependencies at path \(dependenciesPath.pathString)", metadata: .success) } } diff --git a/Sources/TuistKit/Services/DecryptService.swift b/Sources/TuistKit/Services/DecryptService.swift deleted file mode 100644 index 641e8005764..00000000000 --- a/Sources/TuistKit/Services/DecryptService.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistSigning -import TuistSupport - -final class DecryptService { - private let signingCipher: SigningCiphering - - init(signingCipher: SigningCiphering = SigningCipher()) { - self.signingCipher = signingCipher - } - - func run(path: String?) throws { - let path = try self.path(path) - try signingCipher.decryptSigning(at: path, keepFiles: false) - logger.notice("Successfully decrypted all signing files", metadata: .success) - } - - // MARK: - Helpers - - private func path(_ path: String?) throws -> AbsolutePath { - if let path { - return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) - } else { - return FileHandler.shared.currentPath - } - } -} diff --git a/Sources/TuistKit/Services/DumpService.swift b/Sources/TuistKit/Services/DumpService.swift index e14dda10032..ba35e49f33e 100644 --- a/Sources/TuistKit/Services/DumpService.swift +++ b/Sources/TuistKit/Services/DumpService.swift @@ -1,4 +1,5 @@ import Foundation +import Path import TSCBasic import TuistCore import TuistLoader @@ -13,7 +14,7 @@ final class DumpService { } func run(path: String?, manifest: DumpableManifest) async throws { - let projectPath: AbsolutePath + let projectPath: Path.AbsolutePath if let path { projectPath = try AbsolutePath(validating: path, relativeTo: AbsolutePath.current) } else { @@ -30,21 +31,21 @@ final class DumpService { let encoded: Encodable switch manifest { case .project: - encoded = try manifestLoader.loadProject(at: projectPath, rootPath: nil) + encoded = try await manifestLoader.loadProject(at: projectPath, rootPath: nil) case .workspace: - encoded = try manifestLoader.loadWorkspace(at: projectPath) + encoded = try await manifestLoader.loadWorkspace(at: projectPath) case .config: - encoded = try manifestLoader.loadConfig(at: projectPath.appending(component: Constants.tuistDirectoryName)) + encoded = try await manifestLoader.loadConfig(at: projectPath.appending(component: Constants.tuistDirectoryName)) case .template: - encoded = try manifestLoader.loadTemplate(at: projectPath) - case .dependencies: - encoded = try manifestLoader.loadDependencies(at: projectPath) + encoded = try await manifestLoader.loadTemplate(at: projectPath) case .plugin: - encoded = try manifestLoader.loadPlugin(at: projectPath) + encoded = try await manifestLoader.loadPlugin(at: projectPath) + case .package: + encoded = try await manifestLoader.loadPackageSettings(at: projectPath) } let json: JSON = try encoded.toJSON() - logger.notice("\(json.toString(prettyPrint: true))") + logger.notice("\(json.toString(prettyPrint: true))", metadata: .json) } } @@ -53,6 +54,6 @@ enum DumpableManifest: String, CaseIterable { case workspace case config case template - case dependencies case plugin + case package } diff --git a/Sources/TuistKit/Services/EditService.swift b/Sources/TuistKit/Services/EditService.swift index b94493e9d5b..499405f11e2 100644 --- a/Sources/TuistKit/Services/EditService.swift +++ b/Sources/TuistKit/Services/EditService.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic +import Path +import TuistCore import TuistGenerator -import TuistGraph import TuistLoader import TuistPlugin import TuistSupport +import XcodeGraph enum EditServiceError: FatalError { case xcodeNotSelected @@ -29,22 +30,20 @@ final class EditService { private let opener: Opening private let configLoader: ConfigLoading private let pluginService: PluginServicing - private let signalHandler: SignalHandling - - private static var temporaryDirectory: AbsolutePath? + private let cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring init( projectEditor: ProjectEditing = ProjectEditor(), opener: Opening = Opener(), configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()), pluginService: PluginServicing = PluginService(), - signalHandler: SignalHandling = SignalHandler() + cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring = CacheDirectoriesProviderFactory() ) { self.projectEditor = projectEditor self.opener = opener self.configLoader = configLoader self.pluginService = pluginService - self.signalHandler = signalHandler + self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory } func run( @@ -56,29 +55,25 @@ final class EditService { let plugins = await loadPlugins(at: path) if !permanent { - try withTemporaryDirectory(removeTreeOnDeinit: true) { generationDirectory in - EditService.temporaryDirectory = generationDirectory + let cacheDirectoryProvider = try cacheDirectoryProviderFactory.cacheDirectories() + let cacheDirectory = try cacheDirectoryProvider.cacheDirectory(for: .editProjects) + let cachedManifestDirectory = cacheDirectory.appending(component: path.pathString.md5) - signalHandler.trap { _ in - try? EditService.temporaryDirectory.map(FileHandler.shared.delete) - exit(0) - } + guard let selectedXcode = try XcodeController.shared.selected() else { + throw EditServiceError.xcodeNotSelected + } - guard let selectedXcode = try XcodeController.shared.selected() else { - throw EditServiceError.xcodeNotSelected - } + let workspacePath = try await projectEditor.edit( + at: path, + in: cachedManifestDirectory, + onlyCurrentDirectory: onlyCurrentDirectory, + plugins: plugins + ) + logger.notice("Opening Xcode to edit the project.", metadata: .pretty) + try opener.open(path: workspacePath, application: selectedXcode.path, wait: false) - let workspacePath = try projectEditor.edit( - at: path, - in: generationDirectory, - onlyCurrentDirectory: onlyCurrentDirectory, - plugins: plugins - ) - logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing") - try opener.open(path: workspacePath, application: selectedXcode.path, wait: true) - } } else { - let workspacePath = try projectEditor.edit( + let workspacePath = try await projectEditor.edit( at: path, in: path, onlyCurrentDirectory: onlyCurrentDirectory, @@ -99,7 +94,7 @@ final class EditService { } private func loadPlugins(at path: AbsolutePath) async -> Plugins { - guard let config = try? configLoader.loadConfig(path: path) else { + guard let config = try? await configLoader.loadConfig(path: path) else { logger.warning("Unable to load Config.swift, fix any compiler errors and re-run for plugins to be loaded.") return .none } diff --git a/Sources/TuistKit/Services/EncryptService.swift b/Sources/TuistKit/Services/EncryptService.swift deleted file mode 100644 index a39e333f18c..00000000000 --- a/Sources/TuistKit/Services/EncryptService.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistSigning -import TuistSupport - -final class EncryptService { - private let signingCipher: SigningCiphering - - init(signingCipher: SigningCiphering = SigningCipher()) { - self.signingCipher = signingCipher - } - - func run(path: String?) throws { - let path = try self.path(path) - try signingCipher.encryptSigning(at: path, keepFiles: false) - - logger.notice("Successfully encrypted all signing files", metadata: .success) - } - - // MARK: - Helpers - - private func path(_ path: String?) throws -> AbsolutePath { - if let path { - return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) - } else { - return FileHandler.shared.currentPath - } - } -} diff --git a/Sources/TuistKit/Services/FetchService.swift b/Sources/TuistKit/Services/FetchService.swift deleted file mode 100644 index ffc7ce08f43..00000000000 --- a/Sources/TuistKit/Services/FetchService.swift +++ /dev/null @@ -1,167 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistDependencies -import TuistGraph -import TuistLoader -import TuistPlugin -import TuistSupport - -final class FetchService { - private let pluginService: PluginServicing - private let configLoader: ConfigLoading - 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(), - configLoader: ConfigLoading = ConfigLoader(manifestLoader: CachedManifestLoader()), - manifestLoader: ManifestLoading = ManifestLoader(), - dependenciesController: DependenciesControlling = DependenciesController(), - dependenciesModelLoader: DependenciesModelLoading = DependenciesModelLoader(), - 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 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) - } else { - return currentPath - } - } - - private var currentPath: AbsolutePath { - fileHandler.currentPath - } - - private func fetchPlugins(path: AbsolutePath) async throws -> TuistGraph.Plugins { - logger.info("Resolving and fetching plugins.", metadata: .section) - - let config = try configLoader.loadConfig(path: path) - let plugins = try await pluginService.loadPlugins(using: config) - - logger.info("Plugins resolved and fetched successfully.", metadata: .success) - - return plugins - } - - private func fetchDependencies(path: AbsolutePath, update: Bool, with plugins: TuistGraph.Plugins) throws { - try manifestLoader.validateHasProjectOrWorkspaceManifest(at: path) - - 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 - } - - if update { - logger.info("Updating dependencies.", metadata: .section) - } else { - logger.info("Resolving and fetching dependencies.", metadata: .section) - } - - let config = try configLoader.loadConfig(path: path) - let swiftVersion = config.swiftVersion - - let dependenciesManifest: TuistCore.DependenciesGraph - 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 { - 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) - - try dependenciesController.save( - dependenciesGraph: dependenciesGraph, - to: path - ) - - if update { - logger.info("Dependencies updated successfully.", metadata: .success) - } else { - logger.info("Dependencies resolved and fetched successfully.", metadata: .success) - } - } -} diff --git a/Sources/TuistKit/Services/GenerateService.swift b/Sources/TuistKit/Services/GenerateService.swift index 0993e77ce6f..2fab3fc3e76 100644 --- a/Sources/TuistKit/Services/GenerateService.swift +++ b/Sources/TuistKit/Services/GenerateService.swift @@ -1,47 +1,61 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistGenerator -import TuistGraph import TuistLoader import TuistPlugin +import TuistServer import TuistSupport +import XcodeGraph final class GenerateService { private let opener: Opening private let clock: Clock private let timeTakenLoggerFormatter: TimeTakenLoggerFormatting + private let cacheStorageFactory: CacheStorageFactorying private let generatorFactory: GeneratorFactorying private let manifestLoader: ManifestLoading private let pluginService: PluginServicing private let configLoader: ConfigLoading init( + cacheStorageFactory: CacheStorageFactorying, + generatorFactory: GeneratorFactorying, clock: Clock = WallClock(), timeTakenLoggerFormatter: TimeTakenLoggerFormatting = TimeTakenLoggerFormatter(), manifestLoader: ManifestLoading = ManifestLoader(), opener: Opening = Opener(), - generatorFactory: GeneratorFactorying = GeneratorFactory(), pluginService: PluginServicing = PluginService(), configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()) ) { + self.generatorFactory = generatorFactory + self.cacheStorageFactory = cacheStorageFactory self.clock = clock self.timeTakenLoggerFormatter = timeTakenLoggerFormatter self.manifestLoader = manifestLoader self.opener = opener - self.generatorFactory = generatorFactory self.pluginService = pluginService self.configLoader = configLoader } func run( path: String?, - noOpen: Bool + sources: Set, + noOpen: Bool, + configuration: String?, + ignoreBinaryCache: Bool ) async throws { let timer = clock.startTimer() let path = try self.path(path) - let config = try configLoader.loadConfig(path: path) - let generator = generatorFactory.default(config: config) + let config = try await configLoader.loadConfig(path: path) + let cacheStorage = try cacheStorageFactory.cacheStorage(config: config) + let generator = generatorFactory.generation( + config: config, + sources: sources, + configuration: configuration, + ignoreBinaryCache: ignoreBinaryCache, + cacheStorage: cacheStorage + ) let workspacePath = try await generator.generate(path: path) if !noOpen { try opener.open(path: workspacePath) diff --git a/Sources/TuistKit/Services/GraphService.swift b/Sources/TuistKit/Services/GraphService.swift index f57268bf38a..11b323922bb 100644 --- a/Sources/TuistKit/Services/GraphService.swift +++ b/Sources/TuistKit/Services/GraphService.swift @@ -1,19 +1,21 @@ import DOT +import FileSystem import Foundation import GraphViz +import Path import ProjectAutomation import Tools -import TSCBasic import TuistCore import TuistGenerator -import TuistGraph import TuistLoader import TuistPlugin import TuistSupport +import XcodeGraph final class GraphService { private let graphVizMapper: GraphToGraphVizMapping private let manifestGraphLoader: ManifestGraphLoading + private let fileSystem: FileSystem convenience init() { let manifestLoader = ManifestLoaderFactory() @@ -32,10 +34,12 @@ final class GraphService { init( graphVizGenerator: GraphToGraphVizMapping, - manifestGraphLoader: ManifestGraphLoading + manifestGraphLoader: ManifestGraphLoading, + fileSystem: FileSystem = FileSystem() ) { graphVizMapper = graphVizGenerator self.manifestGraphLoader = manifestGraphLoader + self.fileSystem = fileSystem } func run( @@ -49,12 +53,12 @@ final class GraphService { path: AbsolutePath, outputPath: AbsolutePath ) async throws { - let (graph, _, _) = try await manifestGraphLoader.load(path: path) + let (graph, _, _, _) = try await manifestGraphLoader.load(path: path) let filePath = outputPath.appending(component: "graph.\(format.rawValue)") if FileHandler.shared.exists(filePath) { logger.notice("Deleting existing graph at \(filePath.pathString)") - try FileHandler.shared.delete(filePath) + try await fileSystem.remove(filePath) } let filteredTargetsAndDependencies = graph.filter( @@ -129,162 +133,3 @@ final class GraphService { try System.shared.runAndPrint(["brew", "install", "graphviz"], verbose: false, environment: env) } } - -private enum GraphServiceError: FatalError { - case jsonNotValidForVisualExport - case encodingError(String) - - var description: String { - switch self { - case .jsonNotValidForVisualExport: - return "json format is not valid for visual export" - case let .encodingError(format): - return "failed to encode graph to \(format)" - } - } - - var type: ErrorType { - switch self { - case .jsonNotValidForVisualExport: - return .abort - case .encodingError: - return .abort - } - } -} - -extension ProjectAutomation.Graph { - fileprivate static func from( - graph: TuistGraph.Graph, - targetsAndDependencies: [GraphTarget: Set] - ) -> ProjectAutomation.Graph { - // generate targets projects only - let projects = targetsAndDependencies - .map(\.key.project) - .reduce(into: [String: ProjectAutomation.Project]()) { - $0[$1.path.pathString] = ProjectAutomation.Project.from($1) - } - - return ProjectAutomation.Graph(name: graph.name, path: graph.path.pathString, projects: projects) - } - - fileprivate func export(to filePath: AbsolutePath) throws { - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes] - let jsonData = try encoder.encode(self) - let jsonString = String(data: jsonData, encoding: .utf8) - guard let jsonString else { - throw GraphServiceError.encodingError(GraphFormat.json.rawValue) - } - - try FileHandler.shared.write(jsonString, path: filePath, atomically: true) - } -} - -extension ProjectAutomation.Project { - fileprivate static func from(_ project: TuistGraph.Project) -> ProjectAutomation.Project { - let packages = project.packages - .reduce(into: [ProjectAutomation.Package]()) { $0.append(ProjectAutomation.Package.from($1)) } - let schemes = project.schemes.reduce(into: [ProjectAutomation.Scheme]()) { $0.append(ProjectAutomation.Scheme.from($1)) } - let targets = project.targets.reduce(into: [ProjectAutomation.Target]()) { $0.append(ProjectAutomation.Target.from($1)) } - - return ProjectAutomation.Project( - name: project.name, - path: project.path.pathString, - isExternal: project.isExternal, - packages: packages, - targets: targets, - schemes: schemes - ) - } -} - -extension ProjectAutomation.Package { - fileprivate static func from(_ package: TuistGraph.Package) -> ProjectAutomation.Package { - switch package { - case let .remote(url, _): - return ProjectAutomation.Package(kind: ProjectAutomation.Package.PackageKind.remote, path: url) - case let .local(path): - return ProjectAutomation.Package(kind: ProjectAutomation.Package.PackageKind.local, path: path.pathString) - } - } -} - -extension ProjectAutomation.Target { - fileprivate static func from(_ target: TuistGraph.Target) -> ProjectAutomation.Target { - let dependencies = target.dependencies.map { Self.from($0) } - return ProjectAutomation.Target( - name: target.name, - product: target.product.rawValue, - sources: target.sources.map(\.path.pathString), - resources: target.resources.map(\.path.pathString), - dependencies: dependencies - ) - } - - private static func from(_ dependency: TuistGraph.TargetDependency) -> ProjectAutomation.TargetDependency { - switch dependency { - case let .target(name, _): - return .target(name: name) - case let .project(target, path, _): - return .project(target: target, path: path.pathString) - case let .framework(path, status, _): - let frameworkStatus: ProjectAutomation.FrameworkStatus - switch status { - case .optional: - frameworkStatus = .optional - case .required: - frameworkStatus = .required - } - return .framework(path: path.pathString, status: frameworkStatus) - case let .xcframework(path, status, _): - let frameworkStatus: ProjectAutomation.FrameworkStatus - switch status { - case .optional: - frameworkStatus = .optional - case .required: - frameworkStatus = .required - } - return .xcframework(path: path.pathString, status: frameworkStatus) - case let .library(path, publicHeaders, swiftModuleMap, _): - return .library( - path: path.pathString, - publicHeaders: publicHeaders.pathString, - swiftModuleMap: swiftModuleMap?.pathString - ) - case let .package(product, type, _): - switch type { - case .macro: - return .packageMacro(product: product) - case .plugin: - return .packagePlugin(product: product) - case .runtime: - return .package(product: product) - } - case let .sdk(name, status, _): - let projectAutomationStatus: ProjectAutomation.SDKStatus - switch status { - case .optional: - projectAutomationStatus = .optional - case .required: - projectAutomationStatus = .required - } - return .sdk(name: name, status: projectAutomationStatus) - case .xctest: - return .xctest - } - } -} - -extension ProjectAutomation.Scheme { - fileprivate static func from(_ scheme: TuistGraph.Scheme) -> ProjectAutomation.Scheme { - var testTargets = [String]() - if let testAction = scheme.testAction { - for testTarget in testAction.targets { - testTargets.append(testTarget.target.name) - } - } - - return ProjectAutomation.Scheme(name: scheme.name, testActionTargets: testTargets) - } -} diff --git a/Sources/TuistKit/Services/InitService.swift b/Sources/TuistKit/Services/InitService.swift index c7e74e608c0..3267c205c15 100644 --- a/Sources/TuistKit/Services/InitService.swift +++ b/Sources/TuistKit/Services/InitService.swift @@ -1,9 +1,9 @@ -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader import TuistScaffold import TuistSupport +import XcodeGraph enum InitServiceError: FatalError, Equatable { case ungettableProjectName(AbsolutePath) @@ -44,24 +44,27 @@ class InitService { private let templatesDirectoryLocator: TemplatesDirectoryLocating private let templateGenerator: TemplateGenerating private let templateGitLoader: TemplateGitLoading + private let tuistVersionLoader: TuistVersionLoading init( templateLoader: TemplateLoading = TemplateLoader(), templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(), templateGenerator: TemplateGenerating = TemplateGenerator(), - templateGitLoader: TemplateGitLoading = TemplateGitLoader() + templateGitLoader: TemplateGitLoading = TemplateGitLoader(), + tuistVersionLoader: TuistVersionLoading = TuistVersionLoader() ) { self.templateLoader = templateLoader self.templatesDirectoryLocator = templatesDirectoryLocator self.templateGenerator = templateGenerator self.templateGitLoader = templateGitLoader + self.tuistVersionLoader = tuistVersionLoader } func loadTemplateOptions( name: String, templateName: String, path: String? - ) throws -> ( + ) async throws -> ( required: [String], optional: [String] ) { @@ -70,7 +73,7 @@ class InitService { var attributes: [Template.Attribute] = [] if templateName.isGitURL { - try templateGitLoader.loadTemplate(from: templateName, templateName: name) { template in + try await templateGitLoader.loadTemplate(from: templateName, templateName: name) { template in attributes = template.attributes } } else { @@ -79,7 +82,7 @@ class InitService { template: templateName ) - let template = try templateLoader.loadTemplate(at: templateDirectory) + let template = try await templateLoader.loadTemplate(at: templateDirectory, plugins: .none) attributes = template.attributes } @@ -106,24 +109,26 @@ class InitService { templateName: String?, requiredTemplateOptions: [String: String], optionalTemplateOptions: [String: String?] - ) throws { + ) async throws { let platform = try self.platform(platform) let path = try self.path(path) let name = try self.name(name, path: path) let templateName = templateName ?? "default" + let tuistVersion = try tuistVersionLoader.getVersion() try verifyDirectoryIsEmpty(path: path) if templateName.isGitURL { - try templateGitLoader.loadTemplate(from: templateName, templateName: name, closure: { template in - let parsedAttributes = try parseAttributes( + try await templateGitLoader.loadTemplate(from: templateName, templateName: name, closure: { template in + let parsedAttributes = try self.parseAttributes( name: name, platform: platform, + tuistVersion: tuistVersion, requiredTemplateOptions: requiredTemplateOptions, optionalTemplateOptions: optionalTemplateOptions, template: template ) - try templateGenerator.generate( + try await self.templateGenerator.generate( template: template, to: path, attributes: parsedAttributes @@ -134,23 +139,31 @@ class InitService { guard let templateDirectory = directories.first(where: { $0.basename == templateName }) else { throw InitServiceError.templateNotFound(templateName) } - let template = try templateLoader.loadTemplate(at: templateDirectory) + let template = try await templateLoader.loadTemplate(at: templateDirectory, plugins: .none) let parsedAttributes = try parseAttributes( name: name, platform: platform, + tuistVersion: tuistVersion, requiredTemplateOptions: requiredTemplateOptions, optionalTemplateOptions: optionalTemplateOptions, template: template ) - try templateGenerator.generate( + try await templateGenerator.generate( template: template, to: path, attributes: parsedAttributes ) } - logger.notice("Project generated at path \(path.pathString).", metadata: .success) + logger.notice( + "Project generated at path \(path.pathString). Run `tuist generate` to generate the project and open it in Xcode. Use `tuist edit` to easily update the Tuist project definition.", + metadata: .success + ) + logger + .info( + "To learn more about tuist features, such as how to add external dependencies or how to use our ProjectDescription helpers, head to our tutorials page: https://docs.tuist.io/tutorials/tuist-tutorials" + ) } // MARK: - Helpers @@ -171,20 +184,26 @@ class InitService { private func parseAttributes( name: String, platform: Platform, + tuistVersion: String, requiredTemplateOptions: [String: String], optionalTemplateOptions: [String: String?], template: Template - ) throws -> [String: String] { - let defaultAttributes = ["name": name, "platform": platform.caseValue] + ) throws -> [String: Template.Attribute.Value] { + let defaultAttributes: [String: Template.Attribute.Value] = [ + "name": .string(name), + "platform": .string(platform.caseValue), + "tuist_version": .string(tuistVersion), + "class_name": .string(name.toValidSwiftIdentifier()), + "bundle_identifier": .string(name.toValidInBundleIdentifier()), + ] return try template.attributes.reduce(into: defaultAttributes) { attributesDictionary, attribute in - if attribute.name == "name" || attribute.name == "platform" { - return - } + if defaultAttributes.keys.contains(attribute.name) { return } + switch attribute { case let .required(name): guard let option = requiredTemplateOptions[name] else { throw ScaffoldServiceError.attributeNotProvided(name) } - attributesDictionary[name] = option + attributesDictionary[name] = .string(option) case let .optional(name, default: defaultValue): guard let unwrappedOption = optionalTemplateOptions[name], let option = unwrappedOption @@ -192,7 +211,7 @@ class InitService { attributesDictionary[name] = defaultValue return } - attributesDictionary[name] = option + attributesDictionary[name] = .string(option) } } } @@ -208,16 +227,15 @@ class InitService { return templateDirectory } + /// Returns name to use. If `name` is nil, returns a directory name executed `init` command. private func name(_ name: String?, path: AbsolutePath) throws -> String { - let initName: String if let name { - initName = name - } else if let name = path.components.last { - initName = name + return name + } else if let directoryName = path.components.last { + return directoryName } else { throw InitServiceError.ungettableProjectName(AbsolutePath.current) } - return initName.camelized.uppercasingFirst } private func path(_ path: String?) throws -> AbsolutePath { diff --git a/Sources/TuistKit/Services/InstallService.swift b/Sources/TuistKit/Services/InstallService.swift new file mode 100644 index 00000000000..a7af956dc06 --- /dev/null +++ b/Sources/TuistKit/Services/InstallService.swift @@ -0,0 +1,79 @@ +import Foundation +import Path +import TuistCore +import TuistDependencies +import TuistLoader +import TuistPlugin +import TuistSupport +import XcodeGraph + +final class InstallService { + private let pluginService: PluginServicing + private let configLoader: ConfigLoading + private let swiftPackageManagerController: SwiftPackageManagerControlling + private let fileHandler: FileHandling + private let manifestFilesLocator: ManifestFilesLocating + + init( + pluginService: PluginServicing = PluginService(), + configLoader: ConfigLoading = ConfigLoader(manifestLoader: CachedManifestLoader()), + swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController( + system: System.shared, + fileHandler: FileHandler.shared + ), + fileHandler: FileHandling = FileHandler.shared, + manifestFilesLocator: ManifestFilesLocating = ManifestFilesLocator() + ) { + self.pluginService = pluginService + self.configLoader = configLoader + self.swiftPackageManagerController = swiftPackageManagerController + self.fileHandler = fileHandler + self.manifestFilesLocator = manifestFilesLocator + } + + func run( + path: String?, + update: Bool + ) async throws { + let path = try self.path(path) + + try await fetchPlugins(path: path) + try fetchDependencies(path: path, update: update) + } + + // MARK: - Helpers + + private func path(_ path: String?) throws -> AbsolutePath { + if let path { + return try AbsolutePath(validating: path, relativeTo: fileHandler.currentPath) + } else { + return fileHandler.currentPath + } + } + + private func fetchPlugins(path: AbsolutePath) async throws { + logger.notice("Resolving and fetching plugins.", metadata: .section) + + let config = try await configLoader.loadConfig(path: path) + _ = try await pluginService.loadPlugins(using: config) + + logger.notice("Plugins resolved and fetched successfully.", metadata: .success) + } + + private func fetchDependencies(path: AbsolutePath, update: Bool) throws { + guard let packageManifestPath = manifestFilesLocator.locatePackageManifest(at: path) + else { + return + } + + if update { + logger.notice("Updating dependencies.", metadata: .section) + + try swiftPackageManagerController.update(at: packageManifestPath.parentDirectory, printOutput: true) + } else { + logger.notice("Resolving and fetching dependencies.", metadata: .section) + + try swiftPackageManagerController.resolve(at: packageManifestPath.parentDirectory, printOutput: true) + } + } +} diff --git a/Sources/TuistKit/Services/ListService.swift b/Sources/TuistKit/Services/ListService.swift index b4b31e7540b..ad2f8ded48e 100644 --- a/Sources/TuistKit/Services/ListService.swift +++ b/Sources/TuistKit/Services/ListService.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistLoader import TuistPlugin import TuistScaffold import TuistSupport +import XcodeGraph class ListService { // MARK: - OutputFormat @@ -36,13 +37,12 @@ class ListService { let plugins = try await loadPlugins(at: path) let templateDirectories = try locateTemplateDirectories(at: path, plugins: plugins) - let templates: [PrintableTemplate] = try templateDirectories.map { path in - let template = try templateLoader.loadTemplate(at: path) + let templates: [PrintableTemplate] = try await templateDirectories.concurrentMap { path in + let template = try await self.templateLoader.loadTemplate(at: path, plugins: plugins) return PrintableTemplate(name: path.basename, description: template.description) } - let output = try string(for: templates, in: format) - logger.info("\(output)") + try output(for: templates, in: format) } // MARK: - Helpers @@ -55,26 +55,26 @@ class ListService { } } - private func string( + private func output( for templates: [PrintableTemplate], in format: ListService.OutputFormat - ) throws -> String { + ) throws { switch format { case .table: let textTable = TextTable { [ TextTable.Column(title: "Name", value: $0.name), TextTable.Column(title: "Description", value: $0.description), ] } - return textTable.render(templates) + logger.notice("\(textTable.render(templates))") case .json: let json = try templates.toJSON() - return json.toString(prettyPrint: true) + logger.notice("\(json.toString(prettyPrint: true))", metadata: .json) } } private func loadPlugins(at path: AbsolutePath) async throws -> Plugins { - let config = try configLoader.loadConfig(path: path) + let config = try await configLoader.loadConfig(path: path) return try await pluginService.loadPlugins(using: config) } diff --git a/Sources/TuistKit/Services/LogoutService.swift b/Sources/TuistKit/Services/LogoutService.swift new file mode 100644 index 00000000000..ab7b5713481 --- /dev/null +++ b/Sources/TuistKit/Services/LogoutService.swift @@ -0,0 +1,44 @@ +import Foundation +import Path +import TuistCore +import TuistLoader +import TuistServer +import TuistSupport + +protocol LogoutServicing: AnyObject { + /// It removes any session associated to that domain from + /// the keychain + func logout( + directory: String? + ) async throws +} + +final class LogoutService: LogoutServicing { + private let serverSessionController: ServerSessionControlling + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + serverSessionController: ServerSessionControlling = ServerSessionController(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.serverSessionController = serverSessionController + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func logout( + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + try await serverSessionController.logout(serverURL: serverURL) + } +} diff --git a/Sources/TuistKit/Services/Migration/MigrationCheckEmptyBuildSettingsService.swift b/Sources/TuistKit/Services/Migration/MigrationCheckEmptyBuildSettingsService.swift index f996a701934..08cd799cae7 100644 --- a/Sources/TuistKit/Services/Migration/MigrationCheckEmptyBuildSettingsService.swift +++ b/Sources/TuistKit/Services/Migration/MigrationCheckEmptyBuildSettingsService.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistMigration import TuistSupport diff --git a/Sources/TuistKit/Services/Migration/MigrationSettingsToXCConfigService.swift b/Sources/TuistKit/Services/Migration/MigrationSettingsToXCConfigService.swift index 93b2dfbe06c..db90fd3418b 100644 --- a/Sources/TuistKit/Services/Migration/MigrationSettingsToXCConfigService.swift +++ b/Sources/TuistKit/Services/Migration/MigrationSettingsToXCConfigService.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistMigration import TuistSupport diff --git a/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift b/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift index 3a1b3447255..70fe5aad208 100644 --- a/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift +++ b/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistMigration import TuistSupport @@ -19,7 +19,7 @@ final class MigrationTargetsByDependenciesService { func run(xcodeprojPath: AbsolutePath) throws { let sortedTargets = try targetsExtractor.targetsSortedByDependencies(xcodeprojPath: xcodeprojPath) let sortedTargetsJson = try makeJson(from: sortedTargets) - logger.info("\(sortedTargetsJson)") + logger.notice("\(sortedTargetsJson)") } private func makeJson(from sortedTargets: [TargetDependencyCount]) throws -> String { diff --git a/Sources/TuistKit/Services/Organization/OrganizationBillingService.swift b/Sources/TuistKit/Services/Organization/OrganizationBillingService.swift new file mode 100644 index 00000000000..2408db43b87 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationBillingService.swift @@ -0,0 +1,47 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationBillingServicing { + func run( + organizationName: String, + directory: String? + ) async throws +} + +final class OrganizationBillingService: OrganizationBillingServicing { + private let serverURLService: ServerURLServicing + private let opener: Opening + private let configLoader: ConfigLoading + + init( + serverURLService: ServerURLServicing = ServerURLService(), + opener: Opening = Opener(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.serverURLService = serverURLService + self.opener = opener + self.configLoader = configLoader + } + + func run( + organizationName: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + try opener.open( + url: serverURL + .appendingPathComponent(organizationName) + .appendingPathComponent("billing") + ) + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationCreateService.swift b/Sources/TuistKit/Services/Organization/OrganizationCreateService.swift new file mode 100644 index 00000000000..1b8f456d20e --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationCreateService.swift @@ -0,0 +1,49 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationCreateServicing { + func run( + organizationName: String, + directory: String? + ) async throws +} + +final class OrganizationCreateService: OrganizationCreateServicing { + private let createOrganizationService: CreateOrganizationServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + createOrganizationService: CreateOrganizationServicing = CreateOrganizationService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.createOrganizationService = createOrganizationService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let organization = try await createOrganizationService.createOrganization( + name: organizationName, + serverURL: serverURL + ) + + logger.info("Tuist organization \(organization.name) was successfully created 🎉") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationDeleteService.swift b/Sources/TuistKit/Services/Organization/OrganizationDeleteService.swift new file mode 100644 index 00000000000..5db43eaf356 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationDeleteService.swift @@ -0,0 +1,49 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationDeleteServicing { + func run( + organizationName: String, + directory: String? + ) async throws +} + +final class OrganizationDeleteService: OrganizationDeleteServicing { + private let deleteOrganizationService: DeleteOrganizationServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + deleteOrganizationService: DeleteOrganizationServicing = DeleteOrganizationService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.deleteOrganizationService = deleteOrganizationService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + try await deleteOrganizationService.deleteOrganization( + name: organizationName, + serverURL: serverURL + ) + + logger.info("Tuist organization \(organizationName) was successfully deleted.") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationInviteService.swift b/Sources/TuistKit/Services/Organization/OrganizationInviteService.swift new file mode 100644 index 00000000000..f9c99ae0483 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationInviteService.swift @@ -0,0 +1,61 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationInviteServicing { + func run( + organizationName: String, + email: String, + directory: String? + ) async throws +} + +final class OrganizationInviteService: OrganizationInviteServicing { + private let createOrganizationInviteService: CreateOrganizationInviteServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + createOrganizationInviteService: CreateOrganizationInviteServicing = CreateOrganizationInviteService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.createOrganizationInviteService = createOrganizationInviteService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + email: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let invitation = try await createOrganizationInviteService.createOrganizationInvite( + organizationName: organizationName, + email: email, + serverURL: serverURL + ) + + let invitationURL = serverURL + .appendingPathComponent("auth") + .appendingPathComponent("invitations") + .appendingPathComponent(invitation.token) + + logger.info(""" + \(invitation.inviteeEmail) was successfully invited to the \(organizationName) organization 🎉 + + You can also share with them the invite link directly: \(invitationURL.absoluteString) + """) + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationListService.swift b/Sources/TuistKit/Services/Organization/OrganizationListService.swift new file mode 100644 index 00000000000..2a69e25cdb1 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationListService.swift @@ -0,0 +1,61 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationListServicing { + func run( + json: Bool, + directory: String? + ) async throws +} + +final class OrganizationListService: OrganizationListServicing { + private let listOrganizationsService: ListOrganizationsServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + listOrganizationsService: ListOrganizationsServicing = ListOrganizationsService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.listOrganizationsService = listOrganizationsService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + json: Bool, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let organizations = try await listOrganizationsService.listOrganizations( + serverURL: serverURL + ) + + if json { + let json = organizations.toJSON() + logger.info(.init(stringLiteral: json.toString(prettyPrint: true)), metadata: .json) + return + } + + if organizations.isEmpty { + logger.info("You currently have no Cloud organizations. Create one by running `tuist organization create`.") + return + } + + let organizationsString = "Listing all your organizations:\n" + organizations.map { " • \($0)" } + .joined(separator: "\n") + logger.info("\(organizationsString)") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationRemoveInviteService.swift b/Sources/TuistKit/Services/Organization/OrganizationRemoveInviteService.swift new file mode 100644 index 00000000000..2baa7abe9c2 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationRemoveInviteService.swift @@ -0,0 +1,52 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationRemoveInviteServicing { + func run( + organizationName: String, + email: String, + directory: String? + ) async throws +} + +final class OrganizationRemoveInviteService: OrganizationRemoveInviteServicing { + private let cancelOrganizationRemoveInviteService: CancelOrganizationInviteServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + cancelOrganizationRemoveInviteService: CancelOrganizationInviteServicing = CancelOrganizationInviteService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.cancelOrganizationRemoveInviteService = cancelOrganizationRemoveInviteService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + email: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + try await cancelOrganizationRemoveInviteService.cancelOrganizationInvite( + organizationName: organizationName, + email: email, + serverURL: serverURL + ) + + logger.info("The invitation for \(email) to the \(organizationName) organization was successfully cancelled.") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationRemoveMemberService.swift b/Sources/TuistKit/Services/Organization/OrganizationRemoveMemberService.swift new file mode 100644 index 00000000000..7f77bd51f5c --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationRemoveMemberService.swift @@ -0,0 +1,52 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationRemoveMemberServicing { + func run( + organizationName: String, + username: String, + directory: String? + ) async throws +} + +final class OrganizationRemoveMemberService: OrganizationRemoveMemberServicing { + private let removeOrganizationMemberService: RemoveOrganizationMemberServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + removeOrganizationMemberService: RemoveOrganizationMemberServicing = RemoveOrganizationMemberService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.removeOrganizationMemberService = removeOrganizationMemberService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + username: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + try await removeOrganizationMemberService.removeOrganizationMember( + organizationName: organizationName, + username: username, + serverURL: serverURL + ) + + logger.info("The member \(username) was successfully removed from the \(organizationName) organization.") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationRemoveSSOService.swift b/Sources/TuistKit/Services/Organization/OrganizationRemoveSSOService.swift new file mode 100644 index 00000000000..f356c4fa97f --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationRemoveSSOService.swift @@ -0,0 +1,50 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationRemoveSSOServicing { + func run( + organizationName: String, + directory: String? + ) async throws +} + +final class OrganizationRemoveSSOService: OrganizationRemoveSSOServicing { + private let updateOrganizationService: UpdateOrganizationServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + updateOrganizationService: UpdateOrganizationServicing = UpdateOrganizationService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.updateOrganizationService = updateOrganizationService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + + let serverURL = try serverURLService.url(configServerURL: config.url) + _ = try await updateOrganizationService.updateOrganization( + organizationName: organizationName, + serverURL: serverURL, + ssoOrganization: nil + ) + + logger.info("SSO for \(organizationName) was removed.") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationShowService.swift b/Sources/TuistKit/Services/Organization/OrganizationShowService.swift new file mode 100644 index 00000000000..e0c4000deda --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationShowService.swift @@ -0,0 +1,150 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationShowServicing { + func run( + organizationName: String, + json: Bool, + directory: String? + ) async throws +} + +final class OrganizationShowService: OrganizationShowServicing { + private let getOrganizationService: GetOrganizationServicing + private let getOrganizationUsageService: GetOrganizationUsageServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + getOrganizationService: GetOrganizationServicing = GetOrganizationService(), + getOrganizationUsageService: GetOrganizationUsageServicing = GetOrganizationUsageService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.getOrganizationService = getOrganizationService + self.getOrganizationUsageService = getOrganizationUsageService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + json: Bool, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let organization = try await getOrganizationService.getOrganization( + organizationName: organizationName, + serverURL: serverURL + ) + + let organizationUsage = try await getOrganizationUsageService.getOrganizationUsage( + organizationName: organizationName, + serverURL: serverURL + ) + + if json { + let json = try organization.toJSON() + logger.info(.init(stringLiteral: json.toString(prettyPrint: true)), metadata: .json) + return + } + + let membersHeaders = ["username", "email", "role"] + let membersTable = formatDataToTable( + [membersHeaders] + organization.members.map { [$0.name, $0.email, $0.role.rawValue] } + ) + + let invitationsString: String + if organization.invitations.isEmpty { + invitationsString = "There are currently no invited users." + } else { + let invitationsHeaders = ["inviter", "invitee email"] + let invitationsTable = formatDataToTable( + [invitationsHeaders] + organization.invitations.map { [$0.inviter.name, $0.inviteeEmail] } + ) + invitationsString = """ + \("Invitations".bold()) (total number: \(organization.invitations.count)) + \(invitationsTable) + """ + } + + var baseInfo = [ + "Organization".bold(), + "Name: \(organization.name)", + "Plan: \(organization.plan.rawValue.capitalized)", + ] + + if let ssoOrganization = organization.ssoOrganization { + switch ssoOrganization { + case let .google(organizationId): + baseInfo.append("SSO: Google (\(organizationId))") + } + } + + logger.info(""" + \(baseInfo.joined(separator: "\n")) + + \("Usage".bold()) (current calendar month) + Remote cache hits: \(organizationUsage.currentMonthRemoteCacheHits) + + \("Organization members".bold()) (total number: \(organization.members.count)) + \(membersTable) + + \(invitationsString) + """) + } + + private func formatDataToTable(_ data: [[String]]) -> String { + guard !data.isEmpty else { + return "" + } + + var tableString = "" + + // Calculate the maximum width of each column + let columnWidths = data[0].indices.map { colIndex -> Int in + ( + data.map { $0[colIndex].count }.max() ?? 0 + ) + 2 + } + + // Format the data into the `tableString` + for (index, row) in data.enumerated() { + for (index, dataPoint) in row.enumerated() { + if index != row.endIndex - 1 { + tableString += dataPoint.paddedToWidth(columnWidths[index]) + } else { + tableString += dataPoint + } + } + if index != data.endIndex - 1 { + tableString += "\n" + } + } + + return tableString + } +} + +extension String { + fileprivate func paddedToWidth(_ width: Int) -> String { + let length = count + guard length < width else { + return self + } + + let spaces = [Character](repeating: " ", count: width - length) + return self + spaces + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationUpdateMemberService.swift b/Sources/TuistKit/Services/Organization/OrganizationUpdateMemberService.swift new file mode 100644 index 00000000000..050326abb01 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationUpdateMemberService.swift @@ -0,0 +1,55 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationUpdateMemberServicing { + func run( + organizationName: String, + username: String, + role: String, + directory: String? + ) async throws +} + +final class OrganizationUpdateMemberService: OrganizationUpdateMemberServicing { + private let updateOrganizationMemberService: UpdateOrganizationMemberServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + updateOrganizationMemberService: UpdateOrganizationMemberServicing = UpdateOrganizationMemberService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.updateOrganizationMemberService = updateOrganizationMemberService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + username: String, + role: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + + let serverURL = try serverURLService.url(configServerURL: config.url) + let member = try await updateOrganizationMemberService.updateOrganizationMember( + organizationName: organizationName, + username: username, + role: ServerOrganization.Member.Role(rawValue: role) ?? .user, + serverURL: serverURL + ) + + logger.info("The member \(username) role was successfully updated to \(member.role.rawValue).") + } +} diff --git a/Sources/TuistKit/Services/Organization/OrganizationUpdateService.swift b/Sources/TuistKit/Services/Organization/OrganizationUpdateService.swift new file mode 100644 index 00000000000..9dd598ff339 --- /dev/null +++ b/Sources/TuistKit/Services/Organization/OrganizationUpdateService.swift @@ -0,0 +1,63 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol OrganizationUpdateSSOServicing { + func run( + organizationName: String, + provider: SSOProvider, + organizationId: String, + directory: String? + ) async throws +} + +final class OrganizationUpdateSSOService: OrganizationUpdateSSOServicing { + private let updateOrganizationService: UpdateOrganizationServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + updateOrganizationService: UpdateOrganizationServicing = UpdateOrganizationService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.updateOrganizationService = updateOrganizationService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + organizationName: String, + provider: SSOProvider, + organizationId: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + + let ssoOrganization: SSOOrganization + switch provider { + case .google: + ssoOrganization = .google(organizationId) + } + + let serverURL = try serverURLService.url(configServerURL: config.url) + _ = try await updateOrganizationService.updateOrganization( + organizationName: organizationName, + serverURL: serverURL, + ssoOrganization: ssoOrganization + ) + + logger + .info( + "\(organizationName) now uses \(provider.rawValue.capitalized) SSO with \(organizationId). Users authenticated with the \(organizationId) SSO organization will automatically have access to the \(organizationName) projects." + ) + } +} diff --git a/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift b/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift index a81027649f9..d1f40a238bf 100644 --- a/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift @@ -1,6 +1,7 @@ +import FileSystem import Foundation +import Path import ProjectDescription -import TSCBasic import TuistDependencies import TuistLoader import TuistSupport @@ -9,18 +10,24 @@ final class PluginArchiveService { private let swiftPackageManagerController: SwiftPackageManagerControlling private let manifestLoader: ManifestLoading private let fileArchiverFactory: FileArchivingFactorying + private let fileSystem: FileSystem init( - swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController(), + swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController( + system: System.shared, + fileHandler: FileHandler.shared + ), manifestLoader: ManifestLoading = ManifestLoader(), - fileArchiverFactory: FileArchivingFactorying = FileArchivingFactory() + fileArchiverFactory: FileArchivingFactorying = FileArchivingFactory(), + fileSystem: FileSystem = FileSystem() ) { self.swiftPackageManagerController = swiftPackageManagerController self.manifestLoader = manifestLoader self.fileArchiverFactory = fileArchiverFactory + self.fileSystem = fileSystem } - func run(path: String?) throws { + func run(path: String?) async throws { let path = try self.path(path) let packageInfo = try swiftPackageManagerController.loadPackageInfo(at: path) @@ -42,10 +49,10 @@ final class PluginArchiveService { return } - let plugin = try manifestLoader.loadPlugin(at: path) + let plugin = try await manifestLoader.loadPlugin(at: path) - try FileHandler.shared.inTemporaryDirectory { temporaryDirectory in - try archiveProducts( + try await FileHandler.shared.inTemporaryDirectory { temporaryDirectory in + try await self.archiveProducts( taskProducts: taskProducts, path: path, plugin: plugin, @@ -69,7 +76,7 @@ final class PluginArchiveService { path: AbsolutePath, plugin: Plugin, in temporaryDirectory: AbsolutePath - ) throws { + ) async throws { let artifactsPath = temporaryDirectory.appending(component: "artifacts") for product in taskProducts { logger.notice("Building \(product)...") @@ -88,13 +95,13 @@ final class PluginArchiveService { let temporaryZipPath = try archiver.zip(name: zipName) let zipPath = path.appending(component: zipName) if FileHandler.shared.exists(zipPath) { - try FileHandler.shared.delete(zipPath) + try await fileSystem.remove(zipPath) } try FileHandler.shared.copy( from: temporaryZipPath, to: zipPath ) - try archiver.delete() + try await archiver.delete() logger.notice( "Plugin was successfully archived. Create a new Github release and attach the file \(zipPath.pathString) as an artifact.", diff --git a/Sources/TuistKit/Services/Plugin/PluginBuildService.swift b/Sources/TuistKit/Services/Plugin/PluginBuildService.swift index 29e0f208a79..b3d512a38c8 100644 --- a/Sources/TuistKit/Services/Plugin/PluginBuildService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginBuildService.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport final class PluginBuildService { diff --git a/Sources/TuistKit/Services/Plugin/PluginRunService.swift b/Sources/TuistKit/Services/Plugin/PluginRunService.swift index 4a8ae101a3b..2d3ef938905 100644 --- a/Sources/TuistKit/Services/Plugin/PluginRunService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginRunService.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport final class PluginRunService { diff --git a/Sources/TuistKit/Services/Plugin/PluginTestService.swift b/Sources/TuistKit/Services/Plugin/PluginTestService.swift index b761322eefc..dad1c14d4f6 100644 --- a/Sources/TuistKit/Services/Plugin/PluginTestService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginTestService.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport final class PluginTestService { diff --git a/Sources/TuistKit/Services/Project/ProjectCreateService.swift b/Sources/TuistKit/Services/Project/ProjectCreateService.swift new file mode 100644 index 00000000000..d7444338a98 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectCreateService.swift @@ -0,0 +1,50 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol ProjectCreateServicing { + func run( + fullHandle: String, + directory: String? + ) async throws +} + +final class ProjectCreateService: ProjectCreateServicing { + private let createProjectService: CreateProjectServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + createProjectService: CreateProjectServicing = CreateProjectService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.createProjectService = createProjectService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + fullHandle: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + + let serverURL = try serverURLService.url(configServerURL: config.url) + + let project = try await createProjectService.createProject( + fullHandle: fullHandle, + serverURL: serverURL + ) + + logger.info("Tuist project \(project.fullName) was successfully created 🎉") + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectDeleteService.swift b/Sources/TuistKit/Services/Project/ProjectDeleteService.swift new file mode 100644 index 00000000000..0b40d0aae90 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectDeleteService.swift @@ -0,0 +1,60 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol ProjectDeleteServicing { + func run( + fullHandle: String, + directory: String? + ) async throws +} + +final class ProjectDeleteService: ProjectDeleteServicing { + private let deleteProjectService: DeleteProjectServicing + private let getProjectService: GetProjectServicing + private let credentialsStore: ServerCredentialsStoring + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + deleteProjectService: DeleteProjectServicing = DeleteProjectService(), + getProjectService: GetProjectServicing = GetProjectService(), + credentialsStore: ServerCredentialsStoring = ServerCredentialsStore(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.deleteProjectService = deleteProjectService + self.getProjectService = getProjectService + self.credentialsStore = credentialsStore + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + fullHandle: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let project = try await getProjectService.getProject( + fullHandle: fullHandle, + serverURL: serverURL + ) + + try await deleteProjectService.deleteProject( + projectId: project.id, + serverURL: serverURL + ) + + logger.info("Successfully deleted the \(project.fullName) project.") + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectListService.swift b/Sources/TuistKit/Services/Project/ProjectListService.swift new file mode 100644 index 00000000000..4f6795d3922 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectListService.swift @@ -0,0 +1,60 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol ProjectListServicing { + func run( + json: Bool, + directory: String? + ) async throws +} + +final class ProjectListService: ProjectListServicing { + private let listProjectsService: ListProjectsServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + listProjectsService: ListProjectsServicing = ListProjectsService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.listProjectsService = listProjectsService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + json: Bool, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let projects = try await listProjectsService.listProjects( + serverURL: serverURL + ) + + if json { + let json = try projects.toJSON() + logger.info(.init(stringLiteral: json.toString(prettyPrint: true)), metadata: .json) + return + } + + if projects.isEmpty { + logger.info("You currently have no Tuist projects. Create one by running `tuist project create`.") + return + } + + let projectsString = "Listing all your projects:\n" + projects.map { " • \($0.fullName)" }.joined(separator: "\n") + logger.info("\(projectsString)") + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectShowService.swift b/Sources/TuistKit/Services/Project/ProjectShowService.swift new file mode 100644 index 00000000000..af9bf8171c2 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectShowService.swift @@ -0,0 +1,82 @@ +import Foundation +import Mockable +import Path +import TuistLoader +import TuistServer +import TuistSupport + +enum ProjectShowServiceError: Equatable, FatalError { + case missingFullHandle + + var type: TuistSupport.ErrorType { + switch self { + case .missingFullHandle: .abort + } + } + + var description: String { + switch self { + case .missingFullHandle: + return "We couldn't show the project because the full handle is missing. You can pass either its value or a path to a Tuist project." + } + } +} + +struct ProjectShowService { + private let opener: Opening + private let configLoader: ConfigLoading + private let serverURLService: ServerURLServicing + private let getProjectService: GetProjectServicing + + init( + opener: Opening = Opener(), + configLoader: ConfigLoading = ConfigLoader(), + serverURLService: ServerURLServicing = ServerURLService(), + getProjectService: GetProjectServicing = GetProjectService() + ) { + self.opener = opener + self.configLoader = configLoader + self.serverURLService = serverURLService + self.getProjectService = getProjectService + } + + func run( + fullHandle: String?, + web: Bool, + path: String? + ) async throws { + let path = try self.path(path) + + let config = try await configLoader.loadConfig(path: path) + guard let fullHandle = fullHandle ?? config.fullHandle else { throw ProjectShowServiceError.missingFullHandle } + + let serverURL = try serverURLService.url(configServerURL: config.url) + + if web { + var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)! + components.path = "/\(fullHandle)" + try opener.open(url: components.url!) + } else { + let project = try await getProjectService.getProject( + fullHandle: fullHandle, + serverURL: serverURL + ) + + logger.info(""" + \("Project".bold()) + Full handle: \(fullHandle) + Default branch: \(project.defaultBranch) + """) + } + } + + // MARK: - Helpers + + private func path(_ path: String?) throws -> AbsolutePath { + if let path { + return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + } else { + return FileHandler.shared.currentPath + } + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectTokensCreateService.swift b/Sources/TuistKit/Services/Project/ProjectTokensCreateService.swift new file mode 100644 index 00000000000..f869c678950 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectTokensCreateService.swift @@ -0,0 +1,57 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol ProjectTokensCreateServicing { + func run( + fullHandle: String, + directory: String? + ) async throws +} + +final class ProjectTokensCreateService: ProjectTokensCreateServicing { + private let createProjectTokenService: CreateProjectTokenServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + convenience init() { + self.init( + createProjectTokenService: CreateProjectTokenService(), + serverURLService: ServerURLService(), + configLoader: ConfigLoader() + ) + } + + init( + createProjectTokenService: CreateProjectTokenServicing, + serverURLService: ServerURLServicing, + configLoader: ConfigLoading + ) { + self.createProjectTokenService = createProjectTokenService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + fullHandle: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let token = try await createProjectTokenService.createProjectToken( + fullHandle: fullHandle, + serverURL: serverURL + ) + + logger.info(.init(stringLiteral: token)) + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectTokensListService.swift b/Sources/TuistKit/Services/Project/ProjectTokensListService.swift new file mode 100644 index 00000000000..5c296a46666 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectTokensListService.swift @@ -0,0 +1,57 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol ProjectTokensListServicing { + func run( + fullHandle: String, + directory: String? + ) async throws +} + +final class ProjectTokensListService: ProjectTokensListServicing { + private let listProjectTokensService: ListProjectTokensServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + listProjectTokensService: ListProjectTokensServicing = ListProjectTokensService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.listProjectTokensService = listProjectTokensService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + fullHandle: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + let tokens = try await listProjectTokensService.listProjectTokens( + fullHandle: fullHandle, + serverURL: serverURL + ) + + if tokens.isEmpty { + logger.notice("No project tokens found. Create one by running `tuist project tokens create \(fullHandle).") + } else { + let textTable = TextTable { [ + TextTable.Column(title: "ID", value: $0.id), + TextTable.Column(title: "Created at", value: $0.insertedAt), + ] } + logger.notice("\(textTable.render(tokens))") + } + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectTokensRevokeService.swift b/Sources/TuistKit/Services/Project/ProjectTokensRevokeService.swift new file mode 100644 index 00000000000..f740e6d0f95 --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectTokensRevokeService.swift @@ -0,0 +1,52 @@ +import Foundation +import Path +import TuistLoader +import TuistServer +import TuistSupport + +protocol ProjectTokensRevokeServicing { + func run( + projectTokenId: String, + fullHandle: String, + directory: String? + ) async throws +} + +final class ProjectTokensRevokeService: ProjectTokensRevokeServicing { + private let revokeProjectTokenService: RevokeProjectTokenServicing + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + init( + revokeProjectTokenService: RevokeProjectTokenServicing = RevokeProjectTokenService(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.revokeProjectTokenService = revokeProjectTokenService + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + func run( + projectTokenId: String, + fullHandle: String, + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + + try await revokeProjectTokenService.revokeProjectToken( + projectTokenId: projectTokenId, + fullHandle: fullHandle, + serverURL: serverURL + ) + + logger.info("The project token \(projectTokenId) was successfully revoked.") + } +} diff --git a/Sources/TuistKit/Services/Project/ProjectUpdateService.swift b/Sources/TuistKit/Services/Project/ProjectUpdateService.swift new file mode 100644 index 00000000000..bce86bdc5fe --- /dev/null +++ b/Sources/TuistKit/Services/Project/ProjectUpdateService.swift @@ -0,0 +1,73 @@ +import Foundation +import Mockable +import Path +import TuistLoader +import TuistServer +import TuistSupport + +enum ProjectUpdateServiceError: Equatable, FatalError { + case missingFullHandle + + var type: TuistSupport.ErrorType { + switch self { + case .missingFullHandle: .abort + } + } + + var description: String { + switch self { + case .missingFullHandle: + return "We couldn't update the project because the full handle is missing. You can pass either its value or a path to a Tuist project." + } + } +} + +struct ProjectUpdateService { + private let opener: Opening + private let configLoader: ConfigLoading + private let serverURLService: ServerURLServicing + private let updateProjectService: UpdateProjectServicing + + init( + opener: Opening = Opener(), + configLoader: ConfigLoading = ConfigLoader(), + serverURLService: ServerURLServicing = ServerURLService(), + updateProjectService: UpdateProjectServicing = UpdateProjectService() + ) { + self.opener = opener + self.configLoader = configLoader + self.serverURLService = serverURLService + self.updateProjectService = updateProjectService + } + + func run( + fullHandle: String?, + defaultBranch: String?, + path: String? + ) async throws { + let path = try self.path(path) + + let config = try await configLoader.loadConfig(path: path) + guard let fullHandle = fullHandle ?? config.fullHandle else { throw ProjectUpdateServiceError.missingFullHandle } + + let serverURL = try serverURLService.url(configServerURL: config.url) + + _ = try await updateProjectService.updateProject( + fullHandle: fullHandle, + serverURL: serverURL, + defaultBranch: defaultBranch + ) + + logger.notice("The project \(fullHandle) was successfully updated 🎉", metadata: .success) + } + + // MARK: - Helpers + + private func path(_ path: String?) throws -> AbsolutePath { + if let path { + return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + } else { + return FileHandler.shared.currentPath + } + } +} diff --git a/Sources/TuistKit/Services/ProjectAutomation+ManifestMapper.swift b/Sources/TuistKit/Services/ProjectAutomation+ManifestMapper.swift new file mode 100644 index 00000000000..6c547b40ab1 --- /dev/null +++ b/Sources/TuistKit/Services/ProjectAutomation+ManifestMapper.swift @@ -0,0 +1,278 @@ +import Foundation +import Path +import ProjectAutomation +import TuistSupport +import XcodeGraph + +extension ProjectAutomation.Graph { + static func from( + graph: XcodeGraph.Graph, + targetsAndDependencies: [GraphTarget: Set] + ) -> ProjectAutomation.Graph { + // generate targets projects only + let projects = targetsAndDependencies + .map(\.key.project) + .reduce(into: [String: ProjectAutomation.Project]()) { + $0[$1.path.pathString] = ProjectAutomation.Project.from($1) + } + + return ProjectAutomation.Graph(name: graph.name, path: graph.path.pathString, projects: projects) + } + + func export(to filePath: AbsolutePath) throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes] + let jsonData = try encoder.encode(self) + let jsonString = String(data: jsonData, encoding: .utf8) + guard let jsonString else { + throw GraphServiceError.encodingError(GraphFormat.json.rawValue) + } + + try FileHandler.shared.write(jsonString, path: filePath, atomically: true) + } +} + +extension ProjectAutomation.Project { + static func from(_ project: XcodeGraph.Project) -> ProjectAutomation.Project { + let packages = project.packages + .reduce(into: [ProjectAutomation.Package]()) { $0.append(ProjectAutomation.Package.from($1)) } + let schemes = project.schemes.reduce(into: [ProjectAutomation.Scheme]()) { $0.append(ProjectAutomation.Scheme.from($1)) } + let targets = project.targets.mapValues { target in + ProjectAutomation.Target.from(target) + } + + return ProjectAutomation.Project( + name: project.name, + path: project.path.pathString, + isExternal: project.isExternal, + packages: packages, + targets: Array(targets.values), + schemes: schemes + ) + } +} + +extension ProjectAutomation.Package { + static func from(_ package: XcodeGraph.Package) -> ProjectAutomation.Package { + switch package { + case let .remote(url, _): + return ProjectAutomation.Package(kind: ProjectAutomation.Package.PackageKind.remote, path: url) + case let .local(path): + return ProjectAutomation.Package(kind: ProjectAutomation.Package.PackageKind.local, path: path.pathString) + } + } +} + +extension ProjectAutomation.Target { + static func from(_ target: XcodeGraph.Target) -> ProjectAutomation.Target { + let dependencies = target.dependencies.map { Self.from($0) } + return ProjectAutomation.Target( + name: target.name, + product: target.product.rawValue, + bundleId: target.bundleId, + sources: target.sources.map(\.path.pathString), + resources: target.resources.resources.map(\.path.pathString), + settings: ProjectAutomation.Settings.from(target.settings), + dependencies: dependencies + ) + } + + static func from(_ dependency: XcodeGraph.TargetDependency) -> ProjectAutomation.TargetDependency { + switch dependency { + case let .target(name, _): + return .target(name: name) + case let .project(target, path, _): + return .project(target: target, path: path.pathString) + case let .framework(path, status, _): + let frameworkStatus: ProjectAutomation.FrameworkStatus + switch status { + case .optional: + frameworkStatus = .optional + case .required: + frameworkStatus = .required + } + return .framework(path: path.pathString, status: frameworkStatus) + case let .xcframework(path, status, _): + let frameworkStatus: ProjectAutomation.FrameworkStatus + switch status { + case .optional: + frameworkStatus = .optional + case .required: + frameworkStatus = .required + } + return .xcframework(path: path.pathString, status: frameworkStatus) + case let .library(path, publicHeaders, swiftModuleMap, _): + return .library( + path: path.pathString, + publicHeaders: publicHeaders.pathString, + swiftModuleMap: swiftModuleMap?.pathString + ) + case let .package(product, type, _): + switch type { + case .macro: + return .packageMacro(product: product) + case .plugin: + return .packagePlugin(product: product) + case .runtime: + return .package(product: product) + } + case let .sdk(name, status, _): + let projectAutomationStatus: ProjectAutomation.SDKStatus + switch status { + case .optional: + projectAutomationStatus = .optional + case .required: + projectAutomationStatus = .required + } + return .sdk(name: name, status: projectAutomationStatus) + case .xctest: + return .xctest + } + } +} + +extension ProjectAutomation.Scheme { + static func from(_ scheme: XcodeGraph.Scheme) -> ProjectAutomation.Scheme { + var testTargets = [String]() + if let testAction = scheme.testAction { + for testTarget in testAction.targets { + testTargets.append(testTarget.target.name) + } + } + + return ProjectAutomation.Scheme(name: scheme.name, testActionTargets: testTargets) + } +} + +extension ProjectAutomation.Settings { + public static func from(_ settings: XcodeGraph.Settings?) -> ProjectAutomation.Settings { + ProjectAutomation.Settings( + configurations: [ProjectAutomation.BuildConfiguration: ProjectAutomation.Configuration?].from( + settings?.configurations + ) + ) + } +} + +extension [ProjectAutomation.BuildConfiguration: ProjectAutomation.Configuration?] { + public static func from( + _ buildConfigurationDictionary: [XcodeGraph.BuildConfiguration: XcodeGraph.Configuration?]? + ) -> [ProjectAutomation.BuildConfiguration: ProjectAutomation.Configuration?] { + guard let buildConfigurationDictionary else { + return [:] + } + + var dict = [ProjectAutomation.BuildConfiguration: ProjectAutomation.Configuration?]() + + for (buildConfiguration, configuration) in buildConfigurationDictionary { + let item = ProjectAutomation.BuildConfiguration.from( + buildConfiguration + ) + dict[item] = ProjectAutomation.Configuration.from( + configuration + ) + } + + return dict + } +} + +extension ProjectAutomation.Configuration { + static func from( + _ configuration: XcodeGraph.Configuration? + ) -> ProjectAutomation.Configuration? { + guard let configuration else { + return nil + } + + return ProjectAutomation.Configuration( + settings: ProjectAutomation.SettingsDictionary.from( + configuration.settings + ) + ) + } +} + +extension ProjectAutomation.SettingValue { + static func from( + _ value: XcodeGraph.SettingValue + ) -> ProjectAutomation.SettingValue { + switch value { + case let .string(string): + return ProjectAutomation.SettingValue(string: string) + case let .array(array): + return ProjectAutomation.SettingValue(array: array) + } + } +} + +extension ProjectAutomation.SettingsDictionary { + static func from( + _ settings: XcodeGraph.SettingsDictionary + ) -> ProjectAutomation.SettingsDictionary { + var dict = ProjectAutomation.SettingsDictionary() + for (key, value) in settings { + dict[key] = ProjectAutomation.SettingValue.from( + value + ) + } + return dict + } +} + +extension ProjectAutomation.BuildConfiguration { + static func from( + _ buildConfiguration: XcodeGraph.BuildConfiguration + ) -> ProjectAutomation.BuildConfiguration { + BuildConfiguration( + name: buildConfiguration.name, + variant: ProjectAutomation.BuildConfiguration.Variant.from( + buildConfiguration.variant + ) + ) + } +} + +extension ProjectAutomation.BuildConfiguration.Variant { + static func from( + _ variant: XcodeGraph.BuildConfiguration.Variant + ) -> ProjectAutomation.BuildConfiguration.Variant { + ProjectAutomation.BuildConfiguration.Variant( + variant: variant + ) + } +} + +extension ProjectAutomation.BuildConfiguration.Variant { + private init(variant: XcodeGraph.BuildConfiguration.Variant) { + switch variant { + case .debug: + self = .debug + case .release: + self = .release + } + } +} + +enum GraphServiceError: FatalError { + case jsonNotValidForVisualExport + case encodingError(String) + + var description: String { + switch self { + case .jsonNotValidForVisualExport: + return "json format is not valid for visual export" + case let .encodingError(format): + return "failed to encode graph to \(format)" + } + } + + var type: ErrorType { + switch self { + case .jsonNotValidForVisualExport: + return .abort + case .encodingError: + return .abort + } + } +} diff --git a/Sources/TuistKit/Services/RunService.swift b/Sources/TuistKit/Services/RunService.swift index 3561bccc16b..1e5aac36564 100644 --- a/Sources/TuistKit/Services/RunService.swift +++ b/Sources/TuistKit/Services/RunService.swift @@ -1,17 +1,22 @@ +import FileSystem import Foundation -import TSCBasic +import Path import struct TSCUtility.Version import TuistAutomation import TuistCore -import TuistGraph import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph -enum RunServiceError: FatalError { +enum RunServiceError: FatalError, Equatable { case schemeNotFound(scheme: String, existing: [String]) case schemeWithoutRunnableTarget(scheme: String) case invalidVersion(String) case workspaceNotFound(path: String) + case invalidDownloadBuildURL(String) + case invalidPreviewURL(String) + case appNotFound(String) var description: String { switch self { @@ -23,6 +28,12 @@ enum RunServiceError: FatalError { return "The version \(version) is not a valid version specifier." case let .workspaceNotFound(path): return "Workspace not found expected xcworkspace at \(path)" + case let .invalidDownloadBuildURL(downloadBuildURL): + return "The download build URL \(downloadBuildURL) is invalid." + case let .invalidPreviewURL(previewURL): + return "The preview URL \(previewURL) is invalid." + case let .appNotFound(url): + return "The app at \(url) was not found." } } @@ -30,9 +41,11 @@ enum RunServiceError: FatalError { switch self { case .schemeNotFound, .schemeWithoutRunnableTarget, - .invalidVersion: + .invalidVersion, + .appNotFound, + .invalidPreviewURL: return .abort - case .workspaceNotFound: + case .workspaceNotFound, .invalidDownloadBuildURL: return .bug } } @@ -44,33 +57,73 @@ final class RunService { private let targetBuilder: TargetBuilding private let targetRunner: TargetRunning private let configLoader: ConfigLoading + private let downloadPreviewService: DownloadPreviewServicing + private let fileHandler: FileHandling + private let fileSystem: FileSysteming + private let appRunner: AppRunning + private let remoteArtifactDownloader: RemoteArtifactDownloading + private let appBundleLoader: AppBundleLoading + private let fileArchiverFactory: FileArchivingFactorying + + convenience init() { + self.init( + generatorFactory: GeneratorFactory(), + buildGraphInspector: BuildGraphInspector(), + targetBuilder: TargetBuilder(), + targetRunner: TargetRunner(), + configLoader: ConfigLoader(manifestLoader: ManifestLoader()), + downloadPreviewService: DownloadPreviewService(), + fileHandler: FileHandler.shared, + fileSystem: FileSystem(), + appRunner: AppRunner(), + remoteArtifactDownloader: RemoteArtifactDownloader(), + appBundleLoader: AppBundleLoader(), + fileArchiverFactory: FileArchivingFactory() + ) + } init( - generatorFactory: GeneratorFactorying = GeneratorFactory(), - buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), - targetBuilder: TargetBuilding = TargetBuilder(), - targetRunner: TargetRunning = TargetRunner(), - configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()) + generatorFactory: GeneratorFactorying, + buildGraphInspector: BuildGraphInspecting, + targetBuilder: TargetBuilding, + targetRunner: TargetRunning, + configLoader: ConfigLoading, + downloadPreviewService: DownloadPreviewServicing, + fileHandler: FileHandling, + fileSystem: FileSystem, + appRunner: AppRunning, + remoteArtifactDownloader: RemoteArtifactDownloading, + appBundleLoader: AppBundleLoading, + fileArchiverFactory: FileArchivingFactorying ) { self.generatorFactory = generatorFactory self.buildGraphInspector = buildGraphInspector self.targetBuilder = targetBuilder self.targetRunner = targetRunner self.configLoader = configLoader + self.downloadPreviewService = downloadPreviewService + self.fileHandler = fileHandler + self.fileSystem = fileSystem + self.appRunner = appRunner + self.remoteArtifactDownloader = remoteArtifactDownloader + self.appBundleLoader = appBundleLoader + self.fileArchiverFactory = fileArchiverFactory } // swiftlint:disable:next function_body_length func run( path: String?, - schemeName: String, + runnable: Runnable, generate: Bool, clean: Bool, configuration: String?, device: String?, - version: String?, + osVersion: String?, rosetta: Bool, arguments: [String] ) async throws { + let device = arguments.firstIndex(of: "-destination").map { arguments[$0 + 1] } ?? device + let runPath: AbsolutePath if let path { runPath = try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) @@ -78,18 +131,99 @@ final class RunService { runPath = FileHandler.shared.currentPath } + let osVersion = try osVersion.map { versionString in + guard let version = versionString.version() else { + throw RunServiceError.invalidVersion(versionString) + } + return version + } + + switch runnable { + case let .url(previewLink): + try await runPreviewLink( + previewLink, + device: device, + version: osVersion, + path: runPath + ) + case let .scheme(scheme): + try await runScheme( + scheme, + path: runPath, + generate: generate, + clean: clean, + configuration: configuration, + device: device, + version: osVersion, + rosetta: rosetta, + arguments: arguments + ) + } + } + + private func runPreviewLink( + _ previewLink: URL, + device: String?, + version: Version?, + path _: AbsolutePath + ) async throws { + guard let scheme = previewLink.scheme, + let host = previewLink.host, + let serverURL = URL(string: "\(scheme)://\(host)\(previewLink.port.map { ":" + String($0) } ?? "")"), + previewLink.pathComponents.count > 4 // We expect at least four path components + else { throw RunServiceError.invalidPreviewURL(previewLink.absoluteString) } + + let downloadURLString = try await downloadPreviewService.downloadPreview( + previewLink.lastPathComponent, + fullHandle: "\(previewLink.pathComponents[1])/\(previewLink.pathComponents[2])", + serverURL: serverURL + ) + + guard let downloadURL = URL(string: downloadURLString) + else { throw RunServiceError.invalidDownloadBuildURL(downloadURLString) } + + guard let archivePath = try await remoteArtifactDownloader.download(url: downloadURL) + else { throw RunServiceError.appNotFound(previewLink.absoluteString) } + + let unarchivedDirectory = try fileArchiverFactory.makeFileUnarchiver(for: archivePath).unzip() + + try await fileSystem.remove(archivePath) + + let apps = try await fileHandler.glob(unarchivedDirectory, glob: "*.app") + .concurrentMap { + try await self.appBundleLoader.load($0) + } + + try await appRunner.runApp( + apps, + version: version, + device: device + ) + } + + private func runScheme( + _ scheme: String, + path: AbsolutePath, + generate: Bool, + clean: Bool, + configuration: String?, + device: String?, + version: Version?, + rosetta: Bool, + arguments: [String] + ) async throws { let graph: Graph - let config = try configLoader.loadConfig(path: runPath) - let generator = generatorFactory.default(config: config) - if try (generate || buildGraphInspector.workspacePath(directory: runPath) == nil) { + let config = try await configLoader.loadConfig(path: path) + let generator = generatorFactory.defaultGenerator(config: config) + if try (generate || buildGraphInspector.workspacePath(directory: path) == nil) { logger.notice("Generating project for running", metadata: .section) - graph = try await generator.generateWithGraph(path: runPath).1 + graph = try await generator.generateWithGraph(path: path).1 } else { - graph = try await generator.load(path: runPath) + graph = try await generator.load(path: path) } - guard let workspacePath = try buildGraphInspector.workspacePath(directory: runPath) else { - throw RunServiceError.workspaceNotFound(path: runPath.pathString) + guard let workspacePath = try buildGraphInspector.workspacePath(directory: path) else { + throw RunServiceError.workspaceNotFound(path: path.pathString) } let graphTraverser = GraphTraverser(graph: graph) @@ -97,8 +231,8 @@ final class RunService { logger.debug("Found the following runnable schemes: \(runnableSchemes.map(\.name).joined(separator: ", "))") - guard let scheme = runnableSchemes.first(where: { $0.name == schemeName }) else { - throw RunServiceError.schemeNotFound(scheme: schemeName, existing: runnableSchemes.map(\.name)) + guard let scheme = runnableSchemes.first(where: { $0.name == scheme }) else { + throw RunServiceError.schemeNotFound(scheme: scheme, existing: runnableSchemes.map(\.name)) } guard let graphTarget = buildGraphInspector.runnableTarget(scheme: scheme, graphTraverser: graphTraverser) else { @@ -117,9 +251,10 @@ final class RunService { buildOutputPath: nil, derivedDataPath: nil, device: device, - osVersion: version?.version(), + osVersion: version.map { XcodeGraph.Version(stringLiteral: $0.description) }, rosetta: rosetta, - graphTraverser: graphTraverser + graphTraverser: graphTraverser, + passthroughXcodeBuildArguments: [] ) let minVersion: Version? @@ -137,13 +272,6 @@ final class RunService { .first } - let version: Version? = try version.map { versionString in - guard let version = versionString.version() else { - throw RunServiceError.invalidVersion(versionString) - } - return version - } ?? nil - try await targetRunner.runTarget( graphTarget, platform: try graphTarget.target.servicePlatform, diff --git a/Sources/TuistKit/Services/ScaffoldService.swift b/Sources/TuistKit/Services/ScaffoldService.swift index dc67ea3e8fd..dba3008d19d 100644 --- a/Sources/TuistKit/Services/ScaffoldService.swift +++ b/Sources/TuistKit/Services/ScaffoldService.swift @@ -1,10 +1,10 @@ -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader import TuistPlugin import TuistScaffold import TuistSupport +import XcodeGraph enum ScaffoldServiceError: FatalError, Equatable { var type: ErrorType { @@ -68,7 +68,7 @@ final class ScaffoldService { var attributes: [Template.Attribute] = [] if let templateUrl = url, templateUrl.isGitURL { - try templateGitLoader.loadTemplate(from: templateUrl, templateName: templateName) { template in + try await templateGitLoader.loadTemplate(from: templateUrl, templateName: templateName, plugins: plugins) { template in attributes = template.attributes } } else { @@ -77,10 +77,10 @@ final class ScaffoldService { template: templateName ) - let template = try templateLoader.loadTemplate(at: templateDirectory) + let template = try await templateLoader.loadTemplate(at: templateDirectory, plugins: plugins) attributes = template.attributes } - return attributes.reduce(into: (required: [], optional: [])) { currentValue, attribute in + return template.attributes.reduce(into: (required: [], optional: [])) { currentValue, attribute in switch attribute { case let .optional(name, default: _): currentValue.optional.append(name) @@ -101,14 +101,14 @@ final class ScaffoldService { let plugins = try await loadPlugins(at: path) if let templateUrl = templateUrl, templateUrl.isGitURL { - try templateGitLoader.loadTemplate(from: templateUrl, templateName: templateName, closure: { template in + try await templateGitLoader.loadTemplate(from: templateUrl, templateName: templateName, closure: { template in let parsedAttributes = try parseAttributes( requiredTemplateOptions: requiredTemplateOptions, optionalTemplateOptions: optionalTemplateOptions, template: template ) - try templateGenerator.generate( + try await templateGenerator.generate( template: template, to: path, attributes: parsedAttributes @@ -122,7 +122,7 @@ final class ScaffoldService { template: templateName ) - let template = try templateLoader.loadTemplate(at: templateDirectory) + let template = try await templateLoader.loadTemplate(at: templateDirectory, plugins: plugins) let parsedAttributes = try parseAttributes( requiredTemplateOptions: requiredTemplateOptions, @@ -130,7 +130,7 @@ final class ScaffoldService { template: template ) - try templateGenerator.generate( + try await templateGenerator.generate( template: template, to: path, attributes: parsedAttributes @@ -151,7 +151,7 @@ final class ScaffoldService { } private func loadPlugins(at path: AbsolutePath) async throws -> Plugins { - let config = try configLoader.loadConfig(path: path) + let config = try await configLoader.loadConfig(path: path) return try await pluginService.loadPlugins(using: config) } @@ -162,13 +162,13 @@ final class ScaffoldService { requiredTemplateOptions: [String: String], optionalTemplateOptions: [String: String?], template: Template - ) throws -> [String: String] { + ) throws -> [String: Template.Attribute.Value] { try template.attributes.reduce(into: [:]) { attributesDictionary, attribute in switch attribute { case let .required(name): guard let option = requiredTemplateOptions[name] else { throw ScaffoldServiceError.attributeNotProvided(name) } - attributesDictionary[name] = option + attributesDictionary[name] = .string(option) case let .optional(name, default: defaultValue): guard let unwrappedOption = optionalTemplateOptions[name], let option = unwrappedOption @@ -176,7 +176,7 @@ final class ScaffoldService { attributesDictionary[name] = defaultValue return } - attributesDictionary[name] = option + attributesDictionary[name] = .string(option) } } } diff --git a/Sources/TuistKit/Services/SessionService.swift b/Sources/TuistKit/Services/SessionService.swift new file mode 100644 index 00000000000..17cc18a3345 --- /dev/null +++ b/Sources/TuistKit/Services/SessionService.swift @@ -0,0 +1,48 @@ +import Foundation +import Path +import TuistCore +import TuistLoader +import TuistServer +import TuistSupport + +protocol SessionServicing: AnyObject { + /// It prints any existing session in the keychain to authenticate + /// on a server identified by that URL. + func printSession( + directory: String? + ) async throws +} + +final class SessionService: SessionServicing { + private let serverSessionController: ServerSessionControlling + private let serverURLService: ServerURLServicing + private let configLoader: ConfigLoading + + // MARK: - Init + + init( + serverSessionController: ServerSessionControlling = ServerSessionController(), + serverURLService: ServerURLServicing = ServerURLService(), + configLoader: ConfigLoading = ConfigLoader() + ) { + self.serverSessionController = serverSessionController + self.serverURLService = serverURLService + self.configLoader = configLoader + } + + // MARK: - CloudAuthServicing + + func printSession( + directory: String? + ) async throws { + let directoryPath: AbsolutePath + if let directory { + directoryPath = try AbsolutePath(validating: directory, relativeTo: FileHandler.shared.currentPath) + } else { + directoryPath = FileHandler.shared.currentPath + } + let config = try await configLoader.loadConfig(path: directoryPath) + let serverURL = try serverURLService.url(configServerURL: config.url) + try serverSessionController.printSession(serverURL: serverURL) + } +} diff --git a/Sources/TuistKit/Services/ShareService.swift b/Sources/TuistKit/Services/ShareService.swift new file mode 100644 index 00000000000..df5d82436d7 --- /dev/null +++ b/Sources/TuistKit/Services/ShareService.swift @@ -0,0 +1,269 @@ +import Foundation +import Path +import TuistAutomation +import TuistCore +import TuistLoader +import TuistServer +import TuistSupport +import XcodeGraph + +enum ShareServiceError: Equatable, FatalError { + case projectOrWorkspaceNotFound(path: String) + case noAppsFound(app: String, configuration: String) + case appNotSpecified + case multipleAppsSpecified([String]) + case platformsNotSpecified + case fullHandleNotFound + + var description: String { + switch self { + case let .projectOrWorkspaceNotFound(path): + return "Workspace or project not found at \(path)" + case let .noAppsFound(app: app, configuration: configuration): + return "\(app) for the \(configuration) configuration was not found. You can build it by running `tuist build \(app)`" + case .appNotSpecified: + return "If you're not using Tuist projects, you must specify the app name when sharing an app, such as `tuist share App --platforms ios`." + case .platformsNotSpecified: + return "If you're not using Tuist projects, you must specify the platforms when sharing an app, such as `tuist share App --platforms ios`." + case let .multipleAppsSpecified(apps): + return "You specified multiple apps to share: \(apps.joined(separator: " ")). You cannot specify multiple apps when using `tuist share`." + case .fullHandleNotFound: + return "You are missing full handle in your Config.swift." + } + } + + var type: ErrorType { + switch self { + case .projectOrWorkspaceNotFound, .noAppsFound, .appNotSpecified, .platformsNotSpecified, .multipleAppsSpecified, + .fullHandleNotFound: + return .abort + } + } +} + +struct ShareService { + private let fileHandler: FileHandling + private let xcodeProjectBuildDirectoryLocator: XcodeProjectBuildDirectoryLocating + private let buildGraphInspector: BuildGraphInspecting + private let previewsUploadService: PreviewsUploadServicing + private let configLoader: ConfigLoading + private let serverURLService: ServerURLServicing + private let manifestLoader: ManifestLoading + private let manifestGraphLoader: ManifestGraphLoading + private let userInputReader: UserInputReading + private let defaultConfigurationFetcher: DefaultConfigurationFetching + private let appBundleLoader: AppBundleLoading + + init() { + let manifestLoader = ManifestLoaderFactory() + .createManifestLoader() + let manifestGraphLoader = ManifestGraphLoader( + manifestLoader: manifestLoader, + workspaceMapper: SequentialWorkspaceMapper(mappers: []), + graphMapper: SequentialGraphMapper([]) + ) + + self.init( + fileHandler: FileHandler.shared, + xcodeProjectBuildDirectoryLocator: XcodeProjectBuildDirectoryLocator(), + buildGraphInspector: BuildGraphInspector(), + previewsUploadService: PreviewsUploadService(), + configLoader: ConfigLoader(), + serverURLService: ServerURLService(), + manifestLoader: manifestLoader, + manifestGraphLoader: manifestGraphLoader, + userInputReader: UserInputReader(), + defaultConfigurationFetcher: DefaultConfigurationFetcher(), + appBundleLoader: AppBundleLoader() + ) + } + + init( + fileHandler: FileHandling, + xcodeProjectBuildDirectoryLocator: XcodeProjectBuildDirectoryLocating, + buildGraphInspector: BuildGraphInspecting, + previewsUploadService: PreviewsUploadServicing, + configLoader: ConfigLoading, + serverURLService: ServerURLServicing, + manifestLoader: ManifestLoading, + manifestGraphLoader: ManifestGraphLoading, + userInputReader: UserInputReading, + defaultConfigurationFetcher: DefaultConfigurationFetching, + appBundleLoader: AppBundleLoading + ) { + self.fileHandler = fileHandler + self.xcodeProjectBuildDirectoryLocator = xcodeProjectBuildDirectoryLocator + self.buildGraphInspector = buildGraphInspector + self.previewsUploadService = previewsUploadService + self.configLoader = configLoader + self.serverURLService = serverURLService + self.manifestLoader = manifestLoader + self.manifestGraphLoader = manifestGraphLoader + self.userInputReader = userInputReader + self.defaultConfigurationFetcher = defaultConfigurationFetcher + self.appBundleLoader = appBundleLoader + } + + func run( + path: String?, + apps: [String], + configuration: String?, + platforms: [Platform], + derivedDataPath: String? + ) async throws { + let path = try self.path(path) + + let config = try await configLoader.loadConfig(path: path) + let serverURL = try serverURLService.url(configServerURL: config.url) + + guard let fullHandle = config.fullHandle else { throw ShareServiceError.fullHandleNotFound } + + let derivedDataPath = try derivedDataPath.map { + try AbsolutePath( + validating: $0, + relativeTo: fileHandler.currentPath + ) + } + + if !apps.isEmpty, apps.allSatisfy({ $0.hasSuffix(".app") }) { + let appPaths = try apps.map { + try AbsolutePath( + validating: $0, + relativeTo: fileHandler.currentPath + ) + } + + let appBundles = try await appPaths.concurrentMap { + try await appBundleLoader.load($0) + } + + let appNames = appBundles.map(\.infoPlist.name).uniqued() + guard appNames.count == 1, + let appName = appNames.first else { throw ShareServiceError.multipleAppsSpecified(appNames) } + + let url = try await previewsUploadService.uploadPreviews( + appPaths, + fullHandle: fullHandle, + serverURL: serverURL + ) + logger.notice("\(appName) uploaded – share it with others using the following link: \(url.absoluteString)") + } else if manifestLoader.hasRootManifest(at: path) { + guard apps.count < 2 else { throw ShareServiceError.multipleAppsSpecified(apps) } + + let (graph, _, _, _) = try await manifestGraphLoader.load(path: path) + let graphTraverser = GraphTraverser(graph: graph) + let appTargets = graphTraverser.targets(product: .app) + .map { $0 } + .filter { + if let app = apps.first { + return $0.target.name == app + } else { + return true + } + } + let appTarget: GraphTarget = try userInputReader.readValue( + asking: "Select the app that you want to share:", + values: appTargets.sorted(by: { $0.target.name < $1.target.name }), + valueDescription: \.target.name + ) + + let configuration = try defaultConfigurationFetcher.fetch( + configuration: configuration, + config: config, + graph: graph + ) + + let platforms = platforms.isEmpty ? appTarget.target.supportedPlatforms.map { $0 } : platforms + + try await uploadPreviews( + for: platforms, + workspacePath: graph.workspace.xcWorkspacePath, + configuration: configuration, + app: appTarget.target.productName, + derivedDataPath: derivedDataPath, + fullHandle: fullHandle, + serverURL: serverURL + ) + } else { + guard !apps.isEmpty else { throw ShareServiceError.appNotSpecified } + guard apps.count == 1, let app = apps.first else { throw ShareServiceError.multipleAppsSpecified(apps) } + guard !platforms.isEmpty else { throw ShareServiceError.platformsNotSpecified } + + let configuration = configuration ?? BuildConfiguration.debug.name + + guard let workspaceOrProjectPath = fileHandler.glob(path, glob: "*.xcworkspace").first ?? fileHandler + .glob(path, glob: "*.xcodeproj").first + else { + throw ShareServiceError.projectOrWorkspaceNotFound(path: path.pathString) + } + + try await uploadPreviews( + for: platforms, + workspacePath: workspaceOrProjectPath, + configuration: configuration, + app: app, + derivedDataPath: derivedDataPath, + fullHandle: fullHandle, + serverURL: serverURL + ) + } + } + + // MARK: - Helpers + + private func path(_ path: String?) throws -> AbsolutePath { + if let path { + return try AbsolutePath(validating: path, relativeTo: FileHandler.shared.currentPath) + } else { + return FileHandler.shared.currentPath + } + } + + private func uploadPreviews( + for platforms: [Platform], + workspacePath: AbsolutePath, + configuration: String, + app: String, + derivedDataPath: AbsolutePath?, + fullHandle: String, + serverURL: URL + ) async throws { + try await fileHandler.inTemporaryDirectory { temporaryPath in + let appPaths = try platforms + .map { platform in + let sdkPathComponent: String = { + guard platform != .macOS else { + return platform.xcodeDeviceSDK + } + return "\(platform.xcodeSimulatorSDK!)" + }() + + let appPath = try xcodeProjectBuildDirectoryLocator.locate( + platform: platform, + projectPath: workspacePath, + derivedDataPath: derivedDataPath, + configuration: configuration + ) + .appending(component: "\(app).app") + + let newAppPath = temporaryPath.appending(component: "\(sdkPathComponent)-\(app).app") + + if !fileHandler.exists(appPath) { + throw ShareServiceError.noAppsFound(app: app, configuration: configuration) + } + + try fileHandler.copy(from: appPath, to: newAppPath) + + return newAppPath + } + .uniqued() + + let url = try await previewsUploadService.uploadPreviews( + appPaths, + fullHandle: fullHandle, + serverURL: serverURL + ) + logger.notice("\(app) uploaded – share it with others using the following link: \(url.absoluteString)") + } + } +} diff --git a/Sources/TuistKit/Services/TestService.swift b/Sources/TuistKit/Services/TestService.swift index e8c4ad5c8bd..13ed3293acb 100644 --- a/Sources/TuistKit/Services/TestService.swift +++ b/Sources/TuistKit/Services/TestService.swift @@ -1,11 +1,12 @@ import Foundation -import TSCBasic +import Path import struct TSCUtility.Version import TuistAutomation import TuistCore -import TuistGraph import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph enum TestServiceError: FatalError, Equatable { case schemeNotFound(scheme: String, existing: [String]) @@ -57,51 +58,55 @@ enum TestServiceError: FatalError, Equatable { } } -public final class TestService { // swiftlint:disable:this type_body_length +final class TestService { // swiftlint:disable:this type_body_length private let generatorFactory: GeneratorFactorying + private let cacheStorageFactory: CacheStorageFactorying private let xcodebuildController: XcodeBuildControlling private let buildGraphInspector: BuildGraphInspecting private let simulatorController: SimulatorControlling private let contentHasher: ContentHashing - private let testsCacheTemporaryDirectory: TemporaryDirectory private let cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring + private let configLoader: ConfigLoading + private let fileHandler: FileHandling public convenience init( - testsCacheTemporaryDirectory: TemporaryDirectory + generatorFactory: GeneratorFactorying, + cacheStorageFactory: CacheStorageFactorying ) { + let manifestLoaderFactory = ManifestLoaderFactory() + let manifestLoader = manifestLoaderFactory.createManifestLoader() + let configLoader = ConfigLoader(manifestLoader: manifestLoader) self.init( - testsCacheTemporaryDirectory: testsCacheTemporaryDirectory, - generatorFactory: GeneratorFactory() - ) - } - - convenience init() throws { - let testsCacheTemporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - self.init( - testsCacheTemporaryDirectory: testsCacheTemporaryDirectory + generatorFactory: generatorFactory, + cacheStorageFactory: cacheStorageFactory, + configLoader: configLoader ) } init( - testsCacheTemporaryDirectory: TemporaryDirectory, generatorFactory: GeneratorFactorying = GeneratorFactory(), + cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory(), xcodebuildController: XcodeBuildControlling = XcodeBuildController(), buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), simulatorController: SimulatorControlling = SimulatorController(), contentHasher: ContentHashing = ContentHasher(), - cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring = CacheDirectoriesProviderFactory() + cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring = CacheDirectoriesProviderFactory(), + configLoader: ConfigLoading, + fileHandler: FileHandling = FileHandler.shared ) { - self.testsCacheTemporaryDirectory = testsCacheTemporaryDirectory self.generatorFactory = generatorFactory + self.cacheStorageFactory = cacheStorageFactory self.xcodebuildController = xcodebuildController self.buildGraphInspector = buildGraphInspector self.simulatorController = simulatorController self.contentHasher = contentHasher self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory + self.configLoader = configLoader + self.fileHandler = fileHandler } - public func validateParameters( + static func validateParameters( testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier] ) throws { @@ -152,7 +157,8 @@ public final class TestService { // swiftlint:disable:this type_body_length } // swiftlint:disable:next function_body_length - public func run( + func run( + runId: String, schemeName: String?, clean: Bool, configuration: String?, @@ -169,39 +175,37 @@ public final class TestService { // swiftlint:disable:this type_body_length skipTestTargets: [TestIdentifier], testPlanConfiguration: TestPlanConfiguration?, validateTestTargetsParameters: Bool = true, - generator: Generating? = nil, - generateOnly: Bool + ignoreBinaryCache: Bool, + ignoreSelectiveTesting: Bool, + generateOnly: Bool, + passthroughXcodeBuildArguments: [String] ) async throws { if validateTestTargetsParameters { - try validateParameters( + try Self.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ) } // Load config - let manifestLoaderFactory = ManifestLoaderFactory() - let manifestLoader = manifestLoaderFactory.createManifestLoader() - let configLoader = ConfigLoader(manifestLoader: manifestLoader) - let config = try configLoader.loadConfig(path: path) + let config = try await configLoader.loadConfig(path: path) + let cacheStorage = try cacheStorageFactory.cacheStorage(config: config) - let testGenerator: Generating - if let generator { - testGenerator = generator - } else { - testGenerator = generatorFactory.test( - config: config, - testsCacheDirectory: testsCacheTemporaryDirectory.path, - testPlan: testPlanConfiguration?.testPlan, - includedTargets: Set(testTargets.map(\.target)), - excludedTargets: Set(skipTestTargets.filter { $0.class == nil }.map(\.target)), - skipUITests: skipUITests - ) - } + let testGenerator = generatorFactory.testing( + config: config, + testPlan: testPlanConfiguration?.testPlan, + includedTargets: Set(testTargets.map(\.target)), + excludedTargets: Set(skipTestTargets.filter { $0.class == nil }.map(\.target)), + skipUITests: skipUITests, + configuration: configuration, + ignoreBinaryCache: ignoreBinaryCache, + ignoreSelectiveTesting: ignoreSelectiveTesting, + cacheStorage: cacheStorage + ) logger.notice("Generating project for testing", metadata: .section) - let graph = try await testGenerator.generateWithGraph( + let (_, graph, mapperEnvironment) = try await testGenerator.generateWithGraph( path: path - ).1 + ) if generateOnly { return @@ -223,16 +227,42 @@ public final class TestService { // swiftlint:disable:this type_body_length ) } + let passedResultBundlePath = resultBundlePath + + let resultBundlePath = try self.resultBundlePath( + passedResultBundlePath: passedResultBundlePath, + runId: runId, + config: config + ) + + defer { + if let resultBundlePath, let passedResultBundlePath, config.fullHandle != nil { + if !FileHandler.shared.exists(resultBundlePath.parentDirectory) { + try? FileHandler.shared.createFolder(resultBundlePath.parentDirectory) + } + try? FileHandler.shared.copy(from: passedResultBundlePath, to: resultBundlePath) + } + } + + let testSchemes: [Scheme] if let schemeName { - guard let scheme = testableSchemes.first(where: { $0.name == schemeName }) + guard let scheme = graphTraverser.schemes().first(where: { $0.name == schemeName }) else { - throw TestServiceError.schemeNotFound( - scheme: schemeName, - existing: testableSchemes.map(\.name) - ) + let schemes = mapperEnvironment.initialGraph.map(GraphTraverser.init)?.schemes() ?? graphTraverser.schemes() + if schemes.first(where: { $0.name == schemeName }) != nil { + logger.log(level: .info, "The scheme \(schemeName)'s test action has no tests to run, finishing early.") + return + } else { + throw TestServiceError.schemeNotFound( + scheme: schemeName, + existing: Set(schemes.map(\.name)).map { $0 } + ) + } } switch (testPlanConfiguration?.testPlan, scheme.testAction?.targets.isEmpty, scheme.testAction?.testPlans?.isEmpty) { + case (_, false, _): + break case (nil, true, _), (nil, nil, _): logger.log(level: .info, "The scheme \(schemeName)'s test action has no tests to run, finishing early.") return @@ -243,7 +273,13 @@ public final class TestService { // swiftlint:disable:this type_body_length break } - let testSchemes: [Scheme] = [scheme] + testSchemes = [scheme] + + checkSkippedTargets( + for: testSchemes, + mapperEnvironment: mapperEnvironment, + graph: graph + ) for testScheme in testSchemes { try await self.testScheme( @@ -260,11 +296,13 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: retryCount, testTargets: testTargets, skipTestTargets: skipTestTargets, - testPlanConfiguration: testPlanConfiguration + testPlanConfiguration: testPlanConfiguration, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) } } else { - let testSchemes: [Scheme] = buildGraphInspector.workspaceSchemes(graphTraverser: graphTraverser) + let allSchemes = buildGraphInspector.workspaceSchemes(graphTraverser: graphTraverser) + testSchemes = allSchemes .filter { $0.testAction.map { !$0.targets.isEmpty } ?? false } @@ -274,6 +312,12 @@ public final class TestService { // swiftlint:disable:this type_body_length return } + checkSkippedTargets( + for: allSchemes, + mapperEnvironment: mapperEnvironment, + graph: graph + ) + for testScheme in testSchemes { try await self.testScheme( scheme: testScheme, @@ -289,16 +333,124 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: retryCount, testTargets: testTargets, skipTestTargets: skipTestTargets, - testPlanConfiguration: testPlanConfiguration + testPlanConfiguration: testPlanConfiguration, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) } } + try await storeSuccessfulTestHashes( + for: testSchemes, + graph: graph, + mapperEnvironment: mapperEnvironment, + cacheStorage: cacheStorage + ) + logger.log(level: .notice, "The project tests ran successfully", metadata: .success) } // MARK: - Helpers + private func checkSkippedTargets( + for schemes: [Scheme], + mapperEnvironment: MapperEnvironment, + graph: Graph + ) { + let testActionTargets = testActionTargets(for: schemes, graph: graph) + .map(\.target) + guard let initialGraph = mapperEnvironment.initialGraph else { return } + let initialSchemes = GraphTraverser(graph: initialGraph).schemes() + let initialTestTargets = self.testActionTargets( + for: initialSchemes + .filter { initialScheme in + schemes.contains(where: { $0.name == initialScheme.name }) + }, + graph: initialGraph + ) + let skippedTestTargets = initialTestTargets + .map(\.target) + .filter { target in + !testActionTargets.contains(where: { + $0.bundleId == target.bundleId + }) + } + .map(\.name) + if !skippedTestTargets.isEmpty { + logger + .notice( + "The following targets have not changed since the last successful run and will be skipped: \(skippedTestTargets.joined(separator: ", "))" + ) + } + } + + private func testActionTargets( + for schemes: [Scheme], + graph: Graph + ) -> [GraphTarget] { + return schemes.flatMap { $0.testAction?.targets.map(\.target) ?? [] } + .compactMap { + guard let project = graph.projects[$0.projectPath], + let target = project.targets[$0.name] + else { + return nil + } + return GraphTarget(path: project.path, target: target, project: project) + } + } + + private func storeSuccessfulTestHashes( + for schemes: [Scheme], + graph: Graph, + mapperEnvironment: MapperEnvironment, + cacheStorage: CacheStoring + ) async throws { + let targets: [GraphTarget] = testActionTargets( + for: schemes, + graph: graph + ) + guard let initialGraph = mapperEnvironment.initialGraph else { return } + let graphTraverser = GraphTraverser(graph: initialGraph) + + let testedGraphTargets: [GraphTarget] = targets.compactMap { + guard let project = initialGraph.projects[$0.path], + let target = project.targets[$0.target.name] else { return nil } + return GraphTarget(path: $0.path, target: target, project: project) + } + try await fileHandler.inTemporaryDirectory { _ in + let allTestedTargets: Set = Set( + graphTraverser.allTargetDependencies(traversingFromTargets: testedGraphTargets) + .union(testedGraphTargets).map(\.target) + ) + let hashes = mapperEnvironment.testsCacheUntestedHashes.filter { element in + allTestedTargets.contains(where: { $0.bundleId == element.key.bundleId }) + } + + let cacheableItems: [CacheStorableItem: [AbsolutePath]] = hashes + .reduce(into: [:]) { acc, element in + acc[CacheStorableItem(name: element.key.name, hash: element.value)] = [AbsolutePath]() + } + + try await cacheStorage.store(cacheableItems, cacheCategory: .selectiveTests) + } + } + + /// - Returns: Result bundle path to use. Either passed by the user or a path in the Tuist cache + private func resultBundlePath( + passedResultBundlePath: AbsolutePath?, + runId: String, + config: Config + ) throws -> AbsolutePath? { + let runResultBundlePath = try cacheDirectoryProviderFactory.cacheDirectories() + .cacheDirectory(for: .runs) + .appending(components: runId, Constants.resultBundleName) + + if config.fullHandle == nil { + return passedResultBundlePath + } else { + return passedResultBundlePath ?? runResultBundlePath + } + } + // swiftlint:disable:next function_body_length private func testScheme( scheme: Scheme, @@ -314,7 +466,8 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration? + testPlanConfiguration: TestPlanConfiguration?, + passthroughXcodeBuildArguments: [String] ) async throws { logger.log(level: .notice, "Testing scheme \(scheme.name)", metadata: .section) if let testPlan = testPlanConfiguration?.testPlan, let testPlans = scheme.testAction?.testPlans, @@ -336,10 +489,10 @@ public final class TestService { // swiftlint:disable:this type_body_length throw TestServiceError.schemeWithoutTestableTargets(scheme: scheme.name, testPlan: testPlanConfiguration?.testPlan) } - let buildPlatform: TuistGraph.Platform + let buildPlatform: XcodeGraph.Platform - if let platform, let inputPlatform = TuistGraph.Platform(rawValue: platform) { - buildPlatform = inputPlatform + if let platform { + buildPlatform = try XcodeGraph.Platform.from(commandLineValue: platform) } else { buildPlatform = try buildableTarget.target.servicePlatform } @@ -371,8 +524,8 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: retryCount, testTargets: testTargets, skipTestTargets: skipTestTargets, - testPlanConfiguration: testPlanConfiguration + testPlanConfiguration: testPlanConfiguration, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) - .printFormattedOutput() } } diff --git a/Sources/TuistKit/Services/TuistService.swift b/Sources/TuistKit/Services/TuistService.swift index 089e0911edc..2c5f55d7dc3 100644 --- a/Sources/TuistKit/Services/TuistService.swift +++ b/Sources/TuistKit/Services/TuistService.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistLoader import TuistPlugin @@ -24,7 +24,7 @@ final class TuistService: NSObject { func run( arguments: [String], tuistBinaryPath: String - ) throws { + ) async throws { var arguments = arguments let commandName = "tuist-\(arguments[0])" @@ -39,11 +39,21 @@ final class TuistService: NSObject { path = FileHandler.shared.currentPath } - let config = try configLoader.loadConfig(path: path) - let pluginExecutables = try pluginService.remotePluginPaths(using: config) + let config = try await configLoader.loadConfig(path: path) + + var pluginPaths = try pluginService.remotePluginPaths(using: config) .compactMap(\.releasePath) + + if let pluginPath: String = ProcessInfo.processInfo.environment["TUIST_CONFIG_PLUGIN_BINARY_PATH"] { + let absolutePath = try AbsolutePath(validating: pluginPath) + logger.debug("Using plugin absolutePath \(absolutePath.description)", metadata: .subsection) + pluginPaths.append(absolutePath) + } + + let pluginExecutables = try pluginPaths .flatMap(FileHandler.shared.contentsOfDirectory) .filter { $0.basename.hasPrefix("tuist-") } + if let pluginCommand = pluginExecutables.first(where: { $0.basename == commandName }) { arguments[0] = pluginCommand.pathString } else if System.shared.commandExists(commandName) { @@ -57,9 +67,6 @@ final class TuistService: NSObject { verbose: Environment.shared.isVerbose, environment: [ Constants.EnvironmentVariables.tuistBinaryPath: tuistBinaryPath, - Constants.EnvironmentVariables.forceConfigCacheDirectory: Environment.shared.tuistConfigVariables[ - Constants.EnvironmentVariables.forceConfigCacheDirectory - ] ?? "", ].merging(System.shared.env) { tuistEnv, _ in tuistEnv } ) } diff --git a/Sources/TuistKit/Services/VersionService.swift b/Sources/TuistKit/Services/VersionService.swift deleted file mode 100644 index 12179c35811..00000000000 --- a/Sources/TuistKit/Services/VersionService.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -final class VersionService { - func run() throws { - logger.notice("\(Constants.version)") - } -} diff --git a/Sources/TuistKit/Utils/ManifestGraphLoader.swift b/Sources/TuistKit/Utils/ManifestGraphLoader.swift index ec17c152e10..5c474de4ca6 100644 --- a/Sources/TuistKit/Utils/ManifestGraphLoader.swift +++ b/Sources/TuistKit/Utils/ManifestGraphLoader.swift @@ -1,12 +1,13 @@ import Foundation +import Mockable +import Path import ProjectDescription -import TSCBasic import TuistCore import TuistDependencies -import TuistGraph import TuistLoader import TuistPlugin import TuistSupport +import XcodeGraph /// A utility for loading a graph for a given Manifest path on disk /// @@ -16,10 +17,11 @@ import TuistSupport /// - A graph is loaded from the models /// /// - Note: This is a simplified implementation that loads a graph without applying any mappers or running any linters +@Mockable public protocol ManifestGraphLoading { /// Loads a Workspace or Project Graph at a given path based on manifest availability /// - Note: This will search for a Workspace manifest first, then fallback to searching for a Project manifest - func load(path: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor], [LintingIssue]) + func load(path: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor], MapperEnvironment, [LintingIssue]) // swiftlint:disable:previous large_tuple } @@ -30,11 +32,13 @@ public final class ManifestGraphLoader: ManifestGraphLoading { private let converter: ManifestModelConverting private let graphLoader: GraphLoading private let pluginsService: PluginServicing - private let dependenciesGraphController: DependenciesGraphControlling + private let swiftPackageManagerGraphLoader: SwiftPackageManagerGraphLoading private let graphLoaderLinter: CircularDependencyLinting private let manifestLinter: ManifestLinting private let workspaceMapper: WorkspaceMapping private let graphMapper: GraphMapping + private let packageSettingsLoader: PackageSettingsLoading + private let manifestFilesLocator: ManifestFilesLocating public convenience init( manifestLoader: ManifestLoading, @@ -50,11 +54,13 @@ public final class ManifestGraphLoader: ManifestGraphLoading { ), graphLoader: GraphLoader(), pluginsService: PluginService(manifestLoader: manifestLoader), - dependenciesGraphController: DependenciesGraphController(), + swiftPackageManagerGraphLoader: SwiftPackageManagerGraphLoader(manifestLoader: manifestLoader), graphLoaderLinter: CircularDependencyLinter(), manifestLinter: ManifestLinter(), workspaceMapper: workspaceMapper, - graphMapper: graphMapper + graphMapper: graphMapper, + packageSettingsLoader: PackageSettingsLoader(manifestLoader: manifestLoader), + manifestFilesLocator: ManifestFilesLocator() ) } @@ -65,11 +71,13 @@ public final class ManifestGraphLoader: ManifestGraphLoading { converter: ManifestModelConverting, graphLoader: GraphLoading, pluginsService: PluginServicing, - dependenciesGraphController: DependenciesGraphControlling, + swiftPackageManagerGraphLoader: SwiftPackageManagerGraphLoading, graphLoaderLinter: CircularDependencyLinting, manifestLinter: ManifestLinting, workspaceMapper: WorkspaceMapping, - graphMapper: GraphMapping + graphMapper: GraphMapping, + packageSettingsLoader: PackageSettingsLoading, + manifestFilesLocator: ManifestFilesLocating ) { self.configLoader = configLoader self.manifestLoader = manifestLoader @@ -77,31 +85,69 @@ public final class ManifestGraphLoader: ManifestGraphLoading { self.converter = converter self.graphLoader = graphLoader self.pluginsService = pluginsService - self.dependenciesGraphController = dependenciesGraphController + self.swiftPackageManagerGraphLoader = swiftPackageManagerGraphLoader self.graphLoaderLinter = graphLoaderLinter self.manifestLinter = manifestLinter self.workspaceMapper = workspaceMapper self.graphMapper = graphMapper + self.packageSettingsLoader = packageSettingsLoader + self.manifestFilesLocator = manifestFilesLocator } - // swiftlint:disable:next large_tuple - public func load(path: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor], [LintingIssue]) { - try manifestLoader.validateHasProjectOrWorkspaceManifest(at: path) + // swiftlint:disable:next function_body_length large_tuple + public func load(path: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor], MapperEnvironment, [LintingIssue]) { + try manifestLoader.validateHasRootManifest(at: path) // Load Plugins let plugins = try await loadPlugins(at: path) + // Load Workspace + var allManifests = try await recursiveManifestLoader.loadWorkspace(at: path) + let isSPMProjectOnly = allManifests.projects.isEmpty + let hasExternalDependencies = allManifests.projects.values.contains { $0.containsExternalDependencies } + // Load DependenciesGraph - let dependenciesGraph = try dependenciesGraphController.load(at: path) - let allManifests = try recursiveManifestLoader.loadWorkspace(at: path) + let dependenciesGraph: XcodeGraph.DependenciesGraph + let packageSettings: TuistCore.PackageSettings? + + // Load SPM graph only if is SPM Project only or the workspace is using external dependencies + if let packagePath = manifestFilesLocator.locatePackageManifest(at: path), + isSPMProjectOnly || hasExternalDependencies + { + let loadedPackageSettings = try await packageSettingsLoader.loadPackageSettings( + at: packagePath.parentDirectory, + with: plugins + ) + + let manifest = try await swiftPackageManagerGraphLoader.load( + packagePath: packagePath, + packageSettings: loadedPackageSettings + ) + dependenciesGraph = try converter.convert(manifest: manifest, path: path) + packageSettings = loadedPackageSettings + } else { + packageSettings = nil + dependenciesGraph = .none + } + + // Merge SPM graph + if let packageSettings { + allManifests = try await recursiveManifestLoader.loadAndMergePackageProjects( + in: allManifests, + packageSettings: packageSettings + ) + } + let (workspaceModels, manifestProjects) = ( try converter.convert(manifest: allManifests.workspace, path: allManifests.path), allManifests.projects ) // Lint Manifests - let lintingIssues = manifestProjects.flatMap { manifestLinter.lint(project: $0.value) } + let workspaceLintingIssues = manifestLinter.lint(workspace: allManifests.workspace) + let projectLintingIssues = manifestProjects.flatMap { manifestLinter.lint(project: $0.value) } + let lintingIssues = workspaceLintingIssues + projectLintingIssues try lintingIssues.printAndThrowErrorsIfNeeded() // Convert to models @@ -128,11 +174,15 @@ public final class ManifestGraphLoader: ManifestGraphLoading { ) // Apply graph mappers - let (mappedGraph, graphMapperSideEffects) = try await graphMapper.map(graph: graph) + let (mappedGraph, graphMapperSideEffects, environment) = try await graphMapper.map( + graph: graph, + environment: MapperEnvironment() + ) return ( mappedGraph, modelMapperSideEffects + graphMapperSideEffects, + environment, lintingIssues ) } @@ -140,9 +190,9 @@ public final class ManifestGraphLoader: ManifestGraphLoading { private func convert( projects: [AbsolutePath: ProjectDescription.Project], plugins: Plugins, - externalDependencies: [String: [TuistGraph.TargetDependency]], + externalDependencies: [String: [XcodeGraph.TargetDependency]], context: ExecutionContext = .concurrent - ) throws -> [TuistGraph.Project] { + ) throws -> [XcodeGraph.Project] { let tuples = projects.map { (path: $0.key, manifest: $0.value) } return try tuples.map(context: context) { try converter.convert( @@ -157,7 +207,7 @@ public final class ManifestGraphLoader: ManifestGraphLoading { @discardableResult func loadPlugins(at path: AbsolutePath) async throws -> Plugins { - let config = try configLoader.loadConfig(path: path) + let config = try await configLoader.loadConfig(path: path) let plugins = try await pluginsService.loadPlugins(using: config) try manifestLoader.register(plugins: plugins) return plugins diff --git a/Sources/TuistKit/Utils/Target+PlatformResolution.swift b/Sources/TuistKit/Utils/Target+PlatformResolution.swift index 5a8daaa61c7..91f23726916 100644 --- a/Sources/TuistKit/Utils/Target+PlatformResolution.swift +++ b/Sources/TuistKit/Utils/Target+PlatformResolution.swift @@ -1,7 +1,10 @@ import Foundation -import TuistGraph +import TuistSupport +import XcodeGraph + +struct UnspecifiedPlatformError: FatalError, CustomStringConvertible { + var type: TuistSupport.ErrorType = .abort -struct UnspecifiedPlatformError: Error, CustomStringConvertible { let target: Target var description: String { "Only single platform targets supported. The target \(target.name) specifies multiple supported platforms (\(target.supportedPlatforms.map(\.rawValue).joined(separator: ", ")))." diff --git a/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift b/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift new file mode 100644 index 00000000000..33f6680d6f6 --- /dev/null +++ b/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift @@ -0,0 +1,95 @@ +import FileSystem +import Foundation +import Path +import TuistAnalytics +import TuistAsyncQueue +import TuistCore +import TuistServer +import TuistSupport +import XcodeGraph + +public class TuistAnalyticsServerBackend: TuistAnalyticsBackend { + private let fullHandle: String + private let url: URL + private let createCommandEventService: CreateCommandEventServicing + private let fileHandler: FileHandling + private let ciChecker: CIChecking + private let cacheDirectoriesProviderFactory: CacheDirectoriesProviderFactoring + private let analyticsArtifactUploadService: AnalyticsArtifactUploadServicing + private let fileSystem: FileSystem + + public convenience init( + fullHandle: String, + url: URL + ) { + self.init( + fullHandle: fullHandle, + url: url, + createCommandEventService: CreateCommandEventService(), + fileHandler: FileHandler.shared, + ciChecker: CIChecker(), + cacheDirectoriesProviderFactory: CacheDirectoriesProviderFactory(), + analyticsArtifactUploadService: AnalyticsArtifactUploadService(), + fileSystem: FileSystem() + ) + } + + public init( + fullHandle: String, + url: URL, + createCommandEventService: CreateCommandEventServicing, + fileHandler: FileHandling, + ciChecker: CIChecking, + cacheDirectoriesProviderFactory: CacheDirectoriesProviderFactoring, + analyticsArtifactUploadService: AnalyticsArtifactUploadServicing, + fileSystem: FileSystem + ) { + self.fullHandle = fullHandle + self.url = url + self.createCommandEventService = createCommandEventService + self.fileHandler = fileHandler + self.ciChecker = ciChecker + self.cacheDirectoriesProviderFactory = cacheDirectoriesProviderFactory + self.analyticsArtifactUploadService = analyticsArtifactUploadService + self.fileSystem = fileSystem + } + + public func send(commandEvent: CommandEvent) async throws { + let cloudCommandEvent = try await createCommandEventService.createCommandEvent( + commandEvent: commandEvent, + projectId: fullHandle, + serverURL: url + ) + + let runDirectory = try cacheDirectoriesProviderFactory.cacheDirectories() + .cacheDirectory(for: .runs) + .appending(component: commandEvent.runId) + + let resultBundle = runDirectory + .appending(component: "\(Constants.resultBundleName).xcresult") + + if fileHandler.exists(resultBundle), + let targetHashes = commandEvent.params["target_hashes"]?.value as? [GraphTarget: String], + let graphPath = commandEvent.params["graph_path"]?.value as? AbsolutePath + { + try await analyticsArtifactUploadService.uploadResultBundle( + resultBundle, + targetHashes: targetHashes, + graphPath: graphPath, + commandEventId: cloudCommandEvent.id, + serverURL: url + ) + } + + if fileHandler.exists(runDirectory) { + try await fileSystem.remove(runDirectory) + } + + if #available(macOS 13.0, *), ciChecker.isCI() { + logger + .info( + "You can view a detailed report at: \(cloudCommandEvent.url.absoluteString)" + ) + } + } +} diff --git a/Sources/TuistKit/Utils/TuistVersionLoader.swift b/Sources/TuistKit/Utils/TuistVersionLoader.swift new file mode 100644 index 00000000000..b512b37e0a2 --- /dev/null +++ b/Sources/TuistKit/Utils/TuistVersionLoader.swift @@ -0,0 +1,20 @@ +import Foundation +import TuistSupport + +protocol TuistVersionLoading { + func getVersion() throws -> String +} + +final class TuistVersionLoader: TuistVersionLoading { + private let system: Systeming + + init(system: Systeming = System.shared) { + self.system = system + } + + func getVersion() throws -> String { + try system + .capture(["tuist", "version"]) + .spm_chomp() + } +} diff --git a/Sources/TuistLoader/Linter/ManifestLinter.swift b/Sources/TuistLoader/Linter/ManifestLinter.swift index 0ca318bc37e..7b1a9821bf8 100644 --- a/Sources/TuistLoader/Linter/ManifestLinter.swift +++ b/Sources/TuistLoader/Linter/ManifestLinter.swift @@ -5,18 +5,32 @@ import TuistSupport public protocol ManifestLinting { func lint(project: ProjectDescription.Project) -> [LintingIssue] + func lint(workspace: ProjectDescription.Workspace) -> [LintingIssue] } public class AnyManifestLinter: ManifestLinting { - let lint: ((ProjectDescription.Project) -> [LintingIssue])? - - public init(lint: ((ProjectDescription.Project) -> [LintingIssue])? = nil) { - self.lint = lint + let lintProject: ((ProjectDescription.Project) -> [LintingIssue])? + let lintWorkspace: ((ProjectDescription.Workspace) -> [LintingIssue])? + + public init( + lintProject: ((ProjectDescription.Project) -> [LintingIssue])? = nil, + lintWorkspace: ((ProjectDescription.Workspace) -> [LintingIssue])? = nil + ) { + self.lintProject = lintProject + self.lintWorkspace = lintWorkspace } public func lint(project: ProjectDescription.Project) -> [LintingIssue] { - if let lint { - return lint(project) + if let lintProject { + return lintProject(project) + } else { + return [] + } + } + + public func lint(workspace: ProjectDescription.Workspace) -> [LintingIssue] { + if let lintWorkspace { + return lintWorkspace(workspace) } else { return [] } @@ -39,6 +53,126 @@ public class ManifestLinter: ManifestLinting { return issues } + public func lint(workspace: ProjectDescription.Workspace) -> [LintingIssue] { + var issues = [LintingIssue]() + + for scheme in workspace.schemes { + issues.append(contentsOf: lintSchemeActions( + buildAction: scheme.buildAction, + runAction: scheme.runAction, + profileAction: scheme.profileAction, + testAction: scheme.testAction, + scheme: scheme + )) + } + + return issues + } + + private func lintSchemeActions( + buildAction: BuildAction?, + runAction: RunAction?, + profileAction: ProfileAction?, + testAction: TestAction?, + scheme: Scheme + ) -> [LintingIssue] { + var issues = [LintingIssue]() + + if let buildAction { + issues.append(contentsOf: lintExecutionActionTargets( + buildAction.preActions, + actionType: "buildAction", + scheme: scheme + )) + issues.append(contentsOf: lintExecutionActionTargets( + buildAction.postActions, + actionType: "buildAction", + scheme: scheme + )) + issues.append(contentsOf: lintSchemeTargets(buildAction.targets, actionType: "buildAction", scheme: scheme)) + } + + if let runAction { + issues.append(contentsOf: lintExecutionActionTargets(runAction.preActions, actionType: "runAction", scheme: scheme)) + issues.append(contentsOf: lintExecutionActionTargets(runAction.postActions, actionType: "runAction", scheme: scheme)) + issues.append(contentsOf: lintSchemeTarget(runAction.executable, actionType: "runAction", scheme: scheme)) + issues.append(contentsOf: lintSchemeTarget( + runAction.expandVariableFromTarget, + actionType: "runAction", + scheme: scheme + )) + } + + if let profileAction { + issues.append(contentsOf: lintExecutionActionTargets( + profileAction.preActions, + actionType: "profileAction", + scheme: scheme + )) + issues.append(contentsOf: lintExecutionActionTargets( + profileAction.postActions, + actionType: "profileAction", + scheme: scheme + )) + issues.append(contentsOf: lintSchemeTarget(profileAction.executable, actionType: "profileAction", scheme: scheme)) + } + + if let testAction { + issues.append(contentsOf: lintExecutionActionTargets(testAction.preActions, actionType: "testAction", scheme: scheme)) + issues.append(contentsOf: lintExecutionActionTargets( + testAction.postActions, + actionType: "testAction", + scheme: scheme + )) + issues.append(contentsOf: lintSchemeTargets( + testAction.targets.map(\.target), + actionType: "testAction", + scheme: scheme + )) + } + + return issues + } + + private func lintExecutionActionTargets( + _ actions: [ExecutionAction], + actionType: String, + scheme: Scheme + ) -> [LintingIssue] { + let targets = actions.compactMap(\.target) + return lintSchemeTargets(targets, actionType: actionType, scheme: scheme) + } + + private func lintSchemeTargets( + _ targets: [TargetReference], + actionType: String, + scheme: Scheme + ) -> [LintingIssue] { + return targets.flatMap { lintSchemeTarget($0, actionType: actionType, scheme: scheme) } + } + + private func lintSchemeTarget( + _ targetReference: TargetReference?, + actionType: String, + scheme: Scheme + ) -> [LintingIssue] { + guard let targetReference else { return [] } + guard targetReference.projectPath == nil else { return [] } + + return [ + LintingIssue( + reason: """ + Workspace.swift: The target '\(targetReference.targetName)' in the \(actionType) of the scheme '\( + scheme + .name + )' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ), + ] + } + private func lintDuplicates(project: ProjectDescription.Project) -> [LintingIssue] { let targetsNames = project.targets.map(\.name) diff --git a/Sources/TuistLoader/Loaders/CachedManifestLoader.swift b/Sources/TuistLoader/Loaders/CachedManifestLoader.swift index 5e329fbcb2d..86e3685be84 100644 --- a/Sources/TuistLoader/Loaders/CachedManifestLoader.swift +++ b/Sources/TuistLoader/Loaders/CachedManifestLoader.swift @@ -1,9 +1,7 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import struct TuistGraph.Config -import struct TuistGraph.Plugins import TuistSupport /// Cached Manifest Loader @@ -22,9 +20,9 @@ public class CachedManifestLoader: ManifestLoading { private let tuistVersion: String private let decoder = JSONDecoder() private let encoder = JSONEncoder() - @Atomic private var helpersCache: [AbsolutePath: String?] = [:] - @Atomic private var pluginsHashCache: String? - @Atomic private var cacheDirectory: AbsolutePath! + private let helpersCache: ThreadSafe<[AbsolutePath: String?]> = ThreadSafe([:]) + private let pluginsHashCache: ThreadSafe = ThreadSafe(nil) + private let cacheDirectory: ThrowableCaching public convenience init(manifestLoader: ManifestLoading = ManifestLoader()) { let environment = TuistSupport.Environment.shared @@ -55,69 +53,74 @@ public class CachedManifestLoader: ManifestLoading { self.environment = environment self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory self.tuistVersion = tuistVersion + cacheDirectory = ThrowableCaching { + try cacheDirectoryProviderFactory.cacheDirectories().cacheDirectory(for: .manifests) + } } - public func loadConfig(at path: AbsolutePath) throws -> ProjectDescription.Config { - try load(manifest: .config, at: path) { - let projectDescriptionConfig = try manifestLoader.loadConfig(at: path) - let config = try TuistGraph.Config.from(manifest: projectDescriptionConfig, at: path) - cacheDirectory = try cacheDirectoryProviderFactory.cacheDirectories(config: config).cacheDirectory(for: .manifests) + public func loadConfig(at path: AbsolutePath) async throws -> ProjectDescription.Config { + try await load(manifest: .config, at: path) { + let projectDescriptionConfig = try await manifestLoader.loadConfig(at: path) return projectDescriptionConfig } } - public func loadProject(at path: AbsolutePath, rootPath: AbsolutePath? = nil) throws -> Project { - try load(manifest: .project, at: path) { - try manifestLoader.loadProject(at: path, rootPath: rootPath) + public func loadProject(at path: AbsolutePath, rootPath: AbsolutePath? = nil) async throws -> Project { + try await load(manifest: .project, at: path) { + try await manifestLoader.loadProject(at: path, rootPath: rootPath) } } - public func loadWorkspace(at path: AbsolutePath) throws -> Workspace { - try load(manifest: .workspace, at: path) { - try manifestLoader.loadWorkspace(at: path) + public func loadWorkspace(at path: AbsolutePath) async throws -> Workspace { + try await load(manifest: .workspace, at: path) { + try await manifestLoader.loadWorkspace(at: path) } } - public func loadTemplate(at path: AbsolutePath) throws -> Template { - try load(manifest: .template, at: path) { - try manifestLoader.loadTemplate(at: path) + public func loadTemplate(at path: AbsolutePath) async throws -> ProjectDescription.Template { + try await load(manifest: .template, at: path) { + try await manifestLoader.loadTemplate(at: path) } } - public func loadPlugin(at path: AbsolutePath) throws -> Plugin { - try load(manifest: .plugin, at: path) { - try manifestLoader.loadPlugin(at: path) + public func loadPlugin(at path: AbsolutePath) async throws -> ProjectDescription.Plugin { + try await load(manifest: .plugin, at: path) { + try await manifestLoader.loadPlugin(at: path) } } - public func loadDependencies(at path: AbsolutePath) throws -> Dependencies { - try manifestLoader.loadDependencies(at: path) + public func loadPackageSettings(at path: AbsolutePath) async throws -> ProjectDescription.PackageSettings { + try await load(manifest: .packageSettings, at: path) { + try await manifestLoader.loadPackageSettings(at: path) + } } - public func loadPackageSettings(at path: AbsolutePath) throws -> PackageSettings { - try manifestLoader.loadPackageSettings(at: path) + public func loadPackage(at path: AbsolutePath) async throws -> PackageInfo { + try await load(manifest: .package, at: path) { + try await manifestLoader.loadPackage(at: path) + } } public func manifests(at path: AbsolutePath) -> Set { manifestLoader.manifests(at: path) } - public func validateHasProjectOrWorkspaceManifest(at path: AbsolutePath) throws { - try manifestLoader.validateHasProjectOrWorkspaceManifest(at: path) + public func validateHasRootManifest(at path: AbsolutePath) throws { + try manifestLoader.validateHasRootManifest(at: path) + } + + public func hasRootManifest(at path: AbsolutePath) -> Bool { + manifestLoader.hasRootManifest(at: path) } public func register(plugins: Plugins) throws { - pluginsHashCache = try calculatePluginsHash(for: plugins) + try pluginsHashCache.mutate { $0 = try calculatePluginsHash(for: plugins) } try manifestLoader.register(plugins: plugins) } // MARK: - Private - private func load(manifest: Manifest, at path: AbsolutePath, loader: () throws -> T) throws -> T { - if cacheDirectory == nil { - cacheDirectory = try cacheDirectoryProviderFactory.cacheDirectories(config: nil).cacheDirectory(for: .manifests) - } - + private func load(manifest: Manifest, at path: AbsolutePath, loader: () async throws -> T) async throws -> T { let manifestPath = path.appending(component: manifest.fileName(path)) guard fileHandler.exists(manifestPath) else { throw ManifestLoaderError.manifestNotFound(manifest, path) @@ -131,10 +134,10 @@ public class CachedManifestLoader: ManifestLoading { guard let hashes = calculatedHashes else { logger.warning("Unable to calculate manifest hash at path: \(path)") - return try loader() + return try await loader() } - let cachedManifestPath = cachedPath(for: manifestPath) + let cachedManifestPath = try cachedPath(for: manifestPath) if let cached: T = loadCachedManifest( at: cachedManifestPath, hashes: hashes @@ -142,7 +145,7 @@ public class CachedManifestLoader: ManifestLoading { return cached } - let loadedManifest = try loader() + let loadedManifest = try await loader() try cacheManifest( manifest: manifest, @@ -166,7 +169,7 @@ public class CachedManifestLoader: ManifestLoading { return Hashes( manifestHash: manifestHash, helpersHash: helpersHash, - pluginsHash: pluginsHashCache, + pluginsHash: pluginsHashCache.value, environmentHash: environmentHash ) } @@ -183,14 +186,16 @@ public class CachedManifestLoader: ManifestLoading { return nil } - if let cached = helpersCache[helpersDirectory] { - return cached - } + return try helpersCache.mutate { cache in + if let cached = cache[helpersDirectory] { + return cached + } - let hash = try projectDescriptionHelpersHasher.hash(helpersDirectory: helpersDirectory) - helpersCache[helpersDirectory] = hash + let hash = try projectDescriptionHelpersHasher.hash(helpersDirectory: helpersDirectory) + cache[helpersDirectory] = hash - return hash + return hash + } } private func calculatePluginsHash(for plugins: Plugins) throws -> String? { @@ -208,11 +213,11 @@ public class CachedManifestLoader: ManifestLoading { return tuistEnvVariables.joined(separator: "-").md5 } - private func cachedPath(for manifestPath: AbsolutePath) -> AbsolutePath { + private func cachedPath(for manifestPath: AbsolutePath) throws -> AbsolutePath { let pathHash = manifestPath.pathString.md5 let cacheVersion = CachedManifest.currentCacheVersion.description let fileName = [cacheVersion, pathHash].joined(separator: ".") - return cacheDirectory.appending(component: fileName) + return try cacheDirectory.value.appending(component: fileName) } private func loadCachedManifest( diff --git a/Sources/TuistLoader/Loaders/ConfigLoader.swift b/Sources/TuistLoader/Loaders/ConfigLoader.swift index 1a1c7279fa9..535a34426a8 100644 --- a/Sources/TuistLoader/Loaders/ConfigLoader.swift +++ b/Sources/TuistLoader/Loaders/ConfigLoader.swift @@ -1,10 +1,12 @@ import Foundation +import Mockable +import Path import struct ProjectDescription.Config -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph +@Mockable public protocol ConfigLoading { /// Loads the Tuist configuration by traversing the file system till the Config manifest is found, /// otherwise returns the default configuration. @@ -12,7 +14,7 @@ public protocol ConfigLoading { /// - Parameter path: Directory from which look up and load the Config. /// - Returns: Loaded Config object. /// - Throws: An error if the Config.swift can't be parsed. - func loadConfig(path: AbsolutePath) throws -> TuistGraph.Config + func loadConfig(path: AbsolutePath) async throws -> TuistCore.Config /// Locates the Config.swift manifest from the given directory. func locateConfig(at: AbsolutePath) -> AbsolutePath? @@ -22,7 +24,7 @@ public final class ConfigLoader: ConfigLoading { private let manifestLoader: ManifestLoading private let rootDirectoryLocator: RootDirectoryLocating private let fileHandler: FileHandling - private var cachedConfigs: [AbsolutePath: TuistGraph.Config] = [:] + private var cachedConfigs: [AbsolutePath: TuistCore.Config] = [:] public init( manifestLoader: ManifestLoading = ManifestLoader(), @@ -34,19 +36,19 @@ public final class ConfigLoader: ConfigLoading { self.fileHandler = fileHandler } - public func loadConfig(path: AbsolutePath) throws -> TuistGraph.Config { + public func loadConfig(path: AbsolutePath) async throws -> TuistCore.Config { if let cached = cachedConfigs[path] { return cached } guard let configPath = locateConfig(at: path) else { - let config = TuistGraph.Config.default + let config = TuistCore.Config.default cachedConfigs[path] = config return config } - let manifest = try manifestLoader.loadConfig(at: configPath.parentDirectory) - let config = try TuistGraph.Config.from(manifest: manifest, at: configPath) + let manifest = try await manifestLoader.loadConfig(at: configPath.parentDirectory) + let config = try TuistCore.Config.from(manifest: manifest, at: configPath) cachedConfigs[path] = config return config } diff --git a/Sources/TuistLoader/Loaders/DependenciesModelLoader.swift b/Sources/TuistLoader/Loaders/DependenciesModelLoader.swift deleted file mode 100644 index e22f9ecb636..00000000000 --- a/Sources/TuistLoader/Loaders/DependenciesModelLoader.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -/// Entity responsible for providing dependencies model. -public protocol DependenciesModelLoading { - /// Load the Dependencies model at the specified path. - /// - Parameter path: The absolute path for the dependency models to load. - /// - Parameter plugins: The plugins for the dependency models to load. - /// - Returns: The Dependencies loaded from the specified path. - /// - Throws: Error encountered during the loading process (e.g. Missing Dependencies file). - func loadDependencies(at path: AbsolutePath, with plugins: Plugins) throws -> TuistGraph.Dependencies -} - -public class DependenciesModelLoader: DependenciesModelLoading { - private let manifestLoader: ManifestLoading - - public init(manifestLoader: ManifestLoading = ManifestLoader()) { - self.manifestLoader = manifestLoader - } - - public func loadDependencies(at path: AbsolutePath, with plugins: Plugins) throws -> TuistGraph.Dependencies { - try manifestLoader.register(plugins: plugins) - let manifest = try manifestLoader.loadDependencies(at: path) - let generatorPaths = GeneratorPaths(manifestDirectory: path) - - return try TuistGraph.Dependencies.from( - manifest: manifest, - generatorPaths: generatorPaths - ) - } -} diff --git a/Sources/TuistLoader/Loaders/ManifestLoader.swift b/Sources/TuistLoader/Loaders/ManifestLoader.swift index 3619620e81f..6db6f8a456d 100644 --- a/Sources/TuistLoader/Loaders/ManifestLoader.swift +++ b/Sources/TuistLoader/Loaders/ManifestLoader.swift @@ -1,9 +1,10 @@ import Foundation +import Mockable +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public enum ManifestLoaderError: FatalError, Equatable { case projectDescriptionNotFound(AbsolutePath) @@ -50,51 +51,55 @@ public enum ManifestLoaderError: FatalError, Equatable { } } +@Mockable public protocol ManifestLoading { /// Loads the Config.swift in the given directory. /// /// - Parameter path: Path to the directory that contains the Config.swift file. /// - Returns: Loaded Config.swift file. /// - Throws: An error if the file has a syntax error. - func loadConfig(at path: AbsolutePath) throws -> ProjectDescription.Config + func loadConfig(at path: AbsolutePath) async throws -> ProjectDescription.Config /// Loads the Project.swift in the given directory. /// - Parameter path: Path to the directory that contains the Project.swift. - func loadProject(at path: AbsolutePath, rootPath: AbsolutePath?) throws -> ProjectDescription.Project + func loadProject(at path: AbsolutePath, rootPath: AbsolutePath?) async throws -> ProjectDescription.Project /// Loads the Workspace.swift in the given directory. /// - Parameter path: Path to the directory that contains the Workspace.swift - func loadWorkspace(at path: AbsolutePath) throws -> ProjectDescription.Workspace + func loadWorkspace(at path: AbsolutePath) async throws -> ProjectDescription.Workspace /// Loads the name_of_template.swift in the given directory. /// - Parameter path: Path to the directory that contains the name_of_template.swift - func loadTemplate(at path: AbsolutePath) throws -> ProjectDescription.Template - - /// Loads the Dependencies.swift in the given directory - /// - Parameters: - /// - Parameter path: Path to the directory that contains the Package.swift - func loadDependencies(at path: AbsolutePath) throws -> ProjectDescription.Dependencies + func loadTemplate(at path: AbsolutePath) async throws -> ProjectDescription.Template /// 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 + /// - path: Path to the directory that contains Package.swift + func loadPackageSettings(at path: AbsolutePath) async throws -> ProjectDescription.PackageSettings + + /// Loads `Package.swift` + /// - path: Path to the directory that contains Package.swift + func loadPackage(at path: AbsolutePath) async throws -> PackageInfo /// 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 + func loadPlugin(at path: AbsolutePath) async throws -> ProjectDescription.Plugin /// List all the manifests in the given directory. /// - Parameter path: Path to the directory whose manifest files will be returned. func manifests(at path: AbsolutePath) -> Set /// Verifies that there is a project or workspace manifest at the given path, or throws an error otherwise. - func validateHasProjectOrWorkspaceManifest(at path: AbsolutePath) throws + func validateHasRootManifest(at path: AbsolutePath) throws + + /// - Returns: `true` if there is a project or workspace manifest at the given path + func hasRootManifest(at path: AbsolutePath) -> Bool /// Registers plugins that will be used within the manifest loading process. /// - Parameter plugins: The plugins to register. func register(plugins: Plugins) throws } +// swiftlint:disable:next type_body_length public class ManifestLoader: ManifestLoading { // MARK: - Static @@ -123,7 +128,7 @@ public class ManifestLoader: ManifestLoading { projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactory(), manifestFilesLocator: ManifestFilesLocator(), xcodeController: XcodeController.shared, - swiftPackageManagerController: SwiftPackageManagerController() + swiftPackageManagerController: SwiftPackageManagerController(system: System.shared, fileHandler: FileHandler.shared) ) } @@ -150,38 +155,43 @@ public class ManifestLoader: ManifestLoading { Set(manifestFilesLocator.locateManifests(at: path).map(\.0)) } - public func validateHasProjectOrWorkspaceManifest(at path: AbsolutePath) throws { - let manifests = manifests(at: path) - guard manifests.contains(.workspace) || manifests.contains(.project) else { + public func validateHasRootManifest(at path: AbsolutePath) throws { + guard hasRootManifest(at: path) else { throw ManifestLoaderError.manifestNotFound(path) } } - public func loadConfig(at path: AbsolutePath) throws -> ProjectDescription.Config { - try loadManifest(.config, at: path) + public func hasRootManifest(at path: AbsolutePath) -> Bool { + let manifests = manifests(at: path) + let rootManifests: Set = [.workspace, .project, .package] + return !manifests.isDisjoint(with: rootManifests) + } + + public func loadProject(at path: AbsolutePath) throws -> ProjectDescription.Project { + try loadManifest(.project, at: path) } - public func loadProject(at path: AbsolutePath, rootPath: AbsolutePath? = nil) throws -> ProjectDescription.Project { - try loadManifest(.project, at: path, rootPath: rootPath) + public func loadProject(at path: AbsolutePath) async throws -> ProjectDescription.Project { + try await loadManifest(.project, at: path) } - public func loadWorkspace(at path: AbsolutePath) throws -> ProjectDescription.Workspace { - try loadManifest(.workspace, at: path) + public func loadWorkspace(at path: AbsolutePath) async throws -> ProjectDescription.Workspace { + try await loadManifest(.workspace, at: path) } - public func loadTemplate(at path: AbsolutePath) throws -> ProjectDescription.Template { - try loadManifest(.template, at: path) + public func loadTemplate(at path: AbsolutePath) async throws -> ProjectDescription.Template { + try await loadManifest(.template, at: path) } - public func loadDependencies(at path: AbsolutePath) throws -> ProjectDescription.Dependencies { - let dependencyPath = path.appending(components: Constants.tuistDirectoryName) - return try loadManifest(.dependencies, at: dependencyPath) + public func loadPackage(at path: AbsolutePath) throws -> PackageInfo { + try swiftPackageManagerController.loadPackageInfo( + at: path + ) } - public func loadPackageSettings(at path: AbsolutePath) throws -> ProjectDescription.PackageSettings { - let packageManifestPath = path.appending(components: Constants.tuistDirectoryName) + public func loadPackageSettings(at path: AbsolutePath) async throws -> ProjectDescription.PackageSettings { do { - return try loadManifest(.package, at: packageManifestPath) + return try await loadManifest(.packageSettings, at: path) } catch let error as ManifestLoaderError { switch error { case let .manifestLoadingFailed(path: _, data: data, context: _): @@ -196,8 +206,8 @@ public class ManifestLoader: ManifestLoading { } } - public func loadPlugin(at path: AbsolutePath) throws -> ProjectDescription.Plugin { - try loadManifest(.plugin, at: path) + public func loadPlugin(at path: AbsolutePath) async throws -> ProjectDescription.Plugin { + try await loadManifest(.plugin, at: path) } public func register(plugins: Plugins) throws { @@ -209,18 +219,14 @@ public class ManifestLoader: ManifestLoading { // swiftlint:disable:next function_body_length private func loadManifest( _ manifest: Manifest, - at path: AbsolutePath, - rootPath: AbsolutePath? = nil + at path: AbsolutePath ) throws -> T { let manifestPath = try manifestPath( manifest, at: path ) - // build root manifest file path - let rootManifestPath = try rootManifestPath(manifest, at: rootPath) - - let data = try loadDataForManifest(manifest, at: manifestPath, rootPath: rootManifestPath) + let data = try loadDataForManifest(manifest, at: manifestPath) do { return try decoder.decode(T.self, from: data) @@ -308,8 +314,7 @@ public class ManifestLoader: ManifestLoading { private func loadDataForManifest( _ manifest: Manifest, - at path: AbsolutePath, - rootPath: AbsolutePath? = nil + at path: AbsolutePath ) throws -> Data { let arguments = try buildArguments( manifest, @@ -337,8 +342,8 @@ public class ManifestLoader: ManifestLoading { let preManifestLogs = String(string[string.startIndex ..< startTokenRange.lowerBound]).chomp() let postManifestLogs = String(string[endTokenRange.upperBound ..< string.endIndex]).chomp() - if !preManifestLogs.isEmpty { logger.info("\(path.pathString): \(preManifestLogs)") } - if !postManifestLogs.isEmpty { logger.info("\(path.pathString):\(postManifestLogs)") } + if !preManifestLogs.isEmpty { logger.notice("\(path.pathString): \(preManifestLogs)") } + if !postManifestLogs.isEmpty { logger.notice("\(path.pathString):\(postManifestLogs)") } let manifest = string[startTokenRange.upperBound ..< endTokenRange.lowerBound] return manifest.data(using: .utf8)! @@ -353,18 +358,18 @@ public class ManifestLoader: ManifestLoading { private func buildArguments( _ manifest: Manifest, at path: AbsolutePath - ) throws -> [String] { + ) async throws -> [String] { let projectDescriptionPath = try resourceLocator.projectDescription() let searchPaths = ProjectDescriptionSearchPaths.paths(for: projectDescriptionPath) let frameworkName: String switch manifest { case .config, .plugin, - .dependencies, .project, .template, .workspace, - .package: + .package, + .packageSettings: frameworkName = "ProjectDescription" } var arguments = [ @@ -378,19 +383,18 @@ public class ManifestLoader: ManifestLoading { "-framework", frameworkName, ] let projectDescriptionHelpersCacheDirectory = try cacheDirectoryProviderFactory - .cacheDirectories(config: nil) + .cacheDirectories() .cacheDirectory(for: .projectDescriptionHelpers) - let projectDescriptionHelperArguments: [String] = try { + let projectDescriptionHelperArguments: [String] = try await { switch manifest { - case .config, .plugin: + case .config, .plugin, .package: return [] - case .dependencies, - .project, + case .project, .template, .workspace, - .package: - return try projectDescriptionHelpersBuilderFactory.projectDescriptionHelpersBuilder( + .packageSettings: + return try await projectDescriptionHelpersBuilderFactory.projectDescriptionHelpersBuilder( cacheDirectory: projectDescriptionHelpersCacheDirectory ) .build( @@ -407,7 +411,7 @@ public class ManifestLoader: ManifestLoading { }() let packageDescriptionArguments: [String] = try { - if case .package = manifest { + if case .packageSettings = manifest { guard let xcode = try xcodeController.selected() else { return [] } let packageVersion = try swiftPackageManagerController.getToolsVersion( at: path.parentDirectory @@ -444,7 +448,7 @@ public class ManifestLoader: ManifestLoading { if errorMessage.contains(defaultHelpersName) { logger.error("Cannot import \(defaultHelpersName) in \(manifest.fileName(path))") - logger.info("Project description helpers that depend on plugins are not allowed in \(manifest.fileName(path))") + logger.notice("Project description helpers that depend on plugins are not allowed in \(manifest.fileName(path))") } else if errorMessage.contains("import") { logger.error("Helper plugins are not allowed in \(manifest.fileName(path))") } diff --git a/Sources/TuistLoader/Loaders/ManifestLoaderFactory.swift b/Sources/TuistLoader/Loaders/ManifestLoaderFactory.swift index 4d03b1d78ce..0c8d047d62c 100644 --- a/Sources/TuistLoader/Loaders/ManifestLoaderFactory.swift +++ b/Sources/TuistLoader/Loaders/ManifestLoaderFactory.swift @@ -3,8 +3,8 @@ import TuistSupport public final class ManifestLoaderFactory { private let useCache: Bool - public convenience init() { - let cacheSetting = Environment.shared.tuistConfigVariables[Constants.EnvironmentVariables.cacheManifests, default: "1"] + public convenience init(environment: Environmenting = Environment.shared) { + let cacheSetting = environment.tuistVariables[Constants.EnvironmentVariables.cacheManifests, default: "1"] self.init(useCache: cacheSetting == "1") } diff --git a/Sources/TuistLoader/Loaders/ManifestModelConverter.swift b/Sources/TuistLoader/Loaders/ManifestModelConverter.swift index 426d59e9715..b2f9de4860e 100644 --- a/Sources/TuistLoader/Loaders/ManifestModelConverter.swift +++ b/Sources/TuistLoader/Loaders/ManifestModelConverter.swift @@ -1,22 +1,22 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// A component responsible for converting Manifests (`ProjectDescription`) to Models (`TuistCore`) public protocol ManifestModelConverting { - func convert(manifest: ProjectDescription.Workspace, path: AbsolutePath) throws -> TuistGraph.Workspace + func convert(manifest: ProjectDescription.Workspace, path: AbsolutePath) throws -> XcodeGraph.Workspace func convert( manifest: ProjectDescription.Project, path: AbsolutePath, plugins: Plugins, - externalDependencies: [String: [TuistGraph.TargetDependency]], + externalDependencies: [String: [XcodeGraph.TargetDependency]], isExternal: Bool - ) throws -> TuistGraph.Project - func convert(manifest: TuistCore.DependenciesGraph, path: AbsolutePath) throws -> TuistGraph.DependenciesGraph + ) throws -> XcodeGraph.Project + func convert(manifest: TuistCore.DependenciesGraph, path: AbsolutePath) throws -> XcodeGraph.DependenciesGraph } public final class ManifestModelConverter: ManifestModelConverting { @@ -50,11 +50,11 @@ public final class ManifestModelConverter: ManifestModelConverting { manifest: ProjectDescription.Project, path: AbsolutePath, plugins: Plugins, - externalDependencies: [String: [TuistGraph.TargetDependency]], + externalDependencies: [String: [XcodeGraph.TargetDependency]], isExternal: Bool - ) throws -> TuistGraph.Project { + ) throws -> XcodeGraph.Project { let generatorPaths = GeneratorPaths(manifestDirectory: path) - return try TuistGraph.Project.from( + return try XcodeGraph.Project.from( manifest: manifest, generatorPaths: generatorPaths, plugins: plugins, @@ -67,9 +67,9 @@ public final class ManifestModelConverter: ManifestModelConverting { public func convert( manifest: ProjectDescription.Workspace, path: AbsolutePath - ) throws -> TuistGraph.Workspace { + ) throws -> XcodeGraph.Workspace { let generatorPaths = GeneratorPaths(manifestDirectory: path) - let workspace = try TuistGraph.Workspace.from( + let workspace = try XcodeGraph.Workspace.from( manifest: manifest, path: path, generatorPaths: generatorPaths, @@ -81,12 +81,12 @@ public final class ManifestModelConverter: ManifestModelConverting { public func convert( manifest: TuistCore.DependenciesGraph, path: AbsolutePath - ) throws -> TuistGraph.DependenciesGraph { - var externalDependencies: [String: [TuistGraph.TargetDependency]] = .init() + ) throws -> XcodeGraph.DependenciesGraph { + var externalDependencies: [String: [XcodeGraph.TargetDependency]] = .init() externalDependencies = try manifest.externalDependencies.mapValues { targetDependencies in try targetDependencies.flatMap { targetDependencyManifest in - try TuistGraph.TargetDependency.from( + try XcodeGraph.TargetDependency.from( manifest: targetDependencyManifest, generatorPaths: GeneratorPaths(manifestDirectory: path), externalDependencies: [:] // externalDependencies manifest can't contain other external dependencies, @@ -94,7 +94,7 @@ public final class ManifestModelConverter: ManifestModelConverting { } } - let externalProjects = try [AbsolutePath: TuistGraph.Project]( + let externalProjects = try [AbsolutePath: XcodeGraph.Project]( uniqueKeysWithValues: manifest.externalProjects .map { project in let projectPath = try AbsolutePath(validating: project.key.pathString) diff --git a/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift b/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift index 2b733d0ea3c..ab69f4011b8 100644 --- a/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift +++ b/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift @@ -1,9 +1,10 @@ import Foundation +import Path import ProjectDescription -import TSCBasic +import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph /// Entity responsible for providing `PackageSettings`. public protocol PackageSettingsLoading { @@ -11,24 +12,43 @@ public protocol PackageSettingsLoading { /// - 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 + func loadPackageSettings(at path: AbsolutePath, with plugins: Plugins) async throws -> TuistCore.PackageSettings } public final class PackageSettingsLoader: PackageSettingsLoading { private let manifestLoader: ManifestLoading + private let swiftPackageManagerController: SwiftPackageManagerControlling + private let fileHandler: FileHandling + private let manifestFilesLocator: ManifestFilesLocating - public init(manifestLoader: ManifestLoading = ManifestLoader()) { + public init( + manifestLoader: ManifestLoading = ManifestLoader(), + swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController( + system: System.shared, + fileHandler: FileHandler.shared + ), + fileHandler: FileHandling = FileHandler.shared, + manifestFilesLocator: ManifestFilesLocating = ManifestFilesLocator() + ) { self.manifestLoader = manifestLoader + self.swiftPackageManagerController = swiftPackageManagerController + self.fileHandler = fileHandler + self.manifestFilesLocator = manifestFilesLocator } - public func loadPackageSettings(at path: AbsolutePath, with plugins: Plugins) throws -> TuistGraph.PackageSettings { + public func loadPackageSettings(at path: AbsolutePath, with plugins: Plugins) async throws -> TuistCore.PackageSettings { + let path = manifestFilesLocator.locatePackageManifest(at: path)?.parentDirectory ?? path try manifestLoader.register(plugins: plugins) - let manifest = try manifestLoader.loadPackageSettings(at: path) + let manifest = try await manifestLoader.loadPackageSettings(at: path) let generatorPaths = GeneratorPaths(manifestDirectory: path) + let swiftToolsVersion = try swiftPackageManagerController.getToolsVersion( + at: path + ) - return try TuistGraph.PackageSettings.from( + return try TuistCore.PackageSettings.from( manifest: manifest, - generatorPaths: generatorPaths + generatorPaths: generatorPaths, + swiftToolsVersion: swiftToolsVersion ) } } diff --git a/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift b/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift index 773484c72ce..f2a19477afe 100644 --- a/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift +++ b/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift @@ -1,12 +1,27 @@ import Foundation +import Path import ProjectDescription -import TSCBasic -import TuistGraph +import TuistCore import TuistSupport /// A component that can load a manifest and all its (transitive) manifest dependencies public protocol RecursiveManifestLoading { - func loadWorkspace(at path: AbsolutePath) throws -> LoadedWorkspace + /// Load manifest at path + /// - Parameter path: Path of the manifest + /// - Returns: Loaded manifest + func loadWorkspace( + at path: AbsolutePath + ) async throws -> LoadedWorkspace + + /// Load package projects and merge in the loaded manifest + /// - Parameters: + /// - loadedWorkspace: manifest to merge in + /// - packageSettings: custom SPM settings + /// - Returns: Loaded manifest + func loadAndMergePackageProjects( + in loadedWorkspace: LoadedWorkspace, + packageSettings: TuistCore.PackageSettings + ) async throws -> LoadedWorkspace } public struct LoadedProjects { @@ -22,19 +37,22 @@ public struct LoadedWorkspace { public class RecursiveManifestLoader: RecursiveManifestLoading { private let manifestLoader: ManifestLoading private let fileHandler: FileHandling + private let packageInfoMapper: PackageInfoMapping public init( manifestLoader: ManifestLoading = ManifestLoader(), - fileHandler: FileHandling = FileHandler.shared + fileHandler: FileHandling = FileHandler.shared, + packageInfoMapper: PackageInfoMapping = PackageInfoMapper() ) { self.manifestLoader = manifestLoader self.fileHandler = fileHandler + self.packageInfoMapper = packageInfoMapper } - public func loadWorkspace(at path: AbsolutePath) throws -> LoadedWorkspace { + public func loadWorkspace(at path: AbsolutePath) async throws -> LoadedWorkspace { let loadedWorkspace: ProjectDescription.Workspace? do { - loadedWorkspace = try manifestLoader.loadWorkspace(at: path) + loadedWorkspace = try await manifestLoader.loadWorkspace(at: path) } catch ManifestLoaderError.manifestNotFound { loadedWorkspace = nil } @@ -51,7 +69,7 @@ public class RecursiveManifestLoader: RecursiveManifestLoading { manifestLoader.manifests(at: $0).contains(.project) } - let projects = try loadProjects(rootPath: path, paths: projectPaths) + let projects = await LoadedProjects(projects: try loadProjects(rootPath: path, paths: projectPaths).projects) let workspace: ProjectDescription.Workspace if let loadedWorkspace { workspace = loadedWorkspace @@ -67,16 +85,77 @@ public class RecursiveManifestLoader: RecursiveManifestLoading { ) } + public func loadAndMergePackageProjects(in loadedWorkspace: LoadedWorkspace, packageSettings: TuistCore.PackageSettings) + async throws -> LoadedWorkspace + { + let generatorPaths = GeneratorPaths(manifestDirectory: loadedWorkspace.path) + let projectSearchPaths = loadedWorkspace.workspace.projects.isEmpty ? ["."] : loadedWorkspace.workspace.projects + let packagePaths = try projectSearchPaths.map { + try generatorPaths.resolve(path: $0) + }.flatMap { + fileHandler.glob($0, glob: "") + }.filter { + fileHandler.isFolder($0) && $0.basename != Constants.tuistDirectoryName + }.filter { + let manifests = manifestLoader.manifests(at: $0) + return manifests.contains(.package) && !manifests.contains(.project) && !manifests.contains(.workspace) && !$0 + .pathString.contains(".build/checkouts") + } + + let packageProjects = try await loadPackageProjects(paths: packagePaths, packageSettings: packageSettings) + + let projects = loadedWorkspace.projects.merging( + packageProjects.projects, + uniquingKeysWith: { _, newValue in newValue } + ) + + return LoadedWorkspace( + path: loadedWorkspace.path, + workspace: loadedWorkspace.workspace, + projects: projects + ) + } + // MARK: - Private - private func loadProjects(rootPath: AbsolutePath, paths: [AbsolutePath]) throws -> LoadedProjects { + private func loadPackageProjects( + paths: [AbsolutePath], + packageSettings: TuistCore.PackageSettings? + ) async throws -> LoadedProjects { + guard let packageSettings else { return LoadedProjects(projects: [:]) } + var cache = [AbsolutePath: ProjectDescription.Project]() + + var paths = Set(paths) + while !paths.isEmpty { + paths.subtract(cache.keys) + let projects = try await Array(paths).concurrentCompactMap { + let packageInfo = try await self.manifestLoader.loadPackage(at: $0) + return try self.packageInfoMapper.map( + packageInfo: packageInfo, + path: $0, + packageType: .local, + packageSettings: packageSettings, + packageToProject: [:] + ) + } + var newDependenciesPaths = Set() + for (path, project) in zip(paths, projects) { + cache[path] = project + newDependenciesPaths.formUnion(try dependencyPaths(for: project, path: path)) + } + paths = newDependenciesPaths + } + return LoadedProjects(projects: cache) + } + + private func loadProjects(rootPath: AbsolutePath, paths: [AbsolutePath]) async throws -> LoadedProjects { var cache = [AbsolutePath: ProjectDescription.Project]() var paths = Set(paths) while !paths.isEmpty { paths.subtract(cache.keys) - let projects = try Array(paths).map(context: ExecutionContext.concurrent) { - return try manifestLoader.loadProject(at: $0, rootPath: rootPath) + let projects = try await Array(paths).concurrentMap { + try await self.manifestLoader.loadProject(at: $0, rootPath: rootPath) } var newDependenciesPaths = Set() for (path, project) in zip(paths, projects) { diff --git a/Sources/TuistLoader/Loaders/SwiftPackageManagerGraphLoader.swift b/Sources/TuistLoader/Loaders/SwiftPackageManagerGraphLoader.swift new file mode 100644 index 00000000000..808c476623a --- /dev/null +++ b/Sources/TuistLoader/Loaders/SwiftPackageManagerGraphLoader.swift @@ -0,0 +1,188 @@ +import Foundation +import Path +import ProjectDescription +import TSCUtility +import TuistCore +import TuistSupport +import XcodeGraph + +// MARK: - Swift Package Manager Graph Generator Errors + +enum SwiftPackageManagerGraphGeneratorError: FatalError, Equatable { + /// Thrown when `SwiftPackageManagerWorkspaceState.Dependency.Kind` is not one of the expected values. + case unsupportedDependencyKind(String) + /// Thrown when `SwiftPackageManagerWorkspaceState.packageRef.path` is not present in a local swift package. + case missingPathInLocalSwiftPackage(String) + /// Thrown when dependencies were not installed before loading the graph SwiftPackageManagerGraph + case installRequired + + /// Error type. + var type: ErrorType { + switch self { + case .unsupportedDependencyKind, .missingPathInLocalSwiftPackage: + return .bug + case .installRequired: + return .abort + } + } + + /// Error description. + var description: String { + switch self { + case let .unsupportedDependencyKind(name): + return "The dependency kind \(name) is not supported." + case let .missingPathInLocalSwiftPackage(name): + return "The local package \(name) does not contain the path in the generated `workspace-state.json` file." + case .installRequired: + return "We could not find external dependencies. Run `tuist install` before you continue." + } + } +} + +// MARK: - Swift Package Manager Graph Generator + +/// A protocol that defines an interface to load the `DependenciesGraph` for the `SwiftPackageManager` dependencies. +public protocol SwiftPackageManagerGraphLoading { + /// Generates the `DependenciesGraph` for the `SwiftPackageManager` dependencies. + /// - Parameter path: The path to the directory that contains the `checkouts` directory where `SwiftPackageManager` installed + /// dependencies. + func load( + packagePath: AbsolutePath, + packageSettings: TuistCore.PackageSettings + ) async throws -> TuistCore.DependenciesGraph +} + +public final class SwiftPackageManagerGraphLoader: SwiftPackageManagerGraphLoading { + private let swiftPackageManagerController: SwiftPackageManagerControlling + private let packageInfoMapper: PackageInfoMapping + private let manifestLoader: ManifestLoading + private let fileHandler: FileHandling + + public init( + swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController( + system: System.shared, + fileHandler: FileHandler.shared + ), + packageInfoMapper: PackageInfoMapping = PackageInfoMapper(), + manifestLoader: ManifestLoading = ManifestLoader(), + fileHandler: FileHandling = FileHandler.shared + ) { + self.swiftPackageManagerController = swiftPackageManagerController + self.packageInfoMapper = packageInfoMapper + self.manifestLoader = manifestLoader + self.fileHandler = fileHandler + } + + // swiftlint:disable:next function_body_length + public func load( + packagePath: AbsolutePath, + packageSettings: TuistCore.PackageSettings + ) async throws -> TuistCore.DependenciesGraph { + let path = packagePath.parentDirectory.appending( + component: Constants.SwiftPackageManager.packageBuildDirectoryName + ) + let checkoutsFolder = path.appending(component: "checkouts") + let workspacePath = path.appending(component: "workspace-state.json") + + if !fileHandler.exists(workspacePath) { + throw SwiftPackageManagerGraphGeneratorError.installRequired + } + + let workspaceState = try JSONDecoder() + .decode(SwiftPackageManagerWorkspaceState.self, from: try fileHandler.readFile(workspacePath)) + + let packageInfos: [ + // swiftlint:disable:next large_tuple + ( + id: String, + name: String, + folder: AbsolutePath, + targetToArtifactPaths: [String: AbsolutePath], + info: PackageInfo + ) + ] + packageInfos = try await workspaceState.object.dependencies.concurrentMap { dependency in + let name = dependency.packageRef.name + let packageFolder: AbsolutePath + switch dependency.packageRef.kind { + case "remote", "remoteSourceControl": + packageFolder = checkoutsFolder.appending(component: dependency.subpath) + case "local", "fileSystem", "localSourceControl": + // Depending on the swift version, the information is available either in `path` or in `location` + guard let path = dependency.packageRef.path ?? dependency.packageRef.location else { + throw SwiftPackageManagerGraphGeneratorError.missingPathInLocalSwiftPackage(name) + } + packageFolder = try AbsolutePath(validating: path) + case "registry": + let registryFolder = path.appending(try RelativePath(validating: "registry/downloads")) + packageFolder = registryFolder.appending(try RelativePath(validating: dependency.subpath)) + default: + throw SwiftPackageManagerGraphGeneratorError.unsupportedDependencyKind(dependency.packageRef.kind) + } + + let packageInfo = try await self.manifestLoader.loadPackage(at: packageFolder) + let targetToArtifactPaths = try workspaceState.object.artifacts + .filter { $0.packageRef.identity == dependency.packageRef.identity } + .reduce(into: [:]) { result, artifact in + result[artifact.targetName] = try AbsolutePath(validating: artifact.path) + } + + return ( + id: dependency.packageRef.identity.lowercased(), + name: name, + folder: packageFolder, + targetToArtifactPaths: targetToArtifactPaths, + info: packageInfo + ) + } + + let packageToProject = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.name, $0.folder) }) + let packageInfoDictionary = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.name, $0.info) }) + let packageToFolder = Dictionary(uniqueKeysWithValues: packageInfos.map { ($0.name, $0.folder) }) + let packageToTargetsToArtifactPaths = Dictionary(uniqueKeysWithValues: packageInfos.map { + ($0.name, $0.targetToArtifactPaths) + }) + + let externalDependencies = try packageInfoMapper.resolveExternalDependencies( + packageInfos: packageInfoDictionary, + packageToFolder: packageToFolder, + packageToTargetsToArtifactPaths: packageToTargetsToArtifactPaths + ) + + let externalProjects: [Path: ProjectDescription.Project] = try packageInfos.reduce(into: [:]) { result, packageInfo in + let manifest = try packageInfoMapper.map( + packageInfo: packageInfo.info, + path: packageInfo.folder, + packageType: .external(artifactPaths: packageToTargetsToArtifactPaths[packageInfo.name] ?? [:]), + packageSettings: packageSettings, + packageToProject: packageToProject + ) + result[.path(packageInfo.folder.pathString)] = manifest + } + + return DependenciesGraph( + externalDependencies: externalDependencies, + externalProjects: externalProjects + ) + } +} + +extension ProjectDescription.Platform { + /// Maps a XcodeGraph.Platform instance into a ProjectDescription.Platform instance. + /// - Parameters: + /// - graph: Graph representation of platform model. + static func from(graph: XcodeGraph.Platform) -> ProjectDescription.Platform { + switch graph { + case .macOS: + return .macOS + case .iOS: + return .iOS + case .tvOS: + return .tvOS + case .watchOS: + return .watchOS + case .visionOS: + return .visionOS + } + } +} diff --git a/Sources/TuistLoader/Loaders/TemplateGitLoader.swift b/Sources/TuistLoader/Loaders/TemplateGitLoader.swift index 0c6dc3dcd79..375cb7652de 100644 --- a/Sources/TuistLoader/Loaders/TemplateGitLoader.swift +++ b/Sources/TuistLoader/Loaders/TemplateGitLoader.swift @@ -1,13 +1,13 @@ -import TuistGraph +import TuistCore import TuistSupport public protocol TemplateGitLoading { - /// Load `TuistGraph.Template` from the given Git repository + /// Load `TuistCore.Template` from the given Git repository /// to a temporary directory and performs `closure` on that template. /// - Parameters: /// - templateURL: Git repository url /// - closure: Closure to perform work on loaded template - func loadTemplate(from templateURL: String, templateName: String, closure: (TuistGraph.Template) throws -> Void) throws + func loadTemplate(from templateURL: String, templateName: String, closure: @escaping (TuistCore.Template) async throws -> Void) async throws } public final class TemplateGitLoader: TemplateGitLoading { @@ -38,20 +38,23 @@ public final class TemplateGitLoader: TemplateGitLoading { self.templateLocationParser = templateLocationParser } - public func loadTemplate(from templateURL: String, templateName: String, closure: (TuistGraph.Template) throws -> Void) throws { - // TODO: [Bug] `templateLocationParser` currently doesn't support git urls with user names in them: - // "ssh://git@some.server.com:7999/repos/my-tuist-templates.git" + public func loadTemplate( + from templateURL: String, + templateName: String, + closure: @escaping (TuistCore.Template) async throws -> Void + ) async throws { let repoURL = templateLocationParser.parseRepositoryURL(from: templateURL) - let repoBranch: String? = templateLocationParser.parseRepositoryBranch(from: templateURL) - try fileHandler.inTemporaryDirectory { temporaryPath in + let repoBranch = templateLocationParser.parseRepositoryBranch(from: templateURL) + + try await fileHandler.inTemporaryDirectory { temporaryPath in let templatePath = temporaryPath.appending(component: "Template") - try fileHandler.createFolder(templatePath) - try gitHandler.clone(url: repoURL, to: templatePath) + try self.fileHandler.createFolder(templatePath) + try self.gitHandler.clone(url: repoURL, to: templatePath) if let repoBranch { - try gitHandler.checkout(id: repoBranch, in: templatePath) + try self.gitHandler.checkout(id: repoBranch, in: templatePath) } - let template = try templateLoader.loadTemplate(at: templatePath.appending(components: ["Templates", templateName])) - try closure(template) + let template = try await self.templateLoader.loadTemplate(at: templatePath.appending(components: ["Templates", templateName]), plugins: .none) + try await closure(template) } } } diff --git a/Sources/TuistLoader/Loaders/TemplateLoader.swift b/Sources/TuistLoader/Loaders/TemplateLoader.swift index a011645b2a6..8b98669c721 100644 --- a/Sources/TuistLoader/Loaders/TemplateLoader.swift +++ b/Sources/TuistLoader/Loaders/TemplateLoader.swift @@ -1,16 +1,17 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public protocol TemplateLoading { - /// Load `TuistScaffold.Template` at given `path` + /// Load `TuistCore.Template` at given `path` /// - Parameters: /// - path: Path of template manifest file `name_of_template.swift` - /// - Returns: Loaded `TuistScaffold.Template` - func loadTemplate(at path: AbsolutePath) throws -> TuistGraph.Template + /// - plugins: List of available plugins. + /// - Returns: Loaded `TuistCore.Template` + func loadTemplate(at path: AbsolutePath, plugins: Plugins) async throws -> TuistCore.Template } public class TemplateLoader: TemplateLoading { @@ -25,27 +26,28 @@ public class TemplateLoader: TemplateLoading { self.manifestLoader = manifestLoader } - public func loadTemplate(at path: AbsolutePath) throws -> TuistGraph.Template { - let template = try manifestLoader.loadTemplate(at: path) + public func loadTemplate(at path: AbsolutePath, plugins: Plugins) async throws -> TuistCore.Template { + try manifestLoader.register(plugins: plugins) + let template = try await manifestLoader.loadTemplate(at: path) let generatorPaths = GeneratorPaths(manifestDirectory: path) - return try TuistGraph.Template.from( + return try TuistCore.Template.from( manifest: template, generatorPaths: generatorPaths ) } } -extension TuistGraph.Template { - static func from(manifest: ProjectDescription.Template, generatorPaths: GeneratorPaths) throws -> TuistGraph.Template { - let attributes = try manifest.attributes.map(TuistGraph.Template.Attribute.from) +extension TuistCore.Template { + static func from(manifest: ProjectDescription.Template, generatorPaths: GeneratorPaths) throws -> TuistCore.Template { + let attributes = try manifest.attributes.map(TuistCore.Template.Attribute.from) let items = try manifest.items.map { Item( path: try RelativePath(validating: $0.path), - contents: try TuistGraph.Template.Contents.from( + contents: try TuistCore.Template.Contents.from( manifest: $0.contents, generatorPaths: generatorPaths ) ) } - return TuistGraph.Template( + return TuistCore.Template( description: manifest.description, attributes: attributes, items: items @@ -53,22 +55,46 @@ extension TuistGraph.Template { } } -extension TuistGraph.Template.Attribute { - static func from(manifest: ProjectDescription.Template.Attribute) throws -> TuistGraph.Template.Attribute { +extension TuistCore.Template.Attribute { + static func from(manifest: ProjectDescription.Template.Attribute) throws -> TuistCore.Template.Attribute { switch manifest { case let .required(name): return .required(name) case let .optional(name, default: defaultValue): - return .optional(name, default: defaultValue) + return .optional(name, default: try Self.Value.from(value: defaultValue)) } } } -extension TuistGraph.Template.Contents { +extension TuistCore.Template.Attribute.Value { + static func from(value: ProjectDescription.Template.Attribute.Value) throws -> TuistCore.Template.Attribute.Value { + switch value { + case let .string(string): + return .string(string) + case let .integer(integer): + return .integer(integer) + case let .real(real): + return .real(real) + case let .boolean(boolean): + return .boolean(boolean) + case let .dictionary(dictionary): + var newDictionary: [String: TuistCore.Template.Attribute.Value] = [:] + for (key, value) in dictionary { + newDictionary[key] = try from(value: value) + } + return .dictionary(newDictionary) + case let .array(array): + let newArray: [TuistCore.Template.Attribute.Value] = try array.map { try from(value: $0) } + return .array(newArray) + } + } +} + +extension TuistCore.Template.Contents { static func from( manifest: ProjectDescription.Template.Contents, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.Template.Contents { + ) throws -> TuistCore.Template.Contents { switch manifest { case let .string(contents): return .string(contents) diff --git a/Sources/TuistLoader/Loaders/UmbrellaHeaderHeadersExtractor.swift b/Sources/TuistLoader/Loaders/UmbrellaHeaderHeadersExtractor.swift index 69510dd46c6..f8e1067a53c 100644 --- a/Sources/TuistLoader/Loaders/UmbrellaHeaderHeadersExtractor.swift +++ b/Sources/TuistLoader/Loaders/UmbrellaHeaderHeadersExtractor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport public enum UmbrellaHeaderHeadersExtractor { @@ -16,7 +16,7 @@ public enum UmbrellaHeaderHeadersExtractor { return lines.compactMap { line in let stripped = line.trimmingCharacters(in: .whitespaces) - guard let matchingPrefix = expectedPrefixes.first(where: { line.hasPrefix($0) }) else { + guard let matchingPrefix = expectedPrefixes.first(where: { stripped.hasPrefix($0) }) else { return nil } // also we need drop comments and spaces before comments diff --git a/Sources/TuistLoader/Models+ManifestMappers/AnalyzeAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/AnalyzeAction+ManifestMapper.swift index d0fb1d116c3..e19cbeff570 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/AnalyzeAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/AnalyzeAction+ManifestMapper.swift @@ -1,14 +1,14 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.AnalyzeAction { +extension XcodeGraph.AnalyzeAction { static func from( manifest: ProjectDescription.AnalyzeAction, generatorPaths _: GeneratorPaths - ) throws -> TuistGraph.AnalyzeAction { + ) throws -> XcodeGraph.AnalyzeAction { let configurationName = manifest.configuration.rawValue return AnalyzeAction(configurationName: configurationName) diff --git a/Sources/TuistLoader/Models+ManifestMappers/ArchiveAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ArchiveAction+ManifestMapper.swift index 5f81212d749..4ac3cb2722e 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ArchiveAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ArchiveAction+ManifestMapper.swift @@ -1,26 +1,26 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.ArchiveAction { - /// Maps a ProjectDescription.ArchiveAction instance into a TuistGraph.ArchiveAction instance. +extension XcodeGraph.ArchiveAction { + /// Maps a ProjectDescription.ArchiveAction instance into a XcodeGraph.ArchiveAction instance. /// - Parameters: /// - manifest: Manifest representation of archive action model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.ArchiveAction, generatorPaths: GeneratorPaths) throws -> TuistGraph + static func from(manifest: ProjectDescription.ArchiveAction, generatorPaths: GeneratorPaths) throws -> XcodeGraph .ArchiveAction { let configurationName = manifest.configuration.rawValue let revealArchiveInOrganizer = manifest.revealArchiveInOrganizer let customArchiveName = manifest.customArchiveName let preActions = try manifest.preActions - .map { try TuistGraph.ExecutionAction.from(manifest: $0, generatorPaths: generatorPaths) } + .map { try XcodeGraph.ExecutionAction.from(manifest: $0, generatorPaths: generatorPaths) } let postActions = try manifest.postActions - .map { try TuistGraph.ExecutionAction.from(manifest: $0, generatorPaths: generatorPaths) } + .map { try XcodeGraph.ExecutionAction.from(manifest: $0, generatorPaths: generatorPaths) } - return TuistGraph.ArchiveAction( + return XcodeGraph.ArchiveAction( configurationName: configurationName, revealArchiveInOrganizer: revealArchiveInOrganizer, customArchiveName: customArchiveName, diff --git a/Sources/TuistLoader/Models+ManifestMappers/Arguments+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Arguments+ManifestMapper.swift index 830028a968a..7ca8a8f7268 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Arguments+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Arguments+ManifestMapper.swift @@ -1,14 +1,14 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.Arguments { - /// Maps a ProjectDescription.Arguments instance into a TuistGraph.Arguments instance. +extension XcodeGraph.Arguments { + /// Maps a ProjectDescription.Arguments instance into a XcodeGraph.Arguments instance. /// - Parameters: /// - manifest: Manifest representation of arguments model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Arguments) -> TuistGraph.Arguments { + static func from(manifest: ProjectDescription.Arguments) -> XcodeGraph.Arguments { Arguments( environmentVariables: manifest.environmentVariables.mapValues(EnvironmentVariable.from), launchArguments: manifest.launchArguments.map(LaunchArgument.from) diff --git a/Sources/TuistLoader/Models+ManifestMappers/BuildAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/BuildAction+ManifestMapper.swift index 6950cb86ae3..0d64ae043ba 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/BuildAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/BuildAction+ManifestMapper.swift @@ -1,33 +1,33 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.BuildAction { - /// Maps a ProjectDescription.BuildAction instance into a TuistGraph.BuildAction instance. +extension XcodeGraph.BuildAction { + /// Maps a ProjectDescription.BuildAction instance into a XcodeGraph.BuildAction instance. /// - Parameters: /// - manifest: Manifest representation of build action model. /// - generatorPaths: Generator paths. static func from( manifest: ProjectDescription.BuildAction, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.BuildAction { - let preActions = try manifest.preActions.map { try TuistGraph.ExecutionAction.from( + ) throws -> XcodeGraph.BuildAction { + let preActions = try manifest.preActions.map { try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let postActions = try manifest.postActions.map { try TuistGraph.ExecutionAction.from( + let postActions = try manifest.postActions.map { try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let targets: [TuistGraph.TargetReference] = try manifest.targets.map { + let targets: [XcodeGraph.TargetReference] = try manifest.targets.map { .init( projectPath: try generatorPaths.resolveSchemeActionProjectPath($0.projectPath), name: $0.targetName ) } - return TuistGraph.BuildAction( + return XcodeGraph.BuildAction( targets: targets, preActions: preActions, postActions: postActions, diff --git a/Sources/TuistLoader/Models+ManifestMappers/BuildConfiguration+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/BuildConfiguration+ManifestMapper.swift index 54d7e0ff852..c24fc864a0e 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/BuildConfiguration+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/BuildConfiguration+ManifestMapper.swift @@ -1,21 +1,21 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.BuildConfiguration { - /// Maps a ProjectDescription.Configuration instance into a TuistGraph.BuildConfiguration instance. +extension XcodeGraph.BuildConfiguration { + /// Maps a ProjectDescription.Configuration instance into a XcodeGraph.BuildConfiguration instance. /// - Parameters: /// - manifest: Manifest representation of build configuration model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Configuration) -> TuistGraph.BuildConfiguration { - let variant: TuistGraph.BuildConfiguration.Variant + static func from(manifest: ProjectDescription.Configuration) -> XcodeGraph.BuildConfiguration { + let variant: XcodeGraph.BuildConfiguration.Variant switch manifest.variant { case .debug: variant = .debug case .release: variant = .release } - return TuistGraph.BuildConfiguration(name: manifest.name.rawValue, variant: variant) + return XcodeGraph.BuildConfiguration(name: manifest.name.rawValue, variant: variant) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/BuildRule+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/BuildRule+ManifestMapper.swift index bd825f2df96..dcb870530ac 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/BuildRule+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/BuildRule+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.BuildRule { +extension XcodeGraph.BuildRule { static func from(manifest: ProjectDescription.BuildRule) -> Self { .init( - compilerSpec: TuistGraph.BuildRule.CompilerSpec.from(manifest: manifest.compilerSpec), - fileType: TuistGraph.BuildRule.FileType.from(manifest: manifest.fileType), + compilerSpec: XcodeGraph.BuildRule.CompilerSpec.from(manifest: manifest.compilerSpec), + fileType: XcodeGraph.BuildRule.FileType.from(manifest: manifest.fileType), filePatterns: manifest.filePatterns, name: manifest.name, outputFiles: manifest.outputFiles, diff --git a/Sources/TuistLoader/Models+ManifestMappers/BuildRule.CompilerSpec+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/BuildRule.CompilerSpec+ManifestMapper.swift index 32fa638ab2d..fb8440044fa 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/BuildRule.CompilerSpec+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/BuildRule.CompilerSpec+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.BuildRule.CompilerSpec { +extension XcodeGraph.BuildRule.CompilerSpec { // swiftlint:disable function_body_length static func from( manifest: ProjectDescription.BuildRule.CompilerSpec - ) -> TuistGraph.BuildRule.CompilerSpec { + ) -> XcodeGraph.BuildRule.CompilerSpec { switch manifest { case .appIntentsMetadataExtractor: return .appIntentsMetadataExtractor diff --git a/Sources/TuistLoader/Models+ManifestMappers/BuildRule.FileType+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/BuildRule.FileType+ManifestMapper.swift index c09529ff45c..bff1c04a147 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/BuildRule.FileType+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/BuildRule.FileType+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.BuildRule.FileType { +extension XcodeGraph.BuildRule.FileType { // swiftlint:disable function_body_length static func from( manifest: ProjectDescription.BuildRule.FileType - ) -> TuistGraph.BuildRule.FileType { + ) -> XcodeGraph.BuildRule.FileType { switch manifest { case .instrumentsPackageDefinition: return .instrumentsPackageDefinition diff --git a/Sources/TuistLoader/Models+ManifestMappers/Cache+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Cache+ManifestMapper.swift index cc9678170ee..c32dfbb8acf 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Cache+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Cache+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import struct TSCUtility.Version -import TuistGraph import TuistSupport +import XcodeGraph enum CacheProfileError: FatalError, Equatable { case invalidVersion(string: String) @@ -22,34 +22,3 @@ enum CacheProfileError: FatalError, Equatable { } } } - -extension TuistGraph.Cache { - static func from( - manifest: ProjectDescription.Cache, - generatorPaths: GeneratorPaths - ) throws -> TuistGraph.Cache { - let path = try manifest.path.map { try generatorPaths.resolve(path: $0) } - let profiles = try manifest.profiles.map(TuistGraph.Cache.Profile.from(manifest:)) - return TuistGraph.Cache(profiles: profiles, path: path) - } -} - -extension TuistGraph.Cache.Profile { - static func from(manifest: ProjectDescription.Cache.Profile) throws -> TuistGraph.Cache.Profile { - var resolvedVersion: TSCUtility.Version? - - if let versionString = manifest.os { - guard let version = versionString.version() else { - throw CacheProfileError.invalidVersion(string: versionString) - } - resolvedVersion = version - } - - return TuistGraph.Cache.Profile( - name: manifest.name, - configuration: manifest.configuration, - device: manifest.device, - os: resolvedVersion - ) - } -} diff --git a/Sources/TuistLoader/Models+ManifestMappers/CarthageDependencies+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/CarthageDependencies+ManifestMapper.swift deleted file mode 100644 index 33cf766f435..00000000000 --- a/Sources/TuistLoader/Models+ManifestMappers/CarthageDependencies+ManifestMapper.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -extension TuistGraph.CarthageDependencies { - /// Creates `TuistGraph.CarthageDependencies` instance from `ProjectDescription.CarthageDependencies` instance. - static func from(manifest: ProjectDescription.CarthageDependencies) throws -> Self { - let dependencies = manifest.dependencies.map { TuistGraph.CarthageDependencies.Dependency.from(manifest: $0) } - - return .init(dependencies) - } -} - -extension TuistGraph.CarthageDependencies.Dependency { - /// Creates `TuistGraph.CarthageDependencies.Dependency` instance from `ProjectDescription.CarthageDependencies.Dependency` - /// instance. - static func from(manifest: ProjectDescription.CarthageDependencies.Dependency) -> Self { - switch manifest { - case let .github(path, requirement): - return .github(path: path, requirement: .from(manifest: requirement)) - case let .git(path, requirement): - return .git(path: path, requirement: .from(manifest: requirement)) - case let .binary(path, requirement): - return .binary(path: path, requirement: .from(manifest: requirement)) - } - } -} - -extension TuistGraph.CarthageDependencies.Requirement { - /// Creates `TuistGraph.CarthageDependencies.Requirement` instance from `ProjectDescription.CarthageDependencies.Requirement` - /// instance. - static func from(manifest: ProjectDescription.CarthageDependencies.Requirement) -> Self { - switch manifest { - case let .exact(version): - return .exact(version.description) - case let .upToNext(version): - return .upToNext(version.description) - case let .atLeast(version): - return .atLeast(version.description) - case let .branch(branch): - return .branch(branch) - case let .revision(revision): - return .revision(revision) - } - } -} diff --git a/Sources/TuistLoader/Models+ManifestMappers/Cloud+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Cloud+ManifestMapper.swift deleted file mode 100644 index 0d184b1f3fc..00000000000 --- a/Sources/TuistLoader/Models+ManifestMappers/Cloud+ManifestMapper.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -enum CloudManifestMapperError: FatalError { - /// Thrown when the cloud URL is invalid. - case invalidCloudURL(String) - - /// Error type. - var type: ErrorType { - switch self { - case .invalidCloudURL: return .abort - } - } - - /// Error description. - var description: String { - switch self { - case let .invalidCloudURL(url): - return "The cloud URL '\(url)' is not a valid URL" - } - } -} - -extension TuistGraph.Cloud { - static func from(manifest: ProjectDescription.Cloud) throws -> TuistGraph.Cloud { - var cloudURL: URL! - if let manifestCloudURL = URL(string: manifest.url) { - cloudURL = manifestCloudURL - } else { - throw CloudManifestMapperError.invalidCloudURL(manifest.url) - } - let options = manifest.options.compactMap(TuistGraph.Cloud.Option.from) - return TuistGraph.Cloud(url: cloudURL, projectId: manifest.projectId, options: options) - } -} - -extension TuistGraph.Cloud.Option { - static func from(manifest: ProjectDescription.Cloud.Option) -> TuistGraph.Cloud.Option? { - switch manifest { - case .analytics: - return nil - case .disableAnalytics: - return .disableAnalytics - case .optional: - return .optional - } - } -} diff --git a/Sources/TuistLoader/Models+ManifestMappers/CompatibleXcodeVersions+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/CompatibleXcodeVersions+ManifestMapper.swift index 6155b49b137..c9d65f65f2f 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/CompatibleXcodeVersions+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/CompatibleXcodeVersions+ManifestMapper.swift @@ -1,25 +1,24 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport -extension TuistGraph.CompatibleXcodeVersions { - /// Maps a ProjectDescription.CompatibleXcodeVersions instance into a TuistGraph.CompatibleXcodeVersions model. +extension TuistCore.CompatibleXcodeVersions { + /// Maps a ProjectDescription.CompatibleXcodeVersions instance into a XcodeGraph.CompatibleXcodeVersions model. /// - Parameters: /// - manifest: Manifest representation of compatible Xcode versions. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.CompatibleXcodeVersions) -> TuistGraph.CompatibleXcodeVersions { + static func from(manifest: ProjectDescription.CompatibleXcodeVersions) -> TuistCore.CompatibleXcodeVersions { switch manifest { case .all: return .all case let .exact(version): - return .exact(version) + return .exact(.init(stringLiteral: version.description)) case let .upToNextMajor(version): - return .upToNextMajor(version) + return .upToNextMajor(.init(stringLiteral: version.description)) case let .upToNextMinor(version): - return .upToNextMinor(version) + return .upToNextMinor(.init(stringLiteral: version.description)) case let .list(versions): return .list(versions.map { from(manifest: $0) }) } diff --git a/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift index efb8f0a0ec8..2b1edf83669 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift @@ -1,23 +1,43 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.Config { - /// Maps a ProjectDescription.Config instance into a TuistGraph.Config model. +enum ConfigManifestMapperError: FatalError { + /// Thrown when the server URL is invalid. + case invalidServerURL(String) + + /// Error type. + var type: ErrorType { + switch self { + case .invalidServerURL: return .abort + } + } + + /// Error description. + var description: String { + switch self { + case let .invalidServerURL(url): + return "The server URL '\(url)' is not a valid URL" + } + } +} + +extension TuistCore.Config { + /// Maps a ProjectDescription.Config instance into a XcodeGraph.Config model. /// - Parameters: /// - manifest: Manifest representation of Tuist config. /// - path: The path of the config file. - static func from(manifest: ProjectDescription.Config, at path: AbsolutePath) throws -> TuistGraph.Config { + static func from(manifest: ProjectDescription.Config, at path: AbsolutePath) throws -> TuistCore.Config { let generatorPaths = GeneratorPaths(manifestDirectory: path) - let generationOptions = try TuistGraph.Config.GenerationOptions.from( + var generationOptions = try TuistCore.Config.GenerationOptions.from( manifest: manifest.generationOptions, generatorPaths: generatorPaths ) - let compatibleXcodeVersions = TuistGraph.CompatibleXcodeVersions.from(manifest: manifest.compatibleXcodeVersions) + let compatibleXcodeVersions = TuistCore.CompatibleXcodeVersions.from(manifest: manifest.compatibleXcodeVersions) let plugins = try manifest.plugins.map { try PluginLocation.from(manifest: $0, generatorPaths: generatorPaths) } let swiftVersion: TSCUtility.Version? if let configuredVersion = manifest.swiftVersion { @@ -26,21 +46,26 @@ extension TuistGraph.Config { swiftVersion = nil } - var cloud: TuistGraph.Cloud? + let fullHandle: String? + let urlString: String if let manifestCloud = manifest.cloud { - cloud = try TuistGraph.Cloud.from(manifest: manifestCloud) + fullHandle = manifestCloud.projectId + urlString = manifestCloud.url + generationOptions.optionalAuthentication = manifestCloud.options.contains(.optional) + } else { + fullHandle = manifest.fullHandle + urlString = manifest.url } - var cache: TuistGraph.Cache? - if let manifestCache = manifest.cache { - cache = try TuistGraph.Cache.from(manifest: manifestCache, generatorPaths: generatorPaths) + guard let url = URL(string: urlString.dropSuffix("/")) else { + throw ConfigManifestMapperError.invalidServerURL(manifest.url) } - return TuistGraph.Config( + return TuistCore.Config( compatibleXcodeVersions: compatibleXcodeVersions, - cloud: cloud, - cache: cache, - swiftVersion: swiftVersion, + fullHandle: fullHandle, + url: url, + swiftVersion: swiftVersion.map { .init(stringLiteral: $0.description) }, plugins: plugins, generationOptions: generationOptions, path: path @@ -48,15 +73,15 @@ extension TuistGraph.Config { } } -extension TuistGraph.Config.GenerationOptions { - /// Maps a ProjectDescription.Config.GenerationOptions instance into a TuistGraph.Config.GenerationOptions model. +extension TuistCore.Config.GenerationOptions { + /// Maps a ProjectDescription.Config.GenerationOptions instance into a XcodeGraph.Config.GenerationOptions model. /// - Parameters: /// - manifest: Manifest representation of Tuist config generation options /// - generatorPaths: Generator paths static func from( manifest: ProjectDescription.Config.GenerationOptions, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.Config.GenerationOptions { + ) throws -> TuistCore.Config.GenerationOptions { let clonedSourcePackagesDirPath: AbsolutePath? = try { if let path = manifest.clonedSourcePackagesDirPath { return try generatorPaths.resolve(path: path) @@ -68,21 +93,23 @@ extension TuistGraph.Config.GenerationOptions { resolveDependenciesWithSystemScm: manifest.resolveDependenciesWithSystemScm, disablePackageVersionLocking: manifest.disablePackageVersionLocking, clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, - staticSideEffectsWarningTargets: TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets + staticSideEffectsWarningTargets: TuistCore.Config.GenerationOptions.StaticSideEffectsWarningTargets .from(manifest: manifest.staticSideEffectsWarningTargets), - enforceExplicitDependencies: manifest.enforceExplicitDependencies + enforceExplicitDependencies: manifest.enforceExplicitDependencies, + defaultConfiguration: manifest.defaultConfiguration, + optionalAuthentication: manifest.optionalAuthentication ) } } -extension TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets { +extension TuistCore.Config.GenerationOptions.StaticSideEffectsWarningTargets { /// Maps a ProjectDescription.Config.GenerationOptions.StaticSideEffectsWarningTargets instance into a - /// TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets model. + /// XcodeGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets model. /// - Parameters: /// - manifest: Manifest representation of Tuist config static side effects warning targets option static func from( manifest: ProjectDescription.Config.GenerationOptions.StaticSideEffectsWarningTargets - ) -> TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets { + ) -> TuistCore.Config.GenerationOptions.StaticSideEffectsWarningTargets { switch manifest { case .all: return .all case .none: return .none diff --git a/Sources/TuistLoader/Models+ManifestMappers/Configuration+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Configuration+ManifestMapper.swift index d05be996ff5..be7bfb7c260 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Configuration+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Configuration+ManifestMapper.swift @@ -1,21 +1,21 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.Configuration { - /// Maps a ProjectDescription.Configuration instance into a TuistGraph.Configuration instance. +extension XcodeGraph.Configuration { + /// Maps a ProjectDescription.Configuration instance into a XcodeGraph.Configuration instance. /// - Parameters: /// - manifest: Manifest representation of configuration. /// - generatorPaths: Generator paths. static func from( manifest: ProjectDescription.Configuration?, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.Configuration? { + ) throws -> XcodeGraph.Configuration? { guard let manifest else { return nil } - let settings = manifest.settings.mapValues(TuistGraph.SettingValue.from) + let settings = manifest.settings.mapValues(XcodeGraph.SettingValue.from) let xcconfig = try manifest.xcconfig.flatMap { try generatorPaths.resolve(path: $0) } return Configuration(settings: settings, xcconfig: xcconfig) } diff --git a/Sources/TuistLoader/Models+ManifestMappers/CopyFileElement+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/CopyFileElement+ManifestMapper.swift new file mode 100644 index 00000000000..444516a5d8c --- /dev/null +++ b/Sources/TuistLoader/Models+ManifestMappers/CopyFileElement+ManifestMapper.swift @@ -0,0 +1,71 @@ +import Foundation +import Path +import ProjectDescription +import TuistCore +import TuistSupport +import XcodeGraph + +extension XcodeGraph.CopyFileElement { + /// Maps a ProjectDescription.FileElement instance into a [XcodeGraph.FileElement] instance. + /// Glob patterns in file elements are unfolded as part of the mapping. + /// - Parameters: + /// - manifest: Manifest representation of the file element. + /// - generatorPaths: Generator paths. + static func from( + manifest: ProjectDescription.CopyFileElement, + generatorPaths: GeneratorPaths, + includeFiles: @escaping (AbsolutePath) -> Bool = { _ in true } + ) throws -> [XcodeGraph.CopyFileElement] { + func globFiles(_ path: AbsolutePath) throws -> [AbsolutePath] { + if FileHandler.shared.exists(path), !FileHandler.shared.isFolder(path) { return [path] } + + let files = try FileHandler.shared.throwingGlob(AbsolutePath.root, glob: String(path.pathString.dropFirst())) + .filter(includeFiles) + + if files.isEmpty { + if FileHandler.shared.isFolder(path) { + logger.warning("'\(path.pathString)' is a directory, try using: '\(path.pathString)/**' to list its files") + } else { + // FIXME: This should be done in a linter. + logger.warning("No files found at: \(path.pathString)") + } + } + + return files + } + + func folderReferences(_ path: AbsolutePath) -> [AbsolutePath] { + guard FileHandler.shared.exists(path) else { + // FIXME: This should be done in a linter. + logger.warning("\(path.pathString) does not exist") + return [] + } + + guard FileHandler.shared.isFolder(path) else { + // FIXME: This should be done in a linter. + logger.warning("\(path.pathString) is not a directory - folder reference paths need to point to directories") + return [] + } + + return [path] + } + + switch manifest { + case let .glob(pattern: pattern, condition: condition, codeSignOnCopy: codeSign): + let resolvedPath = try generatorPaths.resolve(path: pattern) + return try globFiles(resolvedPath).map { .file( + path: $0, + condition: condition?.asGraphCondition, + codeSignOnCopy: codeSign + ) + } + case let .folderReference(path: folderReferencePath, condition: condition, codeSignOnCopy: codeSign): + let resolvedPath = try generatorPaths.resolve(path: folderReferencePath) + return folderReferences(resolvedPath).map { .folderReference( + path: $0, + condition: condition?.asGraphCondition, + codeSignOnCopy: codeSign + ) } + } + } +} diff --git a/Sources/TuistLoader/Models+ManifestMappers/CopyFilesAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/CopyFilesAction+ManifestMapper.swift index 34281120823..687a52979ab 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/CopyFilesAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/CopyFilesAction+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public enum CopyFilesManifestMapperError: FatalError { case invalidResourcesGlob(actionName: String, invalidGlobs: [InvalidGlob]) @@ -19,21 +19,21 @@ public enum CopyFilesManifestMapperError: FatalError { } } -extension TuistGraph.CopyFilesAction { - /// Maps a ProjectDescription.CopyFilesAction instance into a TuistGraph.CopyFilesAction instance. +extension XcodeGraph.CopyFilesAction { + /// Maps a ProjectDescription.CopyFilesAction instance into a XcodeGraph.CopyFilesAction instance. /// - Parameters: /// - manifest: Manifest representation of platform model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.CopyFilesAction, generatorPaths: GeneratorPaths) throws -> TuistGraph + static func from(manifest: ProjectDescription.CopyFilesAction, generatorPaths: GeneratorPaths) throws -> XcodeGraph .CopyFilesAction { var invalidResourceGlobs: [InvalidGlob] = [] - let files: [TuistGraph.FileElement] = try manifest.files.flatMap { manifest -> [TuistGraph.FileElement] in + let files: [XcodeGraph.CopyFileElement] = try manifest.files.flatMap { manifest -> [XcodeGraph.CopyFileElement] in do { - let files = try TuistGraph.FileElement.from( + let files = try XcodeGraph.CopyFileElement.from( manifest: manifest, generatorPaths: generatorPaths, - includeFiles: { TuistGraph.Target.isResource(path: $0) } + includeFiles: { XcodeGraph.Target.isResource(path: $0) } ) return files.cleanPackages() } catch let GlobError.nonExistentDirectory(invalidGlob) { @@ -46,20 +46,20 @@ extension TuistGraph.CopyFilesAction { throw CopyFilesManifestMapperError.invalidResourcesGlob(actionName: manifest.name, invalidGlobs: invalidResourceGlobs) } - return TuistGraph.CopyFilesAction( + return XcodeGraph.CopyFilesAction( name: manifest.name, - destination: TuistGraph.CopyFilesAction.Destination.from(manifest: manifest.destination), + destination: XcodeGraph.CopyFilesAction.Destination.from(manifest: manifest.destination), subpath: manifest.subpath, files: files ) } } -extension TuistGraph.CopyFilesAction.Destination { - /// Maps a ProjectDescription.TargetAction.Destination instance into a TuistGraph.TargetAction.Destination model. +extension XcodeGraph.CopyFilesAction.Destination { + /// Maps a ProjectDescription.TargetAction.Destination instance into a XcodeGraph.TargetAction.Destination model. /// - Parameters: /// - manifest: Manifest representation of target action destination. - static func from(manifest: ProjectDescription.CopyFilesAction.Destination) -> TuistGraph.CopyFilesAction.Destination { + static func from(manifest: ProjectDescription.CopyFilesAction.Destination) -> XcodeGraph.CopyFilesAction.Destination { switch manifest { case .absolutePath: return .absolutePath @@ -89,7 +89,7 @@ extension TuistGraph.CopyFilesAction.Destination { // MARK: - Array Extension FileElement -extension [TuistGraph.FileElement] { +extension [XcodeGraph.CopyFileElement] { /// Packages should be added as a whole folder not individually. /// (e.g. bundled file formats recognized by the OS like .pages, .numbers, .rtfd...) /// diff --git a/Sources/TuistLoader/Models+ManifestMappers/CoreDataModel+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/CoreDataModel+ManifestMapper.swift index f953e4dccff..23b5ea4ad8b 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/CoreDataModel+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/CoreDataModel+ManifestMapper.swift @@ -1,16 +1,16 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.CoreDataModel { - /// Maps a ProjectDescription.CoreDataModel instance into a TuistGraph.CoreDataModel instance. +extension XcodeGraph.CoreDataModel { + /// Maps a ProjectDescription.CoreDataModel instance into a XcodeGraph.CoreDataModel instance. /// - Parameters: /// - manifest: Manifest representation of Core Data model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.CoreDataModel, generatorPaths: GeneratorPaths) throws -> TuistGraph + static func from(manifest: ProjectDescription.CoreDataModel, generatorPaths: GeneratorPaths) throws -> XcodeGraph .CoreDataModel { let modelPath = try generatorPaths.resolve(path: manifest.path) @@ -30,11 +30,11 @@ extension TuistGraph.CoreDataModel { } } -extension TuistGraph.CoreDataModel { - /// Maps a `.xcdatamodeld` package into a TuistGraph.CoreDataModel instance. +extension XcodeGraph.CoreDataModel { + /// Maps a `.xcdatamodeld` package into a XcodeGraph.CoreDataModel instance. /// - Parameters: /// - path: The path for a `.xcdatamodeld` package. - static func from(path modelPath: AbsolutePath) throws -> TuistGraph.CoreDataModel { + static func from(path modelPath: AbsolutePath) throws -> XcodeGraph.CoreDataModel { let versions = FileHandler.shared.glob(modelPath, glob: "*.xcdatamodel") let currentVersion: String = try { if CoreDataVersionExtractor.isVersioned(at: modelPath) { diff --git a/Sources/TuistLoader/Models+ManifestMappers/DefaultSettings+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/DefaultSettings+ManifestMapper.swift index 07f1380602a..b1d0250d1da 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/DefaultSettings+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/DefaultSettings+ManifestMapper.swift @@ -1,16 +1,16 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.DefaultSettings { - /// Maps a ProjectDescription.DefaultSettings instance into a TuistGraph.DefaultSettings model. +extension XcodeGraph.DefaultSettings { + /// Maps a ProjectDescription.DefaultSettings instance into a XcodeGraph.DefaultSettings model. /// - Parameters: /// - manifest: Manifest representation of default settings. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.DefaultSettings) -> TuistGraph.DefaultSettings { + static func from(manifest: ProjectDescription.DefaultSettings) -> XcodeGraph.DefaultSettings { switch manifest { case let .recommended(excludedKeys): return .recommended(excluding: excludedKeys) diff --git a/Sources/TuistLoader/Models+ManifestMappers/Dependencies+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Dependencies+ManifestMapper.swift deleted file mode 100644 index bb70eda593a..00000000000 --- a/Sources/TuistLoader/Models+ManifestMappers/Dependencies+ManifestMapper.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -extension TuistGraph.Dependencies { - /// Maps a ProjectDescription.Dependencies instance into a TuistGraph.Dependencies instance. - /// - Parameters: - /// - manifest: Manifest representation of dependencies. - /// - generatorPaths: Generator paths. - static func from( - manifest: ProjectDescription.Dependencies, - generatorPaths: GeneratorPaths - ) throws -> Self { - let carthage: TuistGraph.CarthageDependencies? = try { - guard let carthage = manifest.carthage else { - return nil - } - return try TuistGraph.CarthageDependencies.from(manifest: carthage) - }() - let swiftPackageManager: TuistGraph.SwiftPackageManagerDependencies? = try { - guard let swiftPackageManager = manifest.swiftPackageManager else { - return nil - } - return try TuistGraph.SwiftPackageManagerDependencies.from( - manifest: swiftPackageManager, - generatorPaths: generatorPaths - ) - }() - let platforms = try manifest.platforms.map { try TuistGraph.PackagePlatform.from(manifest: $0) } - - return Self( - carthage: carthage, - swiftPackageManager: swiftPackageManager, - platforms: Set(platforms) - ) - } -} diff --git a/Sources/TuistLoader/Models+ManifestMappers/DeploymentTargets+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/DeploymentTargets+ManifestMapper.swift index e1c36ee62de..0318590c1eb 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/DeploymentTargets+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/DeploymentTargets+ManifestMapper.swift @@ -1,14 +1,14 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.DeploymentTargets { - /// Maps a ProjectDescription.DeploymentTargets instance into a TuistGraph.DeploymentTarget instance. +extension XcodeGraph.DeploymentTargets { + /// Maps a ProjectDescription.DeploymentTargets instance into a XcodeGraph.DeploymentTarget instance. /// - Parameters: /// - manifest: Manifest representation of deployment target model. - static func from(manifest: ProjectDescription.DeploymentTargets) -> TuistGraph.DeploymentTargets { - TuistGraph.DeploymentTargets( + static func from(manifest: ProjectDescription.DeploymentTargets) -> XcodeGraph.DeploymentTargets { + XcodeGraph.DeploymentTargets( iOS: manifest.iOS, macOS: manifest.macOS, watchOS: manifest.watchOS, diff --git a/Sources/TuistLoader/Models+ManifestMappers/Destination+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Destination+ManifestMapper.swift index bb15ec48933..389e1bd0a6b 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Destination+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Destination+ManifestMapper.swift @@ -1,16 +1,16 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.Destination { - /// Maps a ProjectDescription.Package instance into a TuistGraph.Package model. +extension XcodeGraph.Destination { + /// Maps a ProjectDescription.Package instance into a XcodeGraph.Package model. /// - Parameters: /// - manifest: Manifest representation of Package. /// - generatorPaths: Generator paths. static func from( destinations: ProjectDescription.Destinations - ) throws -> TuistGraph.Destinations { - let mappedDestinations: [TuistGraph.Destination] = destinations.map { destination in + ) throws -> XcodeGraph.Destinations { + let mappedDestinations: [XcodeGraph.Destination] = destinations.map { destination in switch destination { case .iPhone: return .iPhone diff --git a/Sources/TuistLoader/Models+ManifestMappers/Entitlements+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Entitlements+ManifestMapper.swift index c91e62b504d..8dca99e1caa 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Entitlements+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Entitlements+ManifestMapper.swift @@ -1,15 +1,15 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.Entitlements { - /// Maps a ProjectDescription.Entitlements instance into a TuistGraph.Entitlements instance. +extension XcodeGraph.Entitlements { + /// Maps a ProjectDescription.Entitlements instance into a XcodeGraph.Entitlements instance. /// - Parameters: /// - manifest: Manifest representation of the Entitlements model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Entitlements?, generatorPaths: GeneratorPaths) throws -> TuistGraph + static func from(manifest: ProjectDescription.Entitlements?, generatorPaths: GeneratorPaths) throws -> XcodeGraph .Entitlements? { switch manifest { @@ -17,8 +17,10 @@ extension TuistGraph.Entitlements { return .file(path: try generatorPaths.resolve(path: infoplistPath)) case let .dictionary(dictionary): return .dictionary( - dictionary.mapValues { TuistGraph.Plist.Value.from(manifest: $0) } + dictionary.mapValues { XcodeGraph.Plist.Value.from(manifest: $0) } ) + case let .variable(setting): + return .variable(setting) case .none: return .none } diff --git a/Sources/TuistLoader/Models+ManifestMappers/EnvironmentVariable+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/EnvironmentVariable+ManifestMapper.swift index c710fe3789f..948d909b4d0 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/EnvironmentVariable+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/EnvironmentVariable+ManifestMapper.swift @@ -1,13 +1,13 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.EnvironmentVariable { - /// Maps a ProjectDescription.EnvironmentVariable instance into a TuistGraph.EnvironmentVariable instance. +extension XcodeGraph.EnvironmentVariable { + /// Maps a ProjectDescription.EnvironmentVariable instance into a XcodeGraph.EnvironmentVariable instance. /// - Parameters: /// - manifest: Manifest representation of environment variable model. - static func from(manifest: ProjectDescription.EnvironmentVariable) -> TuistGraph.EnvironmentVariable { + static func from(manifest: ProjectDescription.EnvironmentVariable) -> XcodeGraph.EnvironmentVariable { EnvironmentVariable(value: manifest.value, isEnabled: manifest.isEnabled) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/ExecutionAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ExecutionAction+ManifestMapper.swift index 4b04bdc7d3d..d40c388973d 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ExecutionAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ExecutionAction+ManifestMapper.swift @@ -1,17 +1,17 @@ import Foundation +import Path import ProjectDescription -import TSCBasic -import TuistGraph +import XcodeGraph -extension TuistGraph.ExecutionAction { - /// Maps a ProjectDescription.ExecutionAction instance into a TuistGraph.ExecutionAction instance. +extension XcodeGraph.ExecutionAction { + /// Maps a ProjectDescription.ExecutionAction instance into a XcodeGraph.ExecutionAction instance. /// - Parameters: /// - manifest: Manifest representation of execution action model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.ExecutionAction, generatorPaths: GeneratorPaths) throws -> TuistGraph + static func from(manifest: ProjectDescription.ExecutionAction, generatorPaths: GeneratorPaths) throws -> XcodeGraph .ExecutionAction { - let targetReference: TuistGraph.TargetReference? = try manifest.target.map { + let targetReference: XcodeGraph.TargetReference? = try manifest.target.map { .init( projectPath: try generatorPaths.resolveSchemeActionProjectPath($0.projectPath), name: $0.targetName diff --git a/Sources/TuistLoader/Models+ManifestMappers/FileCodeGen+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/FileCodeGen+ManifestMapper.swift index ff1192af603..b87dfd1b448 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/FileCodeGen+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/FileCodeGen+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.FileCodeGen { - static func from(manifest: ProjectDescription.FileCodeGen) -> TuistGraph.FileCodeGen { +extension XcodeGraph.FileCodeGen { + static func from(manifest: ProjectDescription.FileCodeGen) -> XcodeGraph.FileCodeGen { switch manifest { case .public: return .public diff --git a/Sources/TuistLoader/Models+ManifestMappers/FileElement+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/FileElement+ManifestMapper.swift index 2501c03c573..8555b0b05dc 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/FileElement+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/FileElement+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.FileElement { - /// Maps a ProjectDescription.FileElement instance into a [TuistGraph.FileElement] instance. +extension XcodeGraph.FileElement { + /// Maps a ProjectDescription.FileElement instance into a [XcodeGraph.FileElement] instance. /// Glob patterns in file elements are unfolded as part of the mapping. /// - Parameters: /// - manifest: Manifest representation of the file element. @@ -15,7 +15,7 @@ extension TuistGraph.FileElement { manifest: ProjectDescription.FileElement, generatorPaths: GeneratorPaths, includeFiles: @escaping (AbsolutePath) -> Bool = { _ in true } - ) throws -> [TuistGraph.FileElement] { + ) throws -> [XcodeGraph.FileElement] { func globFiles(_ path: AbsolutePath) throws -> [AbsolutePath] { if FileHandler.shared.exists(path), !FileHandler.shared.isFolder(path) { return [path] } diff --git a/Sources/TuistLoader/Models+ManifestMappers/FrameworkStatus+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/FrameworkStatus+ManifestMapper.swift index bc466de8f40..bf3ee45d7db 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/FrameworkStatus+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/FrameworkStatus+ManifestMapper.swift @@ -1,13 +1,13 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.FrameworkStatus { - /// Maps a ProjectDescription.FrameworkStatus instance into a TuistGraph.FrameworkStatus instance. +extension XcodeGraph.FrameworkStatus { + /// Maps a ProjectDescription.FrameworkStatus instance into a XcodeGraph.FrameworkStatus instance. /// - Parameters: /// - manifest: Manifest representation of the framework status model. - static func from(manifest: ProjectDescription.FrameworkStatus) -> TuistGraph.FrameworkStatus { + static func from(manifest: ProjectDescription.FrameworkStatus) -> XcodeGraph.FrameworkStatus { switch manifest { case .required: return .required diff --git a/Sources/TuistLoader/Models+ManifestMappers/Headers+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Headers+ManifestMapper.swift index d960f455b78..843011f5ec4 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Headers+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Headers+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.Headers { - /// Maps a ProjectDescription.Headers instance into a TuistGraph.Headers model. +extension XcodeGraph.Headers { + /// Maps a ProjectDescription.Headers instance into a XcodeGraph.Headers model. /// Glob patterns are resolved as part of the mapping process. /// - Parameters: /// - manifest: Manifest representation of Headers. @@ -16,7 +16,7 @@ extension TuistGraph.Headers { manifest: ProjectDescription.Headers, generatorPaths: GeneratorPaths, productName: String? - ) throws -> TuistGraph.Headers { + ) throws -> XcodeGraph.Headers { let resolvedUmbrellaPath = try manifest.umbrellaHeader.map { try generatorPaths.resolve(path: $0) } let headersFromUmbrella = try resolvedUmbrellaPath.map { Set(try UmbrellaHeaderHeadersExtractor.headers(from: $0, for: productName)) @@ -27,7 +27,7 @@ extension TuistGraph.Headers { let privateHeaders: [AbsolutePath] let projectHeaders: [AbsolutePath] - let allowedExtensions = TuistGraph.Headers.extensions + let allowedExtensions = XcodeGraph.Headers.extensions func unfold( _ list: FileList?, isPublic: Bool = false diff --git a/Sources/TuistLoader/Models+ManifestMappers/IDETemplateMacros+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/IDETemplateMacros+ManifestMapper.swift index a3045d9cb8b..f314a26d6a9 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/IDETemplateMacros+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/IDETemplateMacros+ManifestMapper.swift @@ -1,6 +1,6 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph extension IDETemplateMacros { static func from( @@ -10,7 +10,7 @@ extension IDETemplateMacros { switch manifest { case let .file(path): let templatePath = try generatorPaths.resolve(path: path) - let templateContent = try String(contentsOf: templatePath.asURL) + let templateContent = try String(contentsOf: URL(fileURLWithPath: templatePath.pathString)) return IDETemplateMacros(fileHeader: templateContent) case let .string(templateContent): return IDETemplateMacros(fileHeader: templateContent) diff --git a/Sources/TuistLoader/Models+ManifestMappers/InfoPlist+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/InfoPlist+ManifestMapper.swift index 2a800df11f1..e029d557b27 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/InfoPlist+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/InfoPlist+ManifestMapper.swift @@ -1,26 +1,26 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.InfoPlist { - /// Maps a ProjectDescription.InfoPlist instance into a TuistGraph.InfoPlist instance. +extension XcodeGraph.InfoPlist { + /// Maps a ProjectDescription.InfoPlist instance into a XcodeGraph.InfoPlist instance. /// - Parameters: /// - manifest: Manifest representation of the Info plist model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.InfoPlist?, generatorPaths: GeneratorPaths) throws -> TuistGraph.InfoPlist? { + static func from(manifest: ProjectDescription.InfoPlist?, generatorPaths: GeneratorPaths) throws -> XcodeGraph.InfoPlist? { switch manifest { case let .file(infoplistPath): return .file(path: try generatorPaths.resolve(path: infoplistPath)) case let .dictionary(dictionary): return .dictionary( - dictionary.mapValues { TuistGraph.Plist.Value.from(manifest: $0) } + dictionary.mapValues { XcodeGraph.Plist.Value.from(manifest: $0) } ) case let .extendingDefault(dictionary): return .extendingDefault( with: - dictionary.mapValues { TuistGraph.Plist.Value.from(manifest: $0) } + dictionary.mapValues { XcodeGraph.Plist.Value.from(manifest: $0) } ) case .none: return .none diff --git a/Sources/TuistLoader/Models+ManifestMappers/LaunchArgument+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/LaunchArgument+ManifestMapper.swift index 978b4bd49b1..3fc8daf8336 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/LaunchArgument+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/LaunchArgument+ManifestMapper.swift @@ -1,13 +1,13 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.LaunchArgument { - /// Maps a ProjectDescription.LaunchArgument instance into a TuistGraph.LaunchArgument instance. +extension XcodeGraph.LaunchArgument { + /// Maps a ProjectDescription.LaunchArgument instance into a XcodeGraph.LaunchArgument instance. /// - Parameters: /// - manifest: Manifest representation of launch argument model. - static func from(manifest: ProjectDescription.LaunchArgument) -> TuistGraph.LaunchArgument { + static func from(manifest: ProjectDescription.LaunchArgument) -> XcodeGraph.LaunchArgument { LaunchArgument(name: manifest.name, isEnabled: manifest.isEnabled) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/LaunchStyle+ManifestMappers.swift b/Sources/TuistLoader/Models+ManifestMappers/LaunchStyle+ManifestMappers.swift index 9a8e97f6cc6..374b8d9cb91 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/LaunchStyle+ManifestMappers.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/LaunchStyle+ManifestMappers.swift @@ -1,11 +1,11 @@ import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.LaunchStyle { - /// Maps a ProjectDescription.LaunchStyle instance into a TuistGraph.LaunchStyle model. +extension XcodeGraph.LaunchStyle { + /// Maps a ProjectDescription.LaunchStyle instance into a XcodeGraph.LaunchStyle model. /// - Parameters: /// - manifest: Manifest representation of LaunchStyle. - static func from(manifest: ProjectDescription.LaunchStyle) -> TuistGraph.LaunchStyle { + static func from(manifest: ProjectDescription.LaunchStyle) -> XcodeGraph.LaunchStyle { switch manifest { case .automatically: return .automatically diff --git a/Sources/TuistLoader/Models+ManifestMappers/MergedBinaryType+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/MergedBinaryType+ManifestMapper.swift index 5281ef03b3a..70564ca10ef 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/MergedBinaryType+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/MergedBinaryType+ManifestMapper.swift @@ -1,11 +1,11 @@ import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.MergedBinaryType { - /// Maps a ProjectDescription.MergedBinaryType instance into a TuistGraph.MergedBinaryType model. +extension XcodeGraph.MergedBinaryType { + /// Maps a ProjectDescription.MergedBinaryType instance into a XcodeGraph.MergedBinaryType model. /// - Parameters: /// - manifest: Manifest representation of Package. - static func from(manifest: ProjectDescription.MergedBinaryType) throws -> TuistGraph.MergedBinaryType { + static func from(manifest: ProjectDescription.MergedBinaryType) throws -> XcodeGraph.MergedBinaryType { switch manifest { case .automatic: return .automatic diff --git a/Sources/TuistLoader/Models+ManifestMappers/Package+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Package+ManifestMapper.swift index 14c2f9e69c2..b11b0732000 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Package+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Package+ManifestMapper.swift @@ -1,16 +1,16 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.Package { - /// Maps a ProjectDescription.Package instance into a TuistGraph.Package model. +extension XcodeGraph.Package { + /// Maps a ProjectDescription.Package instance into a XcodeGraph.Package model. /// - Parameters: /// - manifest: Manifest representation of Package. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Package, generatorPaths: GeneratorPaths) throws -> TuistGraph.Package { + static func from(manifest: ProjectDescription.Package, generatorPaths: GeneratorPaths) throws -> XcodeGraph.Package { switch manifest { case let .local(path: local): return .local(path: try generatorPaths.resolve(path: local)) @@ -20,12 +20,12 @@ extension TuistGraph.Package { } } -extension TuistGraph.Requirement { - /// Maps a ProjectDescription.Package.Requirement instance into a TuistGraph.Package.Requirement model. +extension XcodeGraph.Requirement { + /// Maps a ProjectDescription.Package.Requirement instance into a XcodeGraph.Package.Requirement model. /// - Parameters: /// - manifest: Manifest representation of Package. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Package.Requirement) -> TuistGraph.Requirement { + static func from(manifest: ProjectDescription.Package.Requirement) -> XcodeGraph.Requirement { switch manifest { case let .branch(branch): return .branch(branch) diff --git a/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift index 8f0fe37b475..c0ca642c35e 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift @@ -1,31 +1,34 @@ import Foundation +import Path import ProjectDescription -import TSCBasic +import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.PackageSettings { - /// Creates `TuistGraph.PackageSettings` instance from `ProjectDescription.PackageSettings` +extension TuistCore.PackageSettings { + /// Creates `XcodeGraph.PackageSettings` instance from `ProjectDescription.PackageSettings` /// instance. static func from( manifest: ProjectDescription.PackageSettings, - generatorPaths: GeneratorPaths + generatorPaths: GeneratorPaths, + swiftToolsVersion: TSCUtility.Version ) 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 + let productTypes = manifest.productTypes.mapValues { XcodeGraph.Product.from(manifest: $0) } + let productDestinations = try manifest.productDestinations.mapValues { try XcodeGraph.Destination.from(destinations: $0) } + let baseSettings = try XcodeGraph.Settings.from(manifest: manifest.baseSettings, generatorPaths: generatorPaths) + let targetSettings = manifest.targetSettings.mapValues { XcodeGraph.SettingsDictionary.from(manifest: $0) } + let projectOptions: [String: XcodeGraph.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, + productDestinations: productDestinations, baseSettings: baseSettings, targetSettings: targetSettings, projectOptions: projectOptions, - platforms: platforms + swiftToolsVersion: .init(stringLiteral: swiftToolsVersion.description) ) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/Platform+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Platform+ManifestMapper.swift index ccb87b0a8a0..ef826eda7ec 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Platform+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Platform+ManifestMapper.swift @@ -1,13 +1,13 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.Platform { - /// Maps a ProjectDescription.Platform instance into a TuistGraph.Platform instance. +extension XcodeGraph.Platform { + /// Maps a ProjectDescription.Platform instance into a XcodeGraph.Platform instance. /// - Parameters: /// - manifest: Manifest representation of platform model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Platform) throws -> TuistGraph.Platform { + static func from(manifest: ProjectDescription.Platform) throws -> XcodeGraph.Platform { switch manifest { case .macOS: return .macOS @@ -23,12 +23,12 @@ extension TuistGraph.Platform { } } -extension TuistGraph.PackagePlatform { - /// Maps a ProjectDescription.Platform instance into a TuistGraph.Platform instance. +extension XcodeGraph.PackagePlatform { + /// Maps a ProjectDescription.Platform instance into a XcodeGraph.Platform instance. /// - Parameters: /// - manifest: Manifest representation of platform model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.PackagePlatform) throws -> TuistGraph.PackagePlatform { + static func from(manifest: ProjectDescription.PackagePlatform) throws -> XcodeGraph.PackagePlatform { switch manifest { case .macOS: return .macOS diff --git a/Sources/TuistLoader/Models+ManifestMappers/Plist+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Plist+ManifestMapper.swift index 69526b66bef..00ee7fb28c9 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Plist+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Plist+ManifestMapper.swift @@ -1,15 +1,15 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.Plist.Value { - /// Maps a ProjectDescription.Plist.Value instance into a TuistGraph.Plist.Value instance. +extension XcodeGraph.Plist.Value { + /// Maps a ProjectDescription.Plist.Value instance into a XcodeGraph.Plist.Value instance. /// - Parameters: /// - manifest: Manifest representation of the Info plist value model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Plist.Value) -> TuistGraph.Plist.Value { + static func from(manifest: ProjectDescription.Plist.Value) -> XcodeGraph.Plist.Value { switch manifest { case let .string(value): return .string(value) @@ -20,9 +20,9 @@ extension TuistGraph.Plist.Value { case let .real(value): return .real(value) case let .array(value): - return .array(value.map { TuistGraph.Plist.Value.from(manifest: $0) }) + return .array(value.map { XcodeGraph.Plist.Value.from(manifest: $0) }) case let .dictionary(value): - return .dictionary(value.mapValues { TuistGraph.Plist.Value.from(manifest: $0) }) + return .dictionary(value.mapValues { XcodeGraph.Plist.Value.from(manifest: $0) }) } } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/PluginLocation+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/PluginLocation+ManifestMapper.swift index c8dfbda645a..d5c40208fb5 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/PluginLocation+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/PluginLocation+ManifestMapper.swift @@ -1,13 +1,13 @@ import Foundation import ProjectDescription -import TuistGraph +import TuistCore -extension TuistGraph.PluginLocation { - /// Convert from `ProjectDescription.PluginLocation` to `TuistGraph.PluginLocation` +extension TuistCore.PluginLocation { + /// Convert from `ProjectDescription.PluginLocation` to `XcodeGraph.PluginLocation` static func from( manifest: ProjectDescription.PluginLocation, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.PluginLocation { + ) throws -> TuistCore.PluginLocation { switch manifest.type { case let .local(path): return .local(path: try generatorPaths.resolve(path: path).pathString) diff --git a/Sources/TuistLoader/Models+ManifestMappers/Product+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Product+ManifestMapper.swift index 7290d7e0c4d..50629860c55 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Product+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Product+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.Product { - static func from(manifest: ProjectDescription.Product) -> TuistGraph.Product { +extension XcodeGraph.Product { + static func from(manifest: ProjectDescription.Product) -> XcodeGraph.Product { switch manifest { case .app: return .app diff --git a/Sources/TuistLoader/Models+ManifestMappers/ProfileAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ProfileAction+ManifestMapper.swift index 751cd536e28..c7a056580cd 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ProfileAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ProfileAction+ManifestMapper.swift @@ -1,32 +1,32 @@ import Foundation +import Path import ProjectDescription -import TSCBasic -import TuistGraph +import XcodeGraph -extension TuistGraph.ProfileAction { +extension XcodeGraph.ProfileAction { static func from( manifest: ProjectDescription.ProfileAction, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.ProfileAction { + ) throws -> XcodeGraph.ProfileAction { let configurationName = manifest.configuration.rawValue let preActions = try manifest.preActions.map { - try TuistGraph.ExecutionAction.from( + try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } let postActions = try manifest.postActions.map { - try TuistGraph.ExecutionAction.from( + try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let arguments = manifest.arguments.map { TuistGraph.Arguments.from(manifest: $0) } + let arguments = manifest.arguments.map { XcodeGraph.Arguments.from(manifest: $0) } - var executableResolved: TuistGraph.TargetReference? + var executableResolved: XcodeGraph.TargetReference? if let executable = manifest.executable { executableResolved = TargetReference( projectPath: try generatorPaths.resolveSchemeActionProjectPath(executable.projectPath), diff --git a/Sources/TuistLoader/Models+ManifestMappers/Project+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Project+ManifestMapper.swift index 1240e461ffd..d9202fe7d5c 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Project+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Project+ManifestMapper.swift @@ -1,10 +1,11 @@ import Foundation +import Path import ProjectDescription -import TSCBasic -import TuistGraph +import TuistCore +import XcodeGraph -extension TuistGraph.Project { - /// Maps a `ProjectDescription.Project` instance into a `TuistGraph.Project` instance. +extension XcodeGraph.Project { + /// Maps a `ProjectDescription.Project` instance into a `XcodeGraph.Project` instance. /// Glob patterns in file elements are unfolded as part of the mapping. /// - Parameters: /// - manifest: Manifest representation of the file element. @@ -17,34 +18,35 @@ extension TuistGraph.Project { manifest: ProjectDescription.Project, generatorPaths: GeneratorPaths, plugins: Plugins, - externalDependencies: [String: [TuistGraph.TargetDependency]], + externalDependencies: [String: [XcodeGraph.TargetDependency]], resourceSynthesizerPathLocator: ResourceSynthesizerPathLocating, isExternal: Bool - ) throws -> TuistGraph.Project { + ) throws -> XcodeGraph.Project { let name = manifest.name let xcodeProjectName = manifest.options.xcodeProjectName ?? name let organizationName = manifest.organizationName + let classPrefix = manifest.classPrefix let defaultKnownRegions = manifest.options.defaultKnownRegions let developmentRegion = manifest.options.developmentRegion - let options = TuistGraph.Project.Options.from(manifest: manifest.options) - let settings = try manifest.settings.map { try TuistGraph.Settings.from(manifest: $0, generatorPaths: generatorPaths) } + let options = XcodeGraph.Project.Options.from(manifest: manifest.options) + let settings = try manifest.settings.map { try XcodeGraph.Settings.from(manifest: $0, generatorPaths: generatorPaths) } let targets = try manifest.targets.map { - try TuistGraph.Target.from( + try XcodeGraph.Target.from( manifest: $0, generatorPaths: generatorPaths, externalDependencies: externalDependencies ) } - let schemes = try manifest.schemes.map { try TuistGraph.Scheme.from(manifest: $0, generatorPaths: generatorPaths) } + let schemes = try manifest.schemes.map { try XcodeGraph.Scheme.from(manifest: $0, generatorPaths: generatorPaths) } let additionalFiles = try manifest.additionalFiles - .flatMap { try TuistGraph.FileElement.from(manifest: $0, generatorPaths: generatorPaths) } - let packages = try manifest.packages.map { try TuistGraph.Package.from(manifest: $0, generatorPaths: generatorPaths) } + .flatMap { try XcodeGraph.FileElement.from(manifest: $0, generatorPaths: generatorPaths) } + let packages = try manifest.packages.map { try XcodeGraph.Package.from(manifest: $0, generatorPaths: generatorPaths) } let ideTemplateMacros = try manifest.fileHeaderTemplate .map { try IDETemplateMacros.from(manifest: $0, generatorPaths: generatorPaths) } let resourceSynthesizers = try manifest.resourceSynthesizers.map { - try TuistGraph.ResourceSynthesizer.from( + try XcodeGraph.ResourceSynthesizer.from( manifest: $0, generatorPaths: generatorPaths, plugins: plugins, @@ -57,6 +59,7 @@ extension TuistGraph.Project { xcodeProjPath: generatorPaths.manifestDirectory.appending(component: "\(xcodeProjectName).xcodeproj"), name: name, organizationName: organizationName, + classPrefix: classPrefix, defaultKnownRegions: defaultKnownRegions, developmentRegion: developmentRegion, options: options, diff --git a/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ManifestMapper.swift index 0241cba9608..5589e0c45cf 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ManifestMapper.swift @@ -1,8 +1,8 @@ import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.Project.Options { - /// Maps a ProjectDescription.ProjectOption instance into a TuistGraph.ProjectOption instance. +extension XcodeGraph.Project.Options { + /// Maps a ProjectDescription.ProjectOption instance into a XcodeGraph.ProjectOption instance. /// - Parameters: /// - manifest: Manifest representation of project options. static func from(manifest: ProjectDescription.Project.Options) -> Self { @@ -21,7 +21,7 @@ extension TuistGraph.Project.Options { } } -extension TuistGraph.Project.Options.AutomaticSchemesOptions { +extension XcodeGraph.Project.Options.AutomaticSchemesOptions { static func from( manifest: ProjectDescription.Project.Options.AutomaticSchemesOptions ) -> Self { @@ -52,7 +52,7 @@ extension TuistGraph.Project.Options.AutomaticSchemesOptions { } } -extension TuistGraph.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping { +extension XcodeGraph.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping { static func from( manifest: ProjectDescription.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping ) -> Self { diff --git a/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ToManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ToManifestMapper.swift new file mode 100644 index 00000000000..dce710ecff9 --- /dev/null +++ b/Sources/TuistLoader/Models+ManifestMappers/ProjectOption+ToManifestMapper.swift @@ -0,0 +1,81 @@ +import ProjectDescription +import XcodeGraph + +extension ProjectDescription.Project.Options { + /// Maps a XcodeGraph.ProjectOption instance into a ProjectDescription.ProjectOption instance. + /// - Parameters: + /// - manifest: Manifest representation of project options. + static func from(manifest: XcodeGraph.Project.Options) -> Self { + options( + automaticSchemesOptions: .from(manifest: manifest.automaticSchemesOptions), + disableBundleAccessors: manifest.disableBundleAccessors, + disableShowEnvironmentVarsInScriptPhases: manifest.disableShowEnvironmentVarsInScriptPhases, + disableSynthesizedResourceAccessors: manifest.disableSynthesizedResourceAccessors, + textSettings: .textSettings( + usesTabs: manifest.textSettings.usesTabs, + indentWidth: manifest.textSettings.indentWidth, + tabWidth: manifest.textSettings.tabWidth, + wrapsLines: manifest.textSettings.wrapsLines + ) + ) + } +} + +extension ProjectDescription.Project.Options.AutomaticSchemesOptions { + static func from( + manifest: XcodeGraph.Project.Options.AutomaticSchemesOptions + ) -> Self { + switch manifest { + case let .enabled( + targetSchemesGrouping, + codeCoverageEnabled, + testingOptions, + testLanguage, + testRegion, + testScreenCaptureFormat, + runLanguage, + runRegion + ): + return .enabled( + targetSchemesGrouping: .from(manifest: targetSchemesGrouping), + codeCoverageEnabled: codeCoverageEnabled, + testingOptions: .from(manifest: testingOptions), + testLanguage: testLanguage.map { .init(identifier: $0) }, + testRegion: testRegion, + testScreenCaptureFormat: testScreenCaptureFormat.map { .from(manifest: $0) }, + runLanguage: runLanguage.map { .init(identifier: $0) }, + runRegion: runRegion + ) + case .disabled: + return .disabled + } + } +} + +extension ProjectDescription.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping { + static func from( + manifest: XcodeGraph.Project.Options.AutomaticSchemesOptions.TargetSchemesGrouping + ) -> Self { + switch manifest { + case .singleScheme: + return .singleScheme + case let .byNameSuffix(build, test, run): + return .byNameSuffix(build: build, test: test, run: run) + case .notGrouped: + return .notGrouped + } + } +} + +extension ProjectDescription.ScreenCaptureFormat { + static func from( + manifest: XcodeGraph.ScreenCaptureFormat + ) -> Self { + switch manifest { + case .screenshots: + return .screenshots + case .screenRecording: + return .screenRecording + } + } +} diff --git a/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift index 58c678f5f77..20f3bc83e52 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.ResourceFileElement { - /// Maps a ProjectDescription.ResourceFileElement instance into a [TuistGraph.ResourceFileElement] instance. +extension XcodeGraph.ResourceFileElement { + /// Maps a ProjectDescription.ResourceFileElement instance into a [XcodeGraph.ResourceFileElement] instance. /// Glob patterns in file elements are unfolded as part of the mapping. /// - Parameters: /// - manifest: Manifest representation of the file element. @@ -15,7 +15,7 @@ extension TuistGraph.ResourceFileElement { manifest: ProjectDescription.ResourceFileElement, generatorPaths: GeneratorPaths, includeFiles: @escaping (AbsolutePath) -> Bool = { _ in true } - ) throws -> [TuistGraph.ResourceFileElement] { + ) throws -> [XcodeGraph.ResourceFileElement] { func globFiles(_ path: AbsolutePath, excluding: [String]) throws -> [AbsolutePath] { var excluded: Set = [] for path in excluding { diff --git a/Sources/TuistLoader/Models+ManifestMappers/ResourceSynthesizer+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ResourceSynthesizer+ManifestMapper.swift index a4b7d5e0779..945db504356 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ResourceSynthesizer+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ResourceSynthesizer+ManifestMapper.swift @@ -1,15 +1,16 @@ +import Path import ProjectDescription -import TSCBasic -import TuistGraph +import TuistCore +import XcodeGraph -extension TuistGraph.ResourceSynthesizer { +extension XcodeGraph.ResourceSynthesizer { static func from( manifest: ProjectDescription.ResourceSynthesizer, generatorPaths: GeneratorPaths, plugins: Plugins, resourceSynthesizerPathLocator: ResourceSynthesizerPathLocating ) throws -> Self { - let template: TuistGraph.ResourceSynthesizer.Template + let template: XcodeGraph.ResourceSynthesizer.Template switch manifest.templateType { case let .defaultTemplate(resourceName: resourceName): if let templatePath = resourceSynthesizerPathLocator.templatePath( @@ -30,11 +31,11 @@ extension TuistGraph.ResourceSynthesizer { } let parserOptions = manifest.parserOptions - .compactMapValues { TuistGraph.ResourceSynthesizer.Parser.Option.from(manifest: $0) + .compactMapValues { XcodeGraph.ResourceSynthesizer.Parser.Option.from(manifest: $0) } return .init( - parser: TuistGraph.ResourceSynthesizer.Parser.from(manifest: manifest.parser), + parser: XcodeGraph.ResourceSynthesizer.Parser.from(manifest: manifest.parser), parserOptions: parserOptions, extensions: manifest.extensions, template: template @@ -42,7 +43,7 @@ extension TuistGraph.ResourceSynthesizer { } } -extension TuistGraph.ResourceSynthesizer.Parser { +extension XcodeGraph.ResourceSynthesizer.Parser { static func from( manifest: ProjectDescription.ResourceSynthesizer.Parser ) -> Self { @@ -69,7 +70,7 @@ extension TuistGraph.ResourceSynthesizer.Parser { } } -extension TuistGraph.ResourceSynthesizer.Parser.Option { +extension XcodeGraph.ResourceSynthesizer.Parser.Option { static func from( manifest: ProjectDescription.ResourceSynthesizer.Parser.Option ) -> Self { diff --git a/Sources/TuistLoader/Models+ManifestMappers/RunAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/RunAction+ManifestMapper.swift index ad234c3efb5..b8de5f91815 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/RunAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/RunAction+ManifestMapper.swift @@ -1,18 +1,18 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.RunAction { - /// Maps a ProjectDescription.RunAction instance into a TuistGraph.RunAction instance. +extension XcodeGraph.RunAction { + /// Maps a ProjectDescription.RunAction instance into a XcodeGraph.RunAction instance. /// - Parameters: /// - manifest: Manifest representation of the settings. /// - generatorPaths: Generator paths. static func from( manifest: ProjectDescription.RunAction, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.RunAction { + ) throws -> XcodeGraph.RunAction { let configurationName = manifest.configuration.rawValue let customLLDBInitFile = try manifest.customLLDBInitFile.map { @@ -20,22 +20,22 @@ extension TuistGraph.RunAction { } let preActions = try manifest.preActions.map { - try TuistGraph.ExecutionAction.from( + try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } let postActions = try manifest.postActions.map { - try TuistGraph.ExecutionAction.from( + try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let arguments = manifest.arguments.map { TuistGraph.Arguments.from(manifest: $0) } + let arguments = manifest.arguments.map { XcodeGraph.Arguments.from(manifest: $0) } - var executableResolved: TuistGraph.TargetReference? + var executableResolved: XcodeGraph.TargetReference? if let executable = manifest.executable { executableResolved = TargetReference( projectPath: try generatorPaths.resolveSchemeActionProjectPath(executable.projectPath), @@ -43,21 +43,21 @@ extension TuistGraph.RunAction { ) } - let options = try TuistGraph.RunActionOptions.from(manifest: manifest.options, generatorPaths: generatorPaths) + let options = try XcodeGraph.RunActionOptions.from(manifest: manifest.options, generatorPaths: generatorPaths) - let diagnosticsOptions = Set(manifest.diagnosticsOptions.map { TuistGraph.SchemeDiagnosticsOption.from(manifest: $0) }) + let diagnosticsOptions = XcodeGraph.SchemeDiagnosticsOptions.from(manifest: manifest.diagnosticsOptions) - let expandVariablesFromTarget: TuistGraph.TargetReference? + let expandVariablesFromTarget: XcodeGraph.TargetReference? expandVariablesFromTarget = try manifest.expandVariableFromTarget.map { - TuistGraph.TargetReference( + XcodeGraph.TargetReference( projectPath: try generatorPaths.resolveSchemeActionProjectPath($0.projectPath), name: $0.targetName ) } - let launchStyle = TuistGraph.LaunchStyle.from(manifest: manifest.launchStyle) + let launchStyle = XcodeGraph.LaunchStyle.from(manifest: manifest.launchStyle) - return TuistGraph.RunAction( + return XcodeGraph.RunAction( configurationName: configurationName, attachDebugger: manifest.attachDebugger, customLLDBInitFile: customLLDBInitFile, diff --git a/Sources/TuistLoader/Models+ManifestMappers/RunActionOptions+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/RunActionOptions+ManifestMapper.swift index 371545fb66b..abb8f58245a 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/RunActionOptions+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/RunActionOptions+ManifestMapper.swift @@ -1,20 +1,20 @@ import Foundation +import Path import ProjectDescription -import TSCBasic -import TuistGraph +import XcodeGraph -extension TuistGraph.RunActionOptions { - /// Maps a ProjectDescription.RunActionOptions instance into a TuistGraph.RunActionOptions instance. +extension XcodeGraph.RunActionOptions { + /// Maps a ProjectDescription.RunActionOptions instance into a XcodeGraph.RunActionOptions instance. /// - Parameters: /// - manifest: Manifest representation of the options. /// - generatorPaths: Generator paths. static func from( manifest: ProjectDescription.RunActionOptions, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.RunActionOptions { + ) throws -> XcodeGraph.RunActionOptions { var language: String? var storeKitConfigurationPath: AbsolutePath? - var simulatedLocation: SimulatedLocation? + var simulatedLocation: XcodeGraph.SimulatedLocation? var enableGPUFrameCaptureMode: GPUFrameCaptureMode language = manifest.language?.identifier @@ -45,7 +45,7 @@ extension TuistGraph.RunActionOptions { } } - return TuistGraph.RunActionOptions( + return XcodeGraph.RunActionOptions( language: language, region: manifest.region, storeKitConfigurationPath: storeKitConfigurationPath, diff --git a/Sources/TuistLoader/Models+ManifestMappers/SDKStatus+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/SDKStatus+ManifestMapper.swift index dfed2b929e2..e084cc635cd 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/SDKStatus+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/SDKStatus+ManifestMapper.swift @@ -1,14 +1,14 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.SDKStatus { - /// Maps a ProjectDescription.SDKStatus instance into a TuistGraph.SDKStatus instance. +extension XcodeGraph.SDKStatus { + /// Maps a ProjectDescription.SDKStatus instance into a XcodeGraph.SDKStatus instance. /// - Parameters: /// - manifest: Manifest representation of SDK status model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.SDKStatus) -> TuistGraph.SDKStatus { + static func from(manifest: ProjectDescription.SDKStatus) -> XcodeGraph.SDKStatus { switch manifest { case .required: return .required diff --git a/Sources/TuistLoader/Models+ManifestMappers/Scheme+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Scheme+ManifestMapper.swift index 6c3654ebd43..098581e1b4d 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Scheme+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Scheme+ManifestMapper.swift @@ -1,39 +1,39 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.Scheme { - /// Maps a ProjectDescription.Scheme instance into a TuistGraph.Scheme instance. +extension XcodeGraph.Scheme { + /// Maps a ProjectDescription.Scheme instance into a XcodeGraph.Scheme instance. /// - Parameters: /// - manifest: Manifest representation of build action model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Scheme, generatorPaths: GeneratorPaths) throws -> TuistGraph.Scheme { + static func from(manifest: ProjectDescription.Scheme, generatorPaths: GeneratorPaths) throws -> XcodeGraph.Scheme { let name = manifest.name let shared = manifest.shared let hidden = manifest.hidden - let buildAction = try manifest.buildAction.map { try TuistGraph.BuildAction.from( + let buildAction = try manifest.buildAction.map { try XcodeGraph.BuildAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let testAction = try manifest.testAction.map { try TuistGraph.TestAction.from( + let testAction = try manifest.testAction.map { try XcodeGraph.TestAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let runAction = try manifest.runAction.map { try TuistGraph.RunAction.from( + let runAction = try manifest.runAction.map { try XcodeGraph.RunAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let archiveAction = try manifest.archiveAction.map { try TuistGraph.ArchiveAction.from( + let archiveAction = try manifest.archiveAction.map { try XcodeGraph.ArchiveAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let profileAction = try manifest.profileAction.map { try TuistGraph.ProfileAction.from( + let profileAction = try manifest.profileAction.map { try XcodeGraph.ProfileAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let analyzeAction = try manifest.analyzeAction.map { try TuistGraph.AnalyzeAction.from( + let analyzeAction = try manifest.analyzeAction.map { try XcodeGraph.AnalyzeAction.from( manifest: $0, generatorPaths: generatorPaths ) } diff --git a/Sources/TuistLoader/Models+ManifestMappers/SchemeDiagnosticsOption+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/SchemeDiagnosticsOption+ManifestMapper.swift index ff50236fd1d..5350a83ca62 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/SchemeDiagnosticsOption+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/SchemeDiagnosticsOption+ManifestMapper.swift @@ -1,17 +1,18 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.SchemeDiagnosticsOption { - static func from(manifest: ProjectDescription.SchemeDiagnosticsOption) -> TuistGraph.SchemeDiagnosticsOption { - switch manifest { - case .enableAddressSanitizer: return .enableAddressSanitizer - case .enableDetectStackUseAfterReturn: return .enableASanStackUseAfterReturn - case .enableThreadSanitizer: return .enableThreadSanitizer - case .mainThreadChecker: return .mainThreadChecker - case .performanceAntipatternChecker: return .performanceAntipatternChecker - } +extension XcodeGraph.SchemeDiagnosticsOptions { + static func from(manifest: ProjectDescription.SchemeDiagnosticsOptions) -> XcodeGraph.SchemeDiagnosticsOptions { + return XcodeGraph.SchemeDiagnosticsOptions( + addressSanitizerEnabled: manifest.addressSanitizerEnabled, + detectStackUseAfterReturnEnabled: manifest.detectStackUseAfterReturnEnabled, + threadSanitizerEnabled: manifest.threadSanitizerEnabled, + mainThreadCheckerEnabled: manifest.mainThreadCheckerEnabled, + performanceAntipatternCheckerEnabled: manifest + .performanceAntipatternCheckerEnabled + ) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/ScreenCaptureFormat+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ScreenCaptureFormat+ManifestMapper.swift index 1fe21dcde9e..06a47c7db5f 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ScreenCaptureFormat+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ScreenCaptureFormat+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.ScreenCaptureFormat { - static func from(manifest: ProjectDescription.ScreenCaptureFormat) -> TuistGraph.ScreenCaptureFormat { +extension XcodeGraph.ScreenCaptureFormat { + static func from(manifest: ProjectDescription.ScreenCaptureFormat) -> XcodeGraph.ScreenCaptureFormat { switch manifest { case .screenshots: return .screenshots diff --git a/Sources/TuistLoader/Models+ManifestMappers/SettingValue+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/SettingValue+ManifestMapper.swift index 3566ee25523..673da04b904 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/SettingValue+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/SettingValue+ManifestMapper.swift @@ -1,16 +1,16 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.SettingValue { - /// Maps a ProjectDescription.SettingValue instance into a TuistGraph.SettingValue model. +extension XcodeGraph.SettingValue { + /// Maps a ProjectDescription.SettingValue instance into a XcodeGraph.SettingValue model. /// - Parameters: /// - manifest: Manifest representation of setting value. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.SettingValue) -> TuistGraph.SettingValue { + static func from(manifest: ProjectDescription.SettingValue) -> XcodeGraph.SettingValue { switch manifest { case let .string(value): return .string(value) diff --git a/Sources/TuistLoader/Models+ManifestMappers/Settings+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Settings+ManifestMapper.swift index b1a9e02c862..31f522f64f9 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Settings+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Settings+ManifestMapper.swift @@ -1,25 +1,25 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.Settings { - /// Maps a ProjectDescription.Settings instance into a TuistGraph.Settings instance. +extension XcodeGraph.Settings { + /// Maps a ProjectDescription.Settings instance into a XcodeGraph.Settings instance. /// - Parameters: /// - manifest: Manifest representation of the settings. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.Settings, generatorPaths: GeneratorPaths) throws -> TuistGraph.Settings { - let base = manifest.base.mapValues(TuistGraph.SettingValue.from) + static func from(manifest: ProjectDescription.Settings, generatorPaths: GeneratorPaths) throws -> XcodeGraph.Settings { + let base = manifest.base.mapValues(XcodeGraph.SettingValue.from) let configurations = try manifest.configurations - .reduce([TuistGraph.BuildConfiguration: TuistGraph.Configuration?]()) { acc, val in + .reduce([XcodeGraph.BuildConfiguration: XcodeGraph.Configuration?]()) { acc, val in var result = acc - let variant = TuistGraph.BuildConfiguration.from(manifest: val) - result[variant] = try TuistGraph.Configuration.from(manifest: val, generatorPaths: generatorPaths) + let variant = XcodeGraph.BuildConfiguration.from(manifest: val) + result[variant] = try XcodeGraph.Configuration.from(manifest: val, generatorPaths: generatorPaths) return result } - let defaultSettings = TuistGraph.DefaultSettings.from(manifest: manifest.defaultSettings) - return TuistGraph.Settings( + let defaultSettings = XcodeGraph.DefaultSettings.from(manifest: manifest.defaultSettings) + return XcodeGraph.Settings( base: base, configurations: configurations, defaultSettings: defaultSettings diff --git a/Sources/TuistLoader/Models+ManifestMappers/SettingsDictionary+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/SettingsDictionary+ManifestMapper.swift index 8f57a3a8f0a..51a32d4bac2 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/SettingsDictionary+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/SettingsDictionary+ManifestMapper.swift @@ -1,18 +1,18 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.SettingsDictionary { - /// Maps a ProjectDescription.SettingsDictionary instance into a TuistGraph.SettingsDictionary instance. +extension XcodeGraph.SettingsDictionary { + /// Maps a ProjectDescription.SettingsDictionary instance into a XcodeGraph.SettingsDictionary instance. /// - Parameters: /// - manifest: Manifest representation of deployment target model. - static func from(manifest: ProjectDescription.SettingsDictionary) -> TuistGraph.SettingsDictionary { + static func from(manifest: ProjectDescription.SettingsDictionary) -> XcodeGraph.SettingsDictionary { manifest.mapValues { value in switch value { case let .string(stringValue): - return TuistGraph.SettingValue.string(stringValue) + return XcodeGraph.SettingValue.string(stringValue) case let .array(arrayValue): - return TuistGraph.SettingValue.array(arrayValue) + return XcodeGraph.SettingValue.array(arrayValue) } } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/SwiftPackageManagerDependencies+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/SwiftPackageManagerDependencies+ManifestMapper.swift deleted file mode 100644 index f896e72a00d..00000000000 --- a/Sources/TuistLoader/Models+ManifestMappers/SwiftPackageManagerDependencies+ManifestMapper.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -extension TuistGraph.SwiftPackageManagerDependencies { - /// Creates `TuistGraph.SwiftPackageManagerDependencies` instance from `ProjectDescription.SwiftPackageManagerDependencies` - /// instance. - static func from( - manifest: ProjectDescription.SwiftPackageManagerDependencies, - generatorPaths: GeneratorPaths - ) throws -> Self { - let packagesOrManifest: TuistGraph.PackagesOrManifest - switch manifest.packagesOrManifest { - case let .packages(packages): - packagesOrManifest = .packages(try packages.map { try TuistGraph.Package.from( - manifest: $0, - generatorPaths: generatorPaths - ) }) - case .manifest: - packagesOrManifest = .manifest - } - 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) } - - return .init( - packagesOrManifest, - productTypes: productTypes, - baseSettings: baseSettings, - targetSettings: targetSettings, - projectOptions: projectOptions - ) - } -} diff --git a/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift index eb2db60ec73..b6cc610ac8f 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public enum TargetManifestMapperError: FatalError { case invalidResourcesGlob(targetName: String, invalidGlobs: [InvalidGlob]) @@ -19,8 +19,8 @@ public enum TargetManifestMapperError: FatalError { } // swiftlint:disable function_body_length -extension TuistGraph.Target { - /// Maps a ProjectDescription.Target instance into a TuistGraph.Target instance. +extension XcodeGraph.Target { + /// Maps a ProjectDescription.Target instance into a XcodeGraph.Target instance. /// - Parameters: /// - manifest: Manifest representation of the target. /// - generatorPaths: Generator paths. @@ -28,31 +28,31 @@ extension TuistGraph.Target { static func from( manifest: ProjectDescription.Target, generatorPaths: GeneratorPaths, - externalDependencies: [String: [TuistGraph.TargetDependency]] - ) throws -> TuistGraph.Target { + externalDependencies: [String: [XcodeGraph.TargetDependency]] + ) throws -> XcodeGraph.Target { let name = manifest.name - let destinations = try TuistGraph.Destination.from(destinations: manifest.destinations) + let destinations = try XcodeGraph.Destination.from(destinations: manifest.destinations) - let product = TuistGraph.Product.from(manifest: manifest.product) + let product = XcodeGraph.Product.from(manifest: manifest.product) let bundleId = manifest.bundleId let productName = manifest.productName - let deploymentTargets = manifest.deploymentTargets.map { TuistGraph.DeploymentTargets.from(manifest: $0) } ?? .empty() + let deploymentTargets = manifest.deploymentTargets.map { XcodeGraph.DeploymentTargets.from(manifest: $0) } ?? .empty() let dependencies = try manifest.dependencies.flatMap { - try TuistGraph.TargetDependency.from( + try XcodeGraph.TargetDependency.from( manifest: $0, generatorPaths: generatorPaths, externalDependencies: externalDependencies ) } - let infoPlist = try TuistGraph.InfoPlist.from(manifest: manifest.infoPlist, generatorPaths: generatorPaths) + let infoPlist = try XcodeGraph.InfoPlist.from(manifest: manifest.infoPlist, generatorPaths: generatorPaths) - let entitlements = try TuistGraph.Entitlements.from(manifest: manifest.entitlements, generatorPaths: generatorPaths) + let entitlements = try XcodeGraph.Entitlements.from(manifest: manifest.entitlements, generatorPaths: generatorPaths) - let settings = try manifest.settings.map { try TuistGraph.Settings.from(manifest: $0, generatorPaths: generatorPaths) } - let mergedBinaryType = try TuistGraph.MergedBinaryType.from(manifest: manifest.mergedBinaryType) + let settings = try manifest.settings.map { try XcodeGraph.Settings.from(manifest: $0, generatorPaths: generatorPaths) } + let mergedBinaryType = try XcodeGraph.MergedBinaryType.from(manifest: manifest.mergedBinaryType) let (sources, sourcesPlaygrounds) = try sourcesAndPlaygrounds( manifest: manifest, @@ -70,21 +70,21 @@ extension TuistGraph.Target { } let copyFiles = try (manifest.copyFiles ?? []).map { - try TuistGraph.CopyFilesAction.from(manifest: $0, generatorPaths: generatorPaths) + try XcodeGraph.CopyFilesAction.from(manifest: $0, generatorPaths: generatorPaths) } - let headers = try manifest.headers.map { try TuistGraph.Headers.from( + let headers = try manifest.headers.map { try XcodeGraph.Headers.from( manifest: $0, generatorPaths: generatorPaths, productName: manifest.productName ) } let coreDataModels = try manifest.coreDataModels.map { - try TuistGraph.CoreDataModel.from(manifest: $0, generatorPaths: generatorPaths) - } + resourcesCoreDatas.map { try TuistGraph.CoreDataModel.from(path: $0) } + try XcodeGraph.CoreDataModel.from(manifest: $0, generatorPaths: generatorPaths) + } + resourcesCoreDatas.map { try XcodeGraph.CoreDataModel.from(path: $0) } let scripts = try manifest.scripts.map { - try TuistGraph.TargetScript.from(manifest: $0, generatorPaths: generatorPaths) + try XcodeGraph.TargetScript.from(manifest: $0, generatorPaths: generatorPaths) } let environmentVariables = manifest.environmentVariables.mapValues(EnvironmentVariable.from) @@ -93,13 +93,17 @@ extension TuistGraph.Target { let playgrounds = sourcesPlaygrounds + resourcesPlaygrounds let additionalFiles = try manifest.additionalFiles - .flatMap { try TuistGraph.FileElement.from(manifest: $0, generatorPaths: generatorPaths) } + .flatMap { try XcodeGraph.FileElement.from(manifest: $0, generatorPaths: generatorPaths) } let buildRules = manifest.buildRules.map { - TuistGraph.BuildRule.from(manifest: $0) + XcodeGraph.BuildRule.from(manifest: $0) } - return TuistGraph.Target( + let onDemandResourcesTags = manifest.onDemandResourcesTags.map { + XcodeGraph.OnDemandResourcesTags(initialInstall: $0.initialInstall, prefetchOrder: $0.prefetchOrder) + } + + return XcodeGraph.Target( name: name, destinations: destinations, product: product, @@ -123,7 +127,8 @@ extension TuistGraph.Target { additionalFiles: additionalFiles, buildRules: buildRules, mergedBinaryType: mergedBinaryType, - mergeable: manifest.mergeable + mergeable: manifest.mergeable, + onDemandResourcesTags: onDemandResourcesTags ) } @@ -134,23 +139,32 @@ extension TuistGraph.Target { generatorPaths: GeneratorPaths // swiftlint:disable:next large_tuple ) throws -> ( - resources: [TuistGraph.ResourceFileElement], + resources: XcodeGraph.ResourceFileElements, playgrounds: [AbsolutePath], coreDataModels: [AbsolutePath], invalidResourceGlobs: [InvalidGlob] ) { let resourceFilter = { (path: AbsolutePath) -> Bool in - TuistGraph.Target.isResource(path: path) + XcodeGraph.Target.isResource(path: path) + } + + let privacyManifest: XcodeGraph.PrivacyManifest? = manifest.resources?.privacyManifest.map { + return XcodeGraph.PrivacyManifest( + tracking: $0.tracking, + trackingDomains: $0.trackingDomains, + collectedDataTypes: $0.collectedDataTypes.map { $0.mapValues { XcodeGraph.Plist.Value.from(manifest: $0) }}, + accessedApiTypes: $0.accessedApiTypes.map { $0.mapValues { XcodeGraph.Plist.Value.from(manifest: $0) }} + ) } var invalidResourceGlobs: [InvalidGlob] = [] - var filteredResources: [TuistGraph.ResourceFileElement] = [] + var filteredResources: XcodeGraph.ResourceFileElements = .init([], privacyManifest: privacyManifest) var playgrounds: Set = [] var coreDataModels: Set = [] - let allResources = try (manifest.resources?.resources ?? []).flatMap { manifest -> [TuistGraph.ResourceFileElement] in + let allResources = try (manifest.resources?.resources ?? []).flatMap { manifest -> [XcodeGraph.ResourceFileElement] in do { - return try TuistGraph.ResourceFileElement.from( + return try XcodeGraph.ResourceFileElement.from( manifest: manifest, generatorPaths: generatorPaths, includeFiles: resourceFilter @@ -163,14 +177,14 @@ extension TuistGraph.Target { for fileElement in allResources { switch fileElement { - case .folderReference: filteredResources.append(fileElement) + case .folderReference: filteredResources.resources.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) + filteredResources.resources.append(fileElement) } } } @@ -187,16 +201,16 @@ extension TuistGraph.Target { manifest: ProjectDescription.Target, targetName: String, generatorPaths: GeneratorPaths - ) throws -> (sources: [TuistGraph.SourceFile], playgrounds: [AbsolutePath]) { - var sourcesWithoutPlaygrounds: [TuistGraph.SourceFile] = [] + ) throws -> (sources: [XcodeGraph.SourceFile], playgrounds: [AbsolutePath]) { + var sourcesWithoutPlaygrounds: [XcodeGraph.SourceFile] = [] var playgrounds: Set = [] // Sources - let allSources = try TuistGraph.Target.sources(targetName: targetName, sources: manifest.sources?.globs.map { glob in + let allSources = try XcodeGraph.Target.sources(targetName: targetName, sources: manifest.sources?.globs.map { glob in let globPath = try generatorPaths.resolve(path: glob.glob).pathString let excluding: [String] = try glob.excluding.compactMap { try generatorPaths.resolve(path: $0).pathString } - let mappedCodeGen = glob.codeGen.map(TuistGraph.FileCodeGen.from) - return TuistGraph.SourceFileGlob( + let mappedCodeGen = glob.codeGen.map(XcodeGraph.FileCodeGen.from) + return XcodeGraph.SourceFileGlob( glob: globPath, excluding: excluding, compilerFlags: glob.compilerFlags, diff --git a/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift index 3f3419f29d9..834ce127b44 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift @@ -1,20 +1,20 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.TargetScript { - /// Maps a ProjectDescription.TargetAction instance into a TuistGraph.TargetAction model. +extension XcodeGraph.TargetScript { + /// Maps a ProjectDescription.TargetAction instance into a XcodeGraph.TargetAction model. /// - Parameters: /// - manifest: Manifest representation of target action. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.TargetScript, generatorPaths: GeneratorPaths) throws -> TuistGraph + static func from(manifest: ProjectDescription.TargetScript, generatorPaths: GeneratorPaths) throws -> XcodeGraph .TargetScript { let name = manifest.name - let order = TuistGraph.TargetScript.Order.from(manifest: manifest.order) + let order = XcodeGraph.TargetScript.Order.from(manifest: manifest.order) let inputPaths = try manifest.inputPaths .compactMap { try $0.unfold(generatorPaths: generatorPaths) } .flatMap { $0 } @@ -35,7 +35,7 @@ extension TuistGraph.TargetScript { dependencyFile = nil } - let script: TuistGraph.TargetScript.Script + let script: XcodeGraph.TargetScript.Script switch manifest.script { case let .embedded(text): script = .embedded(text) @@ -76,12 +76,12 @@ extension TuistGraph.TargetScript { } } -extension TuistGraph.TargetScript.Order { - /// Maps a ProjectDescription.TargetAction.Order instance into a TuistGraph.TargetAction.Order model. +extension XcodeGraph.TargetScript.Order { + /// Maps a ProjectDescription.TargetAction.Order instance into a XcodeGraph.TargetAction.Order model. /// - Parameters: /// - manifest: Manifest representation of target action order. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.TargetScript.Order) -> TuistGraph.TargetScript.Order { + static func from(manifest: ProjectDescription.TargetScript.Order) -> XcodeGraph.TargetScript.Order { switch manifest { case .pre: return .pre diff --git a/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift index 721680c67f1..b0fce01a5a6 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph // MARK: - TargetDependency Mapper Error @@ -20,8 +20,8 @@ public enum TargetDependencyMapperError: FatalError { } } -extension TuistGraph.TargetDependency { - /// Maps a ProjectDescription.TargetDependency instance into a TuistGraph.TargetDependency instance. +extension XcodeGraph.TargetDependency { + /// Maps a ProjectDescription.TargetDependency instance into a XcodeGraph.TargetDependency instance. /// - Parameters: /// - manifest: Manifest representation of the target dependency model. /// - generatorPaths: Generator paths. @@ -29,8 +29,8 @@ extension TuistGraph.TargetDependency { static func from( // swiftlint:disable:this function_body_length manifest: ProjectDescription.TargetDependency, generatorPaths: GeneratorPaths, - externalDependencies: [String: [TuistGraph.TargetDependency]] - ) throws -> [TuistGraph.TargetDependency] { + externalDependencies: [String: [XcodeGraph.TargetDependency]] + ) throws -> [XcodeGraph.TargetDependency] { switch manifest { case let .target(name, condition): return [.target(name: name, condition: condition?.asGraphCondition)] @@ -66,9 +66,6 @@ extension TuistGraph.TargetDependency { case .plugin: return [.package(product: product, type: .plugin, condition: condition?.asGraphCondition)] } - case let .packagePlugin(product, condition): - logger.warning(".packagePlugin is deprecated. Use .package(product:, type: .plugin) instead.") - return [.package(product: product, type: .plugin, condition: condition?.asGraphCondition)] case let .sdk(name, type, status, condition): return [ .sdk( @@ -98,19 +95,19 @@ extension TuistGraph.TargetDependency { } extension ProjectDescription.PlatformFilters { - var asGraphFilters: TuistGraph.PlatformFilters { - Set(map(\.graphPlatformFilter)) + var asGraphFilters: XcodeGraph.PlatformFilters { + Set(map(\.graphPlatformFilter)) } } extension ProjectDescription.PlatformCondition { - var asGraphCondition: TuistGraph.PlatformCondition? { + var asGraphCondition: XcodeGraph.PlatformCondition? { .when(Set(platformFilters.asGraphFilters)) } } extension ProjectDescription.PlatformFilter { - fileprivate var graphPlatformFilter: TuistGraph.PlatformFilter { + fileprivate var graphPlatformFilter: XcodeGraph.PlatformFilter { switch self { case .ios: .ios @@ -136,6 +133,8 @@ extension ProjectDescription.SDKType { switch self { case .library: return "lib" + case .swiftLibrary: + return "libswift" case .framework: return "" } @@ -144,7 +143,7 @@ extension ProjectDescription.SDKType { /// The extension associated to the type fileprivate var fileExtension: String { switch self { - case .library: + case .library, .swiftLibrary: return "tbd" case .framework: return "framework" diff --git a/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift index de61bd5f2c7..51611623559 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift @@ -1,28 +1,28 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.TestAction { +extension XcodeGraph.TestAction { // swiftlint:disable function_body_length - /// Maps a ProjectDescription.TestAction instance into a TuistGraph.TestAction instance. + /// Maps a ProjectDescription.TestAction instance into a XcodeGraph.TestAction instance. /// - Parameters: /// - manifest: Manifest representation of test action model. /// - generatorPaths: Generator paths. - static func from(manifest: ProjectDescription.TestAction, generatorPaths: GeneratorPaths) throws -> TuistGraph.TestAction { + static func from(manifest: ProjectDescription.TestAction, generatorPaths: GeneratorPaths) throws -> XcodeGraph.TestAction { // swiftlint:enable function_body_length - let testPlans: [TuistGraph.TestPlan]? - let targets: [TuistGraph.TestableTarget] - let arguments: TuistGraph.Arguments? + let testPlans: [XcodeGraph.TestPlan]? + let targets: [XcodeGraph.TestableTarget] + let arguments: XcodeGraph.Arguments? let coverage: Bool - let codeCoverageTargets: [TuistGraph.TargetReference] - let expandVariablesFromTarget: TuistGraph.TargetReference? - let diagnosticsOptions: Set + let codeCoverageTargets: [XcodeGraph.TargetReference] + let expandVariablesFromTarget: XcodeGraph.TargetReference? + let diagnosticsOptions: XcodeGraph.SchemeDiagnosticsOptions let language: SchemeLanguage? let region: String? - let preferredScreenCaptureFormat: TuistGraph.ScreenCaptureFormat? + let preferredScreenCaptureFormat: XcodeGraph.ScreenCaptureFormat? let skippedTests: [String]? if let plans = manifest.testPlans { @@ -38,30 +38,29 @@ extension TuistGraph.TestAction { coverage = false codeCoverageTargets = [] expandVariablesFromTarget = nil - diagnosticsOptions = [] + diagnosticsOptions = .init() language = nil region = nil preferredScreenCaptureFormat = nil skippedTests = nil } else { targets = try manifest.targets - .map { try TuistGraph.TestableTarget.from(manifest: $0, generatorPaths: generatorPaths) } - arguments = manifest.arguments.map { TuistGraph.Arguments.from(manifest: $0) } + .map { try XcodeGraph.TestableTarget.from(manifest: $0, generatorPaths: generatorPaths) } + arguments = manifest.arguments.map { XcodeGraph.Arguments.from(manifest: $0) } coverage = manifest.options.coverage codeCoverageTargets = try manifest.options.codeCoverageTargets.map { - TuistGraph.TargetReference( + XcodeGraph.TargetReference( projectPath: try generatorPaths.resolveSchemeActionProjectPath($0.projectPath), name: $0.targetName ) } expandVariablesFromTarget = try manifest.expandVariableFromTarget.map { - TuistGraph.TargetReference( + XcodeGraph.TargetReference( projectPath: try generatorPaths.resolveSchemeActionProjectPath($0.projectPath), name: $0.targetName ) } - - diagnosticsOptions = Set(manifest.diagnosticsOptions.map { TuistGraph.SchemeDiagnosticsOption.from(manifest: $0) }) + diagnosticsOptions = XcodeGraph.SchemeDiagnosticsOptions.from(manifest: manifest.diagnosticsOptions) language = manifest.options.language region = manifest.options.region preferredScreenCaptureFormat = manifest.options.preferredScreenCaptureFormat @@ -73,11 +72,11 @@ extension TuistGraph.TestAction { } let configurationName = manifest.configuration.rawValue - let preActions = try manifest.preActions.map { try TuistGraph.ExecutionAction.from( + let preActions = try manifest.preActions.map { try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } - let postActions = try manifest.postActions.map { try TuistGraph.ExecutionAction.from( + let postActions = try manifest.postActions.map { try XcodeGraph.ExecutionAction.from( manifest: $0, generatorPaths: generatorPaths ) } diff --git a/Sources/TuistLoader/Models+ManifestMappers/TestPlan+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TestPlan+ManifestMapper.swift index e810619148f..c6a0ea31e9d 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TestPlan+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TestPlan+ManifestMapper.swift @@ -1,11 +1,11 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph extension TestPlan { init(path: AbsolutePath, isDefault: Bool, generatorPaths: GeneratorPaths) throws { let jsonDecoder = JSONDecoder() - let testPlanData = try Data(contentsOf: path.asURL) + let testPlanData = try Data(contentsOf: URL(fileURLWithPath: path.pathString)) let xcTestPlan = try jsonDecoder.decode(XCTestPlan.self, from: testPlanData) try self.init( diff --git a/Sources/TuistLoader/Models+ManifestMappers/TestableTarget+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TestableTarget+ManifestMapper.swift index 4a0b9342852..c583f1365c0 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TestableTarget+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TestableTarget+ManifestMapper.swift @@ -1,26 +1,42 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph -extension TuistGraph.TestableTarget { - /// Maps a ProjectDescription.TestableTarget instance into a TuistGraph.TestableTarget instance. +extension XcodeGraph.TestableTarget { + /// Maps a ProjectDescription.TestableTarget instance into a XcodeGraph.TestableTarget instance. /// - Parameters: /// - manifest: Manifest representation of testable target model. /// - generatorPaths: Generator paths. static func from( manifest: ProjectDescription.TestableTarget, generatorPaths: GeneratorPaths - ) throws -> TuistGraph.TestableTarget { - TestableTarget( - target: TuistGraph.TargetReference( - projectPath: try generatorPaths.resolveSchemeActionProjectPath(manifest.target.projectPath), - name: manifest.target.targetName - ), + ) throws -> XcodeGraph.TestableTarget { + let target = XcodeGraph.TargetReference( + projectPath: try generatorPaths.resolveSchemeActionProjectPath(manifest.target.projectPath), + name: manifest.target.targetName + ) + + var simulatedLocation: XcodeGraph.SimulatedLocation? + + if let manifestLocation = manifest.simulatedLocation { + switch (manifestLocation.identifier, manifestLocation.gpxFile) { + case let (identifier?, .none): + simulatedLocation = .reference(identifier) + case let (.none, gpxFile?): + simulatedLocation = .gpxFile(try generatorPaths.resolveSchemeActionProjectPath(gpxFile)) + default: + break + } + } + + return TestableTarget( + target: target, skipped: manifest.isSkipped, parallelizable: manifest.isParallelizable, - randomExecutionOrdering: manifest.isRandomExecutionOrdering + randomExecutionOrdering: manifest.isRandomExecutionOrdering, + simulatedLocation: simulatedLocation ) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ManifestMapper.swift index 5e39903d69c..3daddc77de6 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ManifestMapper.swift @@ -1,8 +1,8 @@ import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.TestingOptions { - /// Maps a ProjectDescription.TestingOptions instance into a TuistGraph.TestingOptions instance. +extension XcodeGraph.TestingOptions { + /// Maps a ProjectDescription.TestingOptions instance into a XcodeGraph.TestingOptions instance. /// - Parameters: /// - manifest: Manifest representation of testing options. static func from( diff --git a/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ToManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ToManifestMapper.swift new file mode 100644 index 00000000000..c8c174ef590 --- /dev/null +++ b/Sources/TuistLoader/Models+ManifestMappers/TestingOptions+ToManifestMapper.swift @@ -0,0 +1,23 @@ +import ProjectDescription +import XcodeGraph + +extension ProjectDescription.TestingOptions { + /// Maps a XcodeGraph.TestingOptions instance into a ProjectDescription.TestingOptions instance. + /// - Parameters: + /// - manifest: Manifest representation of testing options. + static func from( + manifest: XcodeGraph.TestingOptions + ) -> Self { + var options: Self = [] + + if manifest.contains(.parallelizable) { + options.insert(.parallelizable) + } + + if manifest.contains(.randomExecutionOrdering) { + options.insert(.randomExecutionOrdering) + } + + return options + } +} diff --git a/Sources/TuistLoader/Models+ManifestMappers/Workspace+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Workspace+ManifestMapper.swift index f46db00337c..99767a9aacc 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Workspace+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Workspace+ManifestMapper.swift @@ -1,12 +1,12 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph -extension TuistGraph.Workspace { - /// Maps a ProjectDescription.Workspace instance into a TuistGraph.Workspace model. +extension XcodeGraph.Workspace { + /// Maps a ProjectDescription.Workspace instance into a XcodeGraph.Workspace model. /// - Parameters: /// - manifest: Manifest representation of workspace. /// - generatorPaths: Generator paths. @@ -15,14 +15,14 @@ extension TuistGraph.Workspace { path: AbsolutePath, generatorPaths: GeneratorPaths, manifestLoader: ManifestLoading - ) throws -> TuistGraph.Workspace { + ) throws -> XcodeGraph.Workspace { func globProjects(_ path: Path) throws -> [AbsolutePath] { let resolvedPath = try generatorPaths.resolve(path: path) let projects = FileHandler.shared.glob(AbsolutePath.root, glob: String(resolvedPath.pathString.dropFirst())) - .lazy .filter(FileHandler.shared.isFolder) + .filter { $0.basename != Constants.tuistDirectoryName && !$0.pathString.contains(".build/checkouts") } .filter { - manifestLoader.manifests(at: $0).contains(.project) + manifestLoader.manifests(at: $0).contains(where: { $0 == .package || $0 == .project }) } if projects.isEmpty { @@ -36,17 +36,17 @@ extension TuistGraph.Workspace { } let additionalFiles = try manifest.additionalFiles.flatMap { - try TuistGraph.FileElement.from(manifest: $0, generatorPaths: generatorPaths) + try XcodeGraph.FileElement.from(manifest: $0, generatorPaths: generatorPaths) } - let schemes = try manifest.schemes.map { try TuistGraph.Scheme.from(manifest: $0, generatorPaths: generatorPaths) } + let schemes = try manifest.schemes.map { try XcodeGraph.Scheme.from(manifest: $0, generatorPaths: generatorPaths) } let generationOptions: GenerationOptions = try .from(manifest: manifest.generationOptions, generatorPaths: generatorPaths) let ideTemplateMacros = try manifest.fileHeaderTemplate .map { try IDETemplateMacros.from(manifest: $0, generatorPaths: generatorPaths) } - return TuistGraph.Workspace( + return XcodeGraph.Workspace( path: path, xcWorkspacePath: path.appending(component: "\(manifest.name).xcworkspace"), name: manifest.name, diff --git a/Sources/TuistLoader/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestMapper.swift index 43959453719..8698b2a1608 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation import ProjectDescription -import TuistGraph +import XcodeGraph -extension TuistGraph.Workspace.GenerationOptions { - /// Maps ProjectDescription.Workspace.GenerationOptions instance into a TuistGraph.Workspace.GenerationOptions model. +extension XcodeGraph.Workspace.GenerationOptions { + /// Maps ProjectDescription.Workspace.GenerationOptions instance into a XcodeGraph.Workspace.GenerationOptions model. /// - Parameters: /// - manifest: Manifest representation of a generation option. /// - generatorPaths: Generator paths. @@ -23,7 +23,7 @@ extension TuistGraph.Workspace.GenerationOptions { } } -extension TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes { +extension XcodeGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes { static func from( manifest: ProjectDescription.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes, generatorPaths: GeneratorPaths @@ -43,7 +43,7 @@ extension TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes { } } -extension TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode { +extension XcodeGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode { static func from( manifest: ProjectDescription.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode, generatorPaths: GeneratorPaths @@ -52,7 +52,7 @@ extension TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.C case .all: return .all case .relevant: return .relevant case let .targets(targets): - let targets: [TuistGraph.TargetReference] = try targets.map { + let targets: [XcodeGraph.TargetReference] = try targets.map { .init( projectPath: try generatorPaths.resolveSchemeActionProjectPath($0.projectPath), name: $0.targetName diff --git a/Sources/TuistLoader/Models/FileListGlob+Unfold.swift b/Sources/TuistLoader/Models/FileListGlob+Unfold.swift index 46663d31d96..9ce57d8cc86 100644 --- a/Sources/TuistLoader/Models/FileListGlob+Unfold.swift +++ b/Sources/TuistLoader/Models/FileListGlob+Unfold.swift @@ -1,8 +1,8 @@ import Foundation +import Path import ProjectDescription -import TSCBasic -import TuistGraph import TuistSupport +import XcodeGraph extension FileListGlob { func unfold( diff --git a/Sources/TuistLoader/Models/GeneratorPaths.swift b/Sources/TuistLoader/Models/GeneratorPaths.swift index 32eaca0b906..8608d79a1b8 100644 --- a/Sources/TuistLoader/Models/GeneratorPaths.swift +++ b/Sources/TuistLoader/Models/GeneratorPaths.swift @@ -1,6 +1,6 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore import TuistSupport diff --git a/Sources/TuistLoader/Models/Manifest.swift b/Sources/TuistLoader/Models/Manifest.swift index 23a999a12b1..4c12a57ec0f 100644 --- a/Sources/TuistLoader/Models/Manifest.swift +++ b/Sources/TuistLoader/Models/Manifest.swift @@ -1,14 +1,14 @@ import Foundation -import TSCBasic +import Path public enum Manifest: CaseIterable { case project case workspace case config case template - case dependencies case plugin case package + case packageSettings /// - Parameters: /// - path: Path to the folder that contains the manifest @@ -23,11 +23,9 @@ public enum Manifest: CaseIterable { return "Config.swift" case .template: return "\(path.basenameWithoutExt).swift" - case .dependencies: - return "Dependencies.swift" case .plugin: return "Plugin.swift" - case .package: + case .package, .packageSettings: return "Package.swift" } } diff --git a/Sources/TuistLoader/Models/PluginStencil.swift b/Sources/TuistLoader/Models/PluginStencil.swift index d2ebef81114..0c42cede60b 100644 --- a/Sources/TuistLoader/Models/PluginStencil.swift +++ b/Sources/TuistLoader/Models/PluginStencil.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// Stencil plugin model public struct PluginStencil: Equatable { diff --git a/Sources/TuistLoader/Models/ProjectDescriptionHelpersModule.swift b/Sources/TuistLoader/Models/ProjectDescriptionHelpersModule.swift index d0b986417ce..43f859ed6f2 100644 --- a/Sources/TuistLoader/Models/ProjectDescriptionHelpersModule.swift +++ b/Sources/TuistLoader/Models/ProjectDescriptionHelpersModule.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// Defines a module for a project description helper. /// Project description helpers are modules which can be imported wherever "ProjectDescription" can be imported. diff --git a/Sources/TuistDependencies/SwiftPackageManager/Models/SwiftPackageManagerWorkspaceState.swift b/Sources/TuistLoader/Models/SwiftPackageManagerWorkspaceState.swift similarity index 100% rename from Sources/TuistDependencies/SwiftPackageManager/Models/SwiftPackageManagerWorkspaceState.swift rename to Sources/TuistLoader/Models/SwiftPackageManagerWorkspaceState.swift diff --git a/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilder.swift b/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilder.swift index 446748d57ab..4ded4ac070f 100644 --- a/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilder.swift +++ b/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilder.swift @@ -1,6 +1,7 @@ +import FileSystem import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistSupport /// This protocol defines the interface to compile a temporary module with the @@ -19,8 +20,8 @@ public protocol ProjectDescriptionHelpersBuilding: AnyObject { func build( at path: AbsolutePath, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths, - projectDescriptionHelperPlugins: [ProjectDescriptionHelpersPlugin] - ) throws -> [ProjectDescriptionHelpersModule] + projectDescriptionHelperPlugins: [TuistCore.ProjectDescriptionHelpersPlugin] + ) async throws -> [ProjectDescriptionHelpersModule] /// Builds all the plugin helpers module and returns the location to the built modules. /// @@ -34,13 +35,13 @@ public protocol ProjectDescriptionHelpersBuilding: AnyObject { at path: AbsolutePath, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths, projectDescriptionHelperPlugins: [ProjectDescriptionHelpersPlugin] - ) throws -> [ProjectDescriptionHelpersModule] + ) async throws -> [ProjectDescriptionHelpersModule] } public final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBuilding { /// A dictionary that keeps in memory the helpers (value of the dictionary) that have been built /// in the current process for helpers directories (key of the dictionary) - private var builtHelpers: [AbsolutePath: ProjectDescriptionHelpersModule] = [:] + private var builtHelpers: ThreadSafe<[AbsolutePath: ProjectDescriptionHelpersModule]> = ThreadSafe([:]) /// Path to the cache directory. private let cacheDirectory: AbsolutePath @@ -53,6 +54,7 @@ public final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBu /// Clock for measuring build duration. private let clock: Clock + private let fileSystem: FileSystem /// The name of the default project description helpers module static let defaultHelpersName = "ProjectDescriptionHelpers" @@ -67,26 +69,28 @@ public final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBu projectDescriptionHelpersHasher: ProjectDescriptionHelpersHashing = ProjectDescriptionHelpersHasher(), cacheDirectory: AbsolutePath, helpersDirectoryLocator: HelpersDirectoryLocating = HelpersDirectoryLocator(), - clock: Clock = WallClock() + clock: Clock = WallClock(), + fileSystem: FileSystem = FileSystem() ) { self.projectDescriptionHelpersHasher = projectDescriptionHelpersHasher self.cacheDirectory = cacheDirectory self.helpersDirectoryLocator = helpersDirectoryLocator self.clock = clock + self.fileSystem = fileSystem } public func build( at path: AbsolutePath, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths, projectDescriptionHelperPlugins: [ProjectDescriptionHelpersPlugin] - ) throws -> [ProjectDescriptionHelpersModule] { - let pluginHelpers = try buildPlugins( + ) async throws -> [ProjectDescriptionHelpersModule] { + let pluginHelpers = try await buildPlugins( at: path, projectDescriptionSearchPaths: projectDescriptionSearchPaths, projectDescriptionHelperPlugins: projectDescriptionHelperPlugins ) - let defaultHelpers = try buildDefaultHelpers( + let defaultHelpers = try await buildDefaultHelpers( in: path, projectDescriptionSearchPaths: projectDescriptionSearchPaths, customProjectDescriptionHelperModules: pluginHelpers @@ -101,21 +105,33 @@ public final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBu at _: AbsolutePath, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths, projectDescriptionHelperPlugins: [ProjectDescriptionHelpersPlugin] - ) throws -> [ProjectDescriptionHelpersModule] { - let pluginHelpers = try projectDescriptionHelperPlugins.map { - try buildHelpers(name: $0.name, in: $0.path, projectDescriptionSearchPaths: projectDescriptionSearchPaths) + ) async throws -> [ProjectDescriptionHelpersModule] { + return try await projectDescriptionHelperPlugins.concurrentMap { plugin in + try await self.buildHelpers( + name: plugin.name, + in: plugin.path, + projectDescriptionSearchPaths: projectDescriptionSearchPaths + ) } - - return pluginHelpers } private func buildDefaultHelpers( in path: AbsolutePath, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths, customProjectDescriptionHelperModules: [ProjectDescriptionHelpersModule] - ) throws -> ProjectDescriptionHelpersModule? { + ) async throws -> ProjectDescriptionHelpersModule? { guard let tuistHelpersDirectory = helpersDirectoryLocator.locate(at: path) else { return nil } - return try buildHelpers( + #if DEBUG + if let sourceRoot = ProcessInfo.processInfo.environment["TUIST_CONFIG_SRCROOT"], + tuistHelpersDirectory.isDescendant( + // swiftlint:disable:next force_try + of: try! AbsolutePath(validating: sourceRoot).appending(component: Constants.tuistDirectoryName) + ) + { + return nil + } + #endif + return try await buildHelpers( name: Self.defaultHelpersName, in: tuistHelpersDirectory, projectDescriptionSearchPaths: projectDescriptionSearchPaths, @@ -142,8 +158,8 @@ public final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBu in path: AbsolutePath, projectDescriptionSearchPaths: ProjectDescriptionSearchPaths, customProjectDescriptionHelperModules: [ProjectDescriptionHelpersModule] = [] - ) throws -> ProjectDescriptionHelpersModule { - if let cachedModule = builtHelpers[path] { return cachedModule } + ) async throws -> ProjectDescriptionHelpersModule { + if let cachedModule = builtHelpers.withValue({ $0[path] }) { return cachedModule } let hash = try projectDescriptionHelpersHasher.hash(helpersDirectory: path) let prefixHash = projectDescriptionHelpersHasher.prefixHash(helpersDirectory: path) @@ -154,18 +170,12 @@ public final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBu let modulePath = helpersModuleCachePath.appending(component: dylibName) let projectDescriptionHelpersModule = ProjectDescriptionHelpersModule(name: name, path: modulePath) - builtHelpers[path] = projectDescriptionHelpersModule + builtHelpers.mutate { $0[path] = projectDescriptionHelpersModule } if FileHandler.shared.exists(helpersModuleCachePath) { return projectDescriptionHelpersModule } - // If the same helpers directory has been previously compiled - // we delete it before compiling the new changes. - if FileHandler.shared.exists(helpersCachePath) { - try FileHandler.shared.delete(helpersCachePath) - } - try FileHandler.shared.createFolder(helpersModuleCachePath) let command = createCommand( diff --git a/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderFactory.swift b/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderFactory.swift index ac6817c0ee6..e748f0c0ea3 100644 --- a/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderFactory.swift +++ b/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderFactory.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph // swiftlint:disable:next type_name public protocol ProjectDescriptionHelpersBuilderFactoring { diff --git a/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasher.swift b/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasher.swift index 4d86228ee67..94c4571a2ad 100644 --- a/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasher.swift +++ b/Sources/TuistLoader/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasher.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport public protocol ProjectDescriptionHelpersHashing: AnyObject { @@ -16,10 +16,18 @@ public protocol ProjectDescriptionHelpersHashing: AnyObject { public final class ProjectDescriptionHelpersHasher: ProjectDescriptionHelpersHashing { /// Tuist version. - let tuistVersion: String - - public init(tuistVersion: String = Constants.version) { + private let tuistVersion: String + private let machineEnvironment: MachineEnvironmentRetrieving + private let swiftVersionProvider: SwiftVersionProviding + + public init( + tuistVersion: String = Constants.version, + machineEnvironment: MachineEnvironmentRetrieving = MachineEnvironment.shared, + swiftVersionProvider: SwiftVersionProviding = SwiftVersionProvider.shared + ) { self.tuistVersion = tuistVersion + self.machineEnvironment = machineEnvironment + self.swiftVersionProvider = swiftVersionProvider } // MARK: - ProjectDescriptionHelpersHashing @@ -31,14 +39,15 @@ public final class ProjectDescriptionHelpersHasher: ProjectDescriptionHelpersHas .compactMap { $0.sha256() } .compactMap { $0.compactMap { byte in String(format: "%02x", byte) }.joined() } let tuistEnvVariables = Environment.shared.manifestLoadingVariables.map { "\($0.key)=\($0.value)" }.sorted() - let swiftVersion = try System.shared.swiftVersion() + let swiftVersion = try swiftVersionProvider.swiftVersion() + let macosVersion = machineEnvironment.macOSVersion #if DEBUG let debug = true #else let debug = false #endif - let identifiers = [swiftVersion, tuistVersion] + fileHashes + tuistEnvVariables + ["\(debug)"] + let identifiers = [macosVersion, swiftVersion, tuistVersion] + fileHashes + tuistEnvVariables + ["\(debug)"] return identifiers.joined(separator: "-").md5 } diff --git a/Sources/TuistLoader/Protocols/GraphInitiatable.swift b/Sources/TuistLoader/Protocols/GraphInitiatable.swift index 786748d6938..6d6e9bcba1e 100644 --- a/Sources/TuistLoader/Protocols/GraphInitiatable.swift +++ b/Sources/TuistLoader/Protocols/GraphInitiatable.swift @@ -1,4 +1,5 @@ import Foundation +import Path import TSCBasic import TuistSupport @@ -15,5 +16,5 @@ protocol GraphInitiatable { /// - projectPath: Absolute path to the folder that contains the manifest. /// This is useful to obtain absolute paths from the relative paths provided in the manifest by the user. /// - Throws: A decoding error if an expected property is missing or has an invalid value. - init(dictionary: JSON, projectPath: AbsolutePath) throws + init(dictionary: JSON, projectPath: Path.AbsolutePath) throws } diff --git a/Sources/TuistLoader/SwiftPackageManager/PackageInfo+graphPlatform.swift b/Sources/TuistLoader/SwiftPackageManager/PackageInfo+graphPlatform.swift new file mode 100644 index 00000000000..58f98f1010c --- /dev/null +++ b/Sources/TuistLoader/SwiftPackageManager/PackageInfo+graphPlatform.swift @@ -0,0 +1,25 @@ +import Foundation +import ProjectDescription +import TuistSupport +import XcodeGraph + +extension PackageInfo.Platform { + func destinations() throws -> ProjectDescription.Destinations { + switch platformName.lowercased() { + case "ios": + return [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + case "maccatalyst": + return [.macCatalyst] + case "macos": + return [.mac] + case "tvos": + return [.appleTv] + case "watchos": + return [.appleWatch] + case "visionos": + return [.appleVision] + default: + throw PackageInfoMapperError.unknownPlatform(platformName) + } + } +} diff --git a/Sources/TuistLoader/SwiftPackageManager/PackageInfoMapper.swift b/Sources/TuistLoader/SwiftPackageManager/PackageInfoMapper.swift new file mode 100644 index 00000000000..15af3e61d69 --- /dev/null +++ b/Sources/TuistLoader/SwiftPackageManager/PackageInfoMapper.swift @@ -0,0 +1,1398 @@ +import Foundation +import Mockable +import Path +import ProjectDescription +import TSCUtility +import TuistCore +import TuistSupport +import XcodeGraph + +// MARK: - PackageInfo Mapper Errors + +enum PackageInfoMapperError: FatalError, Equatable { + /// Thrown when the default path folder is not present. + case defaultPathNotFound(AbsolutePath, String, [String]) + + /// Thrown when the parsing of minimum deployment target failed. + case minDeploymentTargetParsingFailed(ProjectDescription.Platform) + + /// Thrown when no supported platforms are found for a package. + case noSupportedPlatforms( + name: String, + configured: Set, + package: Set + ) + + /// Thrown when `PackageInfo.Target.Dependency.byName` dependency cannot be resolved. + case unknownByNameDependency(String) + + /// Thrown when `PackageInfo.Platform` name cannot be mapped to a `DeploymentTarget`. + case unknownPlatform(String) + + /// Thrown when `PackageInfo.Target.Dependency.product` dependency cannot be resolved. + case unknownProductDependency(String, String) + + /// Thrown when a target defined in a product is not present in the package + case unknownProductTarget(package: String, product: String, target: String) + + /// Thrown when unsupported `PackageInfo.Target.TargetBuildSettingDescription` `Tool`/`SettingName` pair is found. + case unsupportedSetting( + PackageInfo.Target.TargetBuildSettingDescription.Tool, + PackageInfo.Target.TargetBuildSettingDescription.SettingName + ) + + /// Thrown when a binary target defined in a package doesn't have a corresponding artifact + case missingBinaryArtifact(package: String, target: String) + + case modulemapMissing(moduleMapPath: String, package: String, target: String) + + /// Error type. + var type: ErrorType { + switch self { + case .noSupportedPlatforms, .unknownByNameDependency, .unknownPlatform, .unknownProductDependency, .unknownProductTarget, + .modulemapMissing: + return .abort + case .minDeploymentTargetParsingFailed, .defaultPathNotFound, .unsupportedSetting, .missingBinaryArtifact: + return .bug + } + } + + /// Error description. + var description: String { + switch self { + case let .defaultPathNotFound(packageFolder, targetName, predefinedPaths): + return """ + Default source path not found for target \(targetName) in package at \(packageFolder.pathString). \ + Source path must be one of \(predefinedPaths.map { "\($0)/\(targetName)" }) + """ + case let .minDeploymentTargetParsingFailed(platform): + return "The minimum deployment target for \(platform) platform cannot be parsed." + case let .noSupportedPlatforms(name, configured, package): + return "No supported platform found for the \(name) dependency. Configured: \(configured), package: \(package)." + case let .unknownByNameDependency(name): + return "The package associated to the \(name) dependency cannot be found." + case let .unknownPlatform(platform): + return "The \(platform) platform is not supported." + case let .unknownProductDependency(name, package): + return "The product \(name) of package \(package) cannot be found." + case let .unknownProductTarget(package, product, target): + return "The target \(target) of product \(product) cannot be found in package \(package)." + case let .unsupportedSetting(tool, setting): + return "The \(tool) and \(setting) pair is not a supported setting." + case let .missingBinaryArtifact(package, target): + return "The artifact for binary target \(target) of package \(package) cannot be found." + case let .modulemapMissing(moduleMapPath, package, target): + return "Target \(target) of package \(package) is a system library. Module map is missing at \(moduleMapPath)." + } + } +} + +public enum PackageType { + case local + case external(artifactPaths: [String: AbsolutePath]) +} + +// MARK: - PackageInfo Mapper + +/// Protocol that allows to map a `PackageInfo` to a `ProjectDescription.Project`. +@Mockable +public protocol PackageInfoMapping { + /// Resolves external SwiftPackageManager dependencies. + /// - Returns: Mapped project + func resolveExternalDependencies( + packageInfos: [String: PackageInfo], + packageToFolder: [String: AbsolutePath], + packageToTargetsToArtifactPaths: [String: [String: AbsolutePath]] + ) throws -> [String: [ProjectDescription.TargetDependency]] + + /// Maps a `PackageInfo` to a `ProjectDescription.Project`. + /// - Returns: Mapped project + func map( + packageInfo: PackageInfo, + path: AbsolutePath, + packageType: PackageType, + packageSettings: TuistCore.PackageSettings, + packageToProject: [String: AbsolutePath] + ) throws -> ProjectDescription.Project? +} + +// swiftlint:disable:next type_body_length +public final class PackageInfoMapper: PackageInfoMapping { + // Predefined source directories, in order of preference. + // https://github.com/apple/swift-package-manager/blob/751f0b2a00276be2c21c074f4b21d952eaabb93b/Sources/PackageLoading/PackageBuilder.swift#L488 + fileprivate static let predefinedSourceDirectories = ["Sources", "Source", "src", "srcs"] + fileprivate static let predefinedTestDirectories = ["Tests", "Sources", "Source", "src", "srcs"] + private let moduleMapGenerator: SwiftPackageManagerModuleMapGenerating + + public init( + moduleMapGenerator: SwiftPackageManagerModuleMapGenerating = SwiftPackageManagerModuleMapGenerator() + ) { + self.moduleMapGenerator = moduleMapGenerator + } + + /// Resolves all SwiftPackageManager dependencies. + /// - Parameters: + /// - packageInfos: All available `PackageInfo`s + /// - packageToFolder: Mapping from a package name to its local folder + /// - packageToTargetsToArtifactPaths: Mapping from a package name its targets' names to artifacts' paths + /// - Returns: Mapped project + public func resolveExternalDependencies( + packageInfos: [String: PackageInfo], + packageToFolder: [String: AbsolutePath], + packageToTargetsToArtifactPaths: [String: [String: AbsolutePath]] + ) throws -> [String: [ProjectDescription.TargetDependency]] { + let targetDependencyToFramework: [String: Path] = try packageInfos.reduce(into: [:]) { result, packageInfo in + try packageInfo.value.targets.forEach { target in + guard target.type == .binary else { return } + if let path = target.path { + // local binary + result[target.name] = .path( + packageToFolder[packageInfo.key]!.appending(try RelativePath(validating: path)) + .pathString + ) + } + // remote binaries are checked out by SPM in artifacts//.xcframework + // or in artifacts//.xcframework when using SPM 5.6 and later + else if let artifactPath = packageToTargetsToArtifactPaths[packageInfo.key]?[target.name] { + result[target.name] = .path(artifactPath.pathString) + } + // If the binary path is not present in the `.build/workspace-state.json`, we try to use a default path. + // If the target is not used by a downstream target, the generation will ignore a missing binary artifact. + // Otherwise, users will get an error that the xcframework was not found. + else { + result[target.name] = .path( + packageToFolder[packageInfo.key]!.appending( + components: target.name, + "\(target.name).xcframework" + ) + .pathString + ) + } + } + } + + return try packageInfos + .reduce(into: [:]) { result, packageInfo in + for product in packageInfo.value.products { + result[product.name] = try product.targets.flatMap { target in + try ResolvedDependency.fromTarget( + name: target, + targetDependencyToFramework: targetDependencyToFramework, + condition: nil + ) + .map { + switch $0 { + case let .xcframework(path, condition): + return .xcframework(path: path, condition: condition) + case let .target(name, condition): + return .project( + target: name, + path: .path(packageToFolder[packageInfo.key]!.pathString), + condition: condition + ) + case .externalTarget: + throw PackageInfoMapperError.unknownProductTarget( + package: packageInfo.key, + product: product.name, + target: target + ) + } + } + } + } + } + } + + /** + There are certain Swift Package targets that need to run on macOS. Examples of these are Swift Macros. + It's important that we take that into account when generating and serializing the graph, which contains information + about targets' macros, into disk. It's important to note that these targets require its dependencies, direct or transitive, + to compile for macOS too. This function traverses the graph and returns all the targets that need to compile for macOS + in a set. The set is then used in the serialization logic when: + + - Unfolding the target into platform-specific targets. + - Declaring dependencies. + + All the complexity associated to this might go away once we have support for multi-platform targets. + */ + private func macOSTargets( + _ resolvedDependencies: [String: [ResolvedDependency]], + packageInfos: [String: PackageInfo] + ) -> Set { + let targetTypes = packageInfos.reduce(into: [String: PackageInfo.Target.TargetType]()) { partialResult, item in + for target in item.value.targets { + partialResult[target.name] = target.type + } + } + + var targets = Set() + + func visit(target: String, parentMacOS: Bool) { + let isMacOS = targetTypes[target] == .macro || parentMacOS + if isMacOS { + targets.insert(target) + } + let dependencies = resolvedDependencies[target] ?? [] + for dependency in dependencies { + switch dependency { + case let .target(name, _): + visit(target: name, parentMacOS: isMacOS) + case let .externalTarget(_, name, _): + visit(target: name, parentMacOS: isMacOS) + case .xcframework: + break + } + } + } + + for target in resolvedDependencies.keys.sorted() { + visit(target: target, parentMacOS: false) + } + + return targets + } + + // swiftlint:disable:next function_body_length + public func map( + packageInfo: PackageInfo, + path: AbsolutePath, + packageType: PackageType, + packageSettings: TuistCore.PackageSettings, + packageToProject _: [String: AbsolutePath] + ) throws -> ProjectDescription.Project? { + // Hardcoded mapping for some well known libraries, until the logic can handle those properly + let productTypes = packageSettings.productTypes.merging( + // Force dynamic frameworks + Dictionary( + uniqueKeysWithValues: [ + "Checksum", // https://github.com/rnine/Checksum + "RxSwift", // https://github.com/ReactiveX/RxSwift + ].map { + ($0, .framework) + } + ), + uniquingKeysWith: { userDefined, _ in userDefined } + ) + + let targetSettings = packageSettings.targetSettings.merging( + // Force enable testing search paths + Dictionary( + uniqueKeysWithValues: [ + "Mocker", // https://github.com/WeTransfer/Mocker + "Nimble", // https://github.com/Quick/Nimble + "NimbleObjectiveC", // https://github.com/Quick/Nimble + "Quick", // https://github.com/Quick/Quick + "QuickObjCRuntime", // https://github.com/Quick/Quick + "RxTest", // https://github.com/ReactiveX/RxSwift + "RxTest-Dynamic", // https://github.com/ReactiveX/RxSwift + "SnapshotTesting", // https://github.com/pointfreeco/swift-snapshot-testing + "SwiftyMocky", // https://github.com/MakeAWishFoundation/SwiftyMocky + "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 + "MockableTest", // https://github.com/Kolos65/Mockable.git + "Testing", // https://github.com/apple/swift-testing + "Cuckoo", // https://github.com/Brightify/Cuckoo + ].map { + ($0, ["ENABLE_TESTING_SEARCH_PATHS": "YES"]) + } + ), + uniquingKeysWith: { userDefined, defaultDictionary in + userDefined.merging(defaultDictionary, uniquingKeysWith: { userDefined, _ in userDefined }) + } + ) + + let baseSettings = packageSettings.baseSettings.with( + base: packageSettings.baseSettings.base.combine( + with: [ + "OTHER_SWIFT_FLAGS": ["$(inherited)", "-package-name", packageInfo.name.quotedIfContainsSpaces], + ] + ) + ) + + var targetToProducts: [String: Set] = [:] + for product in packageInfo.products { + var targetsToProcess = Set(product.targets) + while !targetsToProcess.isEmpty { + let target = targetsToProcess.removeFirst() + let alreadyProcessed = targetToProducts[target]?.contains(product) ?? false + guard !alreadyProcessed else { + continue + } + targetToProducts[target, default: []].insert(product) + let dependencies = packageInfo.targets.first(where: { $0.name == target })!.dependencies + for dependency in dependencies { + switch dependency { + case let .target(name, _): + targetsToProcess.insert(name) + case let .byName(name, _) where packageInfo.targets.contains(where: { $0.name == name }): + targetsToProcess.insert(name) + case .byName, .product: + continue + } + } + } + } + + let targets: [ProjectDescription.Target] = try packageInfo.targets + .compactMap { target -> ProjectDescription.Target? in + return try map( + target: target, + targetToProducts: targetToProducts, + packageInfo: packageInfo, + packageType: packageType, + path: path, + packageFolder: path, + productTypes: productTypes, + productDestinations: packageSettings.productDestinations, + baseSettings: baseSettings, + targetSettings: targetSettings + ) + } + + guard !targets.isEmpty else { + return nil + } + + let options: ProjectDescription.Project.Options + if let projectOptions = packageSettings.projectOptions[packageInfo.name] { + options = .from(manifest: projectOptions) + } else { + let automaticSchemesOptions: ProjectDescription.Project.Options.AutomaticSchemesOptions + switch packageType { + case .external: + automaticSchemesOptions = .disabled + case .local: + automaticSchemesOptions = .enabled() + } + options = .options( + automaticSchemesOptions: automaticSchemesOptions, + disableSynthesizedResourceAccessors: true + ) + } + + return ProjectDescription.Project( + name: packageInfo.name, + options: options, + settings: packageInfo.projectSettings( + swiftToolsVersion: .init(packageSettings.swiftToolsVersion.description), + buildConfigs: baseSettings.configurations.map { key, _ in key } + ), + targets: targets, + resourceSynthesizers: .default + ) + } + + fileprivate class func sanitize(targetName: String) -> String { + targetName.replacingOccurrences(of: ".", with: "_") + .replacingOccurrences(of: "/", with: "_") + } + + // swiftlint:disable:next function_body_length + private func map( + target: PackageInfo.Target, + targetToProducts: [String: Set], + packageInfo: PackageInfo, + packageType: PackageType, + path: AbsolutePath, + packageFolder: AbsolutePath, + productTypes: [String: XcodeGraph.Product], + productDestinations: [String: XcodeGraph.Destinations], + baseSettings: XcodeGraph.Settings, + targetSettings: [String: XcodeGraph.SettingsDictionary] + ) throws -> ProjectDescription.Target? { + // Ignores or passes a target based on the `type` and the `packageType`. + // After that, it assumes that no target is ignored. + switch target.type { + case .regular, .system, .macro: + break + case .test, .executable: + switch packageType { + case .external: + logger.debug("Target \(target.name) of type \(target.type) ignored") + return nil + case .local: + break + } + default: + logger.debug("Target \(target.name) of type \(target.type) ignored") + return nil + } + + let products = targetToProducts[target.name] ?? Set() + + guard let product = ProjectDescription.Product.from( + name: target.name, + type: target.type, + products: products, + productTypes: productTypes + ) + else { + logger.debug("Target \(target.name) ignored by product type") + return nil + } + + let targetPath = try target.basePath(packageFolder: packageFolder) + + let moduleMap: ModuleMap? + switch target.type { + case .system: + /// System library targets assume the module map is located at the source directory root + /// https://github.com/apple/swift-package-manager/blob/main/Sources/PackageLoading/ModuleMapGenerator.swift + let packagePath = try target.basePath(packageFolder: path) + let moduleMapPath = packagePath.appending(component: ModuleMap.filename) + + guard FileHandler.shared.exists(moduleMapPath), !FileHandler.shared.isFolder(moduleMapPath) else { + throw PackageInfoMapperError.modulemapMissing( + moduleMapPath: moduleMapPath.pathString, + package: packageInfo.name, + target: target.name + ) + } + + moduleMap = ModuleMap.custom(moduleMapPath, umbrellaHeaderPath: nil) + case .regular: + moduleMap = try moduleMapGenerator.generate( + packageDirectory: path, + moduleName: target.name, + publicHeadersPath: target.publicHeadersPath(packageFolder: path) + ) + default: + moduleMap = nil + } + + var destinations: ProjectDescription.Destinations + switch target.type { + case .macro, .executable: + destinations = Set([.mac]) + case .test: + var testDestinations = Set(XcodeGraph.Destination.allCases) + for dependencyTarget in target.dependencies { + if let dependencyProducts = targetToProducts[dependencyTarget.name] { + let dependencyDestinations = unionDestinationsOfProducts(dependencyProducts, in: productDestinations) + testDestinations.formIntersection(dependencyDestinations) + } + } + destinations = ProjectDescription.Destinations.from(destinations: testDestinations) + default: + switch packageType { + case .local: + let productDestinations = unionDestinationsOfProducts(products, in: productDestinations) + destinations = ProjectDescription.Destinations.from(destinations: productDestinations) + case .external: + destinations = Set(Destination.allCases) + } + } + + let version = try Version(versionString: try SwiftVersionProvider.shared.swiftVersion(), usesLenientParsing: true) + let minDeploymentTargets = ProjectDescription.DeploymentTargets.oldestVersions(for: version) + + let deploymentTargets = try ProjectDescription.DeploymentTargets.from( + minDeploymentTargets: minDeploymentTargets, + package: packageInfo.platforms, + destinations: destinations, + packageName: packageInfo.name + ) + + var headers: ProjectDescription.Headers? + var sources: SourceFilesList? + var resources: ProjectDescription.ResourceFileElements? + + if target.type.supportsPublicHeaderPath { + headers = try Headers.from(moduleMap: moduleMap) + } + + if target.type.supportsSources { + sources = try SourceFilesList.from(sources: target.sources, path: targetPath, excluding: target.exclude) + } + + if target.type.supportsResources { + resources = try ResourceFileElements.from( + sources: target.sources, + resources: target.resources, + path: targetPath, + excluding: target.exclude + ) + } + + var dependencies: [ProjectDescription.TargetDependency] = [] + + if target.type.supportsDependencies { + let linkerDependencies: [ProjectDescription.TargetDependency] = target.settings.compactMap { setting in + do { + let condition = try ProjectDescription.PlatformCondition.from(setting.condition) + + switch (setting.tool, setting.name) { + case (.linker, .linkedFramework): + return .sdk(name: setting.value[0], type: .framework, status: .required, condition: condition) + case (.linker, .linkedLibrary): + return .sdk(name: setting.value[0], type: .library, status: .required, condition: condition) + case (.c, _), (.cxx, _), (_, .enableUpcomingFeature), (.swift, _), (.linker, .headerSearchPath), ( + .linker, + .define + ), + (.linker, .unsafeFlags), (_, .enableExperimentalFeature): + return nil + } + } catch { + return nil + } + } + + dependencies = try linkerDependencies + target.dependencies.compactMap { + switch $0 { + case let .byName(name: name, condition: condition), let .product( + name: name, + package: _, + moduleAliases: _, + condition: condition + ), + let .target( + name: name, + condition: condition + ): + let platformCondition: ProjectDescription.PlatformCondition? + do { + platformCondition = try ProjectDescription.PlatformCondition.from(condition) + } catch { + return nil + } + if let target = packageInfo.targets.first(where: { $0.name == name }) { + if target.type == .binary, case let .external(artifactPaths: artifactPaths) = packageType { + guard let artifactPath = artifactPaths[target.name] else { + throw PackageInfoMapperError.missingBinaryArtifact(package: packageInfo.name, target: target.name) + } + return .xcframework(path: .path(artifactPath.pathString), status: .required, condition: nil) + } + return .target(name: name, condition: platformCondition) + } else { + return .external(name: name, condition: platformCondition) + } + } + } + } + + let settings = try Settings.from( + target: target, + packageFolder: packageFolder, + packageName: packageInfo.name, + settings: target.settings, + moduleMap: moduleMap, + baseSettings: baseSettings, + targetSettings: targetSettings + ) + + return .target( + name: PackageInfoMapper.sanitize(targetName: target.name), + destinations: destinations, + product: product, + productName: PackageInfoMapper + .sanitize(targetName: target.name) + .replacingOccurrences(of: "-", with: "_"), + bundleId: target.name + .replacingOccurrences(of: "_", with: ".").replacingOccurrences(of: "/", with: "."), + deploymentTargets: deploymentTargets, + infoPlist: .default, + sources: sources, + resources: resources, + headers: headers, + dependencies: dependencies, + settings: settings + ) + } + + /// Returns a union of products' destinations. + private func unionDestinationsOfProducts( + _ products: Set, + in productToDestinations: [String: XcodeGraph.Destinations] + ) -> XcodeGraph.Destinations { + Set( + products.flatMap { product in + if product.type == .executable { + return Set([XcodeGraph.Destination.mac]) + } + return productToDestinations[product.name] ?? Set(Destination.allCases) + } + ) + } +} + +extension ProjectDescription.DeploymentTargets { + /// A dictionary that contains the oldest supported version of each platform + public static func oldestVersions(for swiftVersion: TSCUtility.Version) -> ProjectDescription.DeploymentTargets { + if swiftVersion < Version(5, 7, 0) { + return .multiplatform( + iOS: "9.0", + macOS: "10.10", + watchOS: "2.0", + tvOS: "9.0", + visionOS: "1.0" + ) + } else if swiftVersion < Version(5, 9, 0) { + return .multiplatform( + iOS: "11.0", + macOS: "10.13", + watchOS: "4.0", + tvOS: "11.0", + visionOS: "1.0" + ) + } else { + return .multiplatform( + iOS: "12.0", + macOS: "10.13", + watchOS: "4.0", + tvOS: "12.0", + visionOS: "1.0" + ) + } + } + + fileprivate static func from( + minDeploymentTargets: ProjectDescription.DeploymentTargets, + package: [PackageInfo.Platform], + destinations: ProjectDescription.Destinations, + packageName _: String + ) throws -> Self { + let versionPairs: [(ProjectDescription.Platform, String)] = package.compactMap { packagePlatform in + guard let tuistPlatform = ProjectDescription.Platform(rawValue: packagePlatform.tuistPlatformName) else { return nil } + return (tuistPlatform, packagePlatform.version) + } + // maccatalyst and iOS will be the same, this chooses the first one defined, hopefully they dont disagree + let platformInfos = Dictionary(versionPairs) { first, _ in first } + let destinationPlatforms = destinations.platforms + + func versionFor(platform: ProjectDescription.Platform) throws -> String? { + guard destinationPlatforms.contains(platform) else { return nil } + return try max(minDeploymentTargets[platform], platformInfos[platform]) + } + + return .multiplatform( + iOS: try versionFor(platform: .iOS), + macOS: try versionFor(platform: .macOS), + watchOS: try versionFor(platform: .watchOS), + tvOS: try versionFor(platform: .tvOS), + visionOS: try versionFor(platform: .visionOS) + ) + } + + fileprivate static func max(_ lVersionString: String?, _ rVersionString: String?) throws -> String? { + guard let rVersionString else { return lVersionString } + guard let lVersionString else { return nil } + let lVersion = try Version(versionString: lVersionString, usesLenientParsing: true) + let rVersion = try Version(versionString: rVersionString, usesLenientParsing: true) + return lVersion > rVersion ? lVersionString : rVersionString + } +} + +extension ProjectDescription.Product { + fileprivate static func from( + name: String, + type: PackageInfo.Target.TargetType, + products: Set, + productTypes: [String: XcodeGraph.Product] + ) -> Self? { + // Swift Macros are command line tools that run in the host (macOS) at compilation time. + switch type { + case .macro: + return .macro + case .executable: + return .commandLineTool + case .test: + return .unitTests + default: + break + } + + if let productType = productTypes[name] { + return ProjectDescription.Product.from(product: productType) + } + + var hasAutomaticProduct = false + let product: ProjectDescription.Product? = products.reduce(nil) { result, product in + switch product.type { + case let .library(type): + switch type { + case .automatic: + hasAutomaticProduct = true + return result + case .static: + return .staticFramework + case .dynamic: + if result == .staticFramework { + // If any of the products is static, the target must be static + return result + } else { + return .framework + } + } + case .executable, .plugin, .test: + return result + } + } + + if hasAutomaticProduct { + // contains automatic product, default to static framework + return .staticFramework + } else if product != nil { + // return found product if there is no automatic products + return product + } else { + // only executable, plugin, or test products, ignore it + return nil + } + } +} + +extension SourceFilesList { + fileprivate static func from(sources: [String]?, path: AbsolutePath, excluding: [String]) throws -> Self? { + let sourcesPaths: [AbsolutePath] + if let customSources = sources { + sourcesPaths = try customSources.map { source in + let absolutePath = path.appending(try RelativePath(validating: source)) + if absolutePath.extension == nil { + return absolutePath.appending(component: "**") + } + return absolutePath + } + } else { + sourcesPaths = [path.appending(component: "**")] + } + guard !sourcesPaths.isEmpty else { return nil } + return .sourceFilesList( + globs: try sourcesPaths.map { absolutePath -> ProjectDescription.SourceFileGlob in + .glob( + .path(absolutePath.pathString), + excluding: try excluding.map { + let excludePath = path.appending(try RelativePath(validating: $0)) + let excludeGlob = excludePath.extension != nil ? excludePath : excludePath.appending(component: "**") + return .path(excludeGlob.pathString) + } + ) + } + ) + } +} + +extension ProjectDescription.ResourceFileElements { + fileprivate static func from( + sources: [String]?, + resources: [PackageInfo.Target.Resource], + path: AbsolutePath, + excluding: [String] + ) throws -> Self? { + /// Handles the conversion of a `.copy` resource rule of SPM + /// + /// - Parameters: + /// - resourceAbsolutePath: The absolute path of that resource + /// - Returns: A ProjectDescription.ResourceFileElement mapped from a `.copy` resource rule of SPM + func handleCopyResource(resourceAbsolutePath: AbsolutePath) -> ProjectDescription.ResourceFileElement { + .folderReference(path: .path(resourceAbsolutePath.pathString)) + } + + /// Handles the conversion of a `.process` resource rule of SPM + /// + /// - Parameters: + /// - resourceAbsolutePath: The absolute path of that resource + /// - Returns: A ProjectDescription.ResourceFileElement mapped from a `.process` resource rule of SPM + func handleProcessResource(resourceAbsolutePath: AbsolutePath) throws -> ProjectDescription.ResourceFileElement? { + let absolutePathGlob = resourceAbsolutePath.extension != nil ? resourceAbsolutePath : resourceAbsolutePath + .appending(component: "**") + for exclude in excluding { + if absolutePathGlob.isDescendantOfOrEqual(to: path.appending(try RelativePath(validating: exclude))) { + return nil + } + } + return .glob( + pattern: .path(absolutePathGlob.pathString), + excluding: try excluding.map { + let excludePath = path.appending(try RelativePath(validating: $0)) + let excludeGlob = excludePath.extension != nil ? excludePath : excludePath.appending(component: "**") + return .path(excludeGlob.pathString) + } + ) + } + + var resourceFileElements: [ProjectDescription.ResourceFileElement] = try resources.compactMap { + let resourceAbsolutePath = path.appending(try RelativePath(validating: $0.path)) + + switch $0.rule { + case .copy: + // Single files or opaque directories are handled like a .process rule + if !FileHandler.shared.isFolder(resourceAbsolutePath) || resourceAbsolutePath.isOpaqueDirectory { + return try handleProcessResource(resourceAbsolutePath: resourceAbsolutePath) + } else { + return handleCopyResource(resourceAbsolutePath: resourceAbsolutePath) + } + case .process: + return try handleProcessResource(resourceAbsolutePath: resourceAbsolutePath) + } + } + .filter { + switch $0 { + case let .glob(pattern: pattern, excluding: _, tags: _, inclusionCondition: _): + // We will automatically skip including globs of non-existing directories for packages + if !FileHandler.shared.exists(try AbsolutePath(validating: String(pattern.pathString)).parentDirectory) { + return false + } + return true + case .folderReference: + return true + } + } + + // Add default resources path if necessary + // They are handled like a `.process` rule + if sources == nil { + // Already included resources should not be added as default resource + let excludedPaths: Set = Set( + resourceFileElements.map { + switch $0 { + case let .folderReference(path: path, _, _): + AbsolutePath(stringLiteral: path.pathString) + case let .glob(pattern: path, _, _, _): + AbsolutePath(stringLiteral: path.pathString).upToLastNonGlob + } + } + ) + resourceFileElements += try defaultResourcePaths(from: path) { candidateURL in + let candidatePath = AbsolutePath(stringLiteral: candidateURL.path) + let candidateNotInExcludedDirectory = excludedPaths.allSatisfy { !$0.isAncestorOfOrEqual(to: candidatePath) } + return candidateNotInExcludedDirectory + } + .compactMap { try handleProcessResource(resourceAbsolutePath: $0) } + } + + // Check for empty resource files + guard !resourceFileElements.isEmpty else { return nil } + + return .resources(resourceFileElements.uniqued()) + } + + // These files are automatically added as resource if they are inside targets directory. + // Check https://developer.apple.com/documentation/swift_packages/bundling_resources_with_a_swift_package + private static let defaultSpmResourceFileExtensions = Set([ + "xib", + "storyboard", + "xcdatamodeld", + "xcmappingmodel", + "xcassets", + "strings", + ]) + + private static func defaultResourcePaths( + from path: AbsolutePath, + filter: @escaping (Foundation.URL) -> Bool + ) -> [AbsolutePath] { + Array(FileHandler.shared.files( + in: path, + filter: filter, + nameFilter: nil, + extensionFilter: defaultSpmResourceFileExtensions + )) + } +} + +extension ProjectDescription.TargetDependency { + fileprivate static func from( + resolvedDependencies: [PackageInfoMapper.ResolvedDependency], + settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting], + packageToProject: [String: AbsolutePath] + ) throws -> [Self] { + let targetDependencies = resolvedDependencies.compactMap { dependency -> Self? in + switch dependency { + case let .target(name, condition): + return .target(name: name, condition: condition) + case let .xcframework(path, condition): + return .xcframework(path: path, condition: condition) + case let .externalTarget(project, target, condition): + return .project( + target: target, + path: .path(packageToProject[project]!.pathString), + condition: condition + ) + } + } + + let linkerDependencies: [ProjectDescription.TargetDependency] = settings.compactMap { setting in + do { + let condition = try ProjectDescription.PlatformCondition.from(setting.condition) + + switch (setting.tool, setting.name) { + case (.linker, .linkedFramework): + return .sdk(name: setting.value[0], type: .framework, status: .required, condition: condition) + case (.linker, .linkedLibrary): + return .sdk(name: setting.value[0], type: .library, status: .required, condition: condition) + case (.c, _), (.cxx, _), (_, .enableUpcomingFeature), (.swift, _), (.linker, .headerSearchPath), ( + .linker, + .define + ), + (.linker, .unsafeFlags), (_, .enableExperimentalFeature): + return nil + } + } catch { + return nil + } + } + + return targetDependencies + linkerDependencies + } +} + +extension ProjectDescription.Headers { + fileprivate static func from(moduleMap: ModuleMap?) throws -> Self? { + guard let moduleMap else { return nil } + // As per SPM logic, headers should be added only when using the umbrella header without modulemap: + // https://github.com/apple/swift-package-manager/blob/9b9bed7eaf0f38eeccd0d8ca06ae08f6689d1c3f/Sources/Xcodeproj/pbxproj.swift#L588-L609 + switch moduleMap { + case let .directory(moduleMapPath: _, umbrellaDirectory: umbrellaDirectory): + return .headers( + public: .list( + [ + .glob("\(umbrellaDirectory.pathString)/*.h"), + ] + ) + ) + case .none, .header, .custom: + return nil + } + } +} + +extension ProjectDescription.Settings { + // swiftlint:disable:next function_body_length + fileprivate static func from( + target: PackageInfo.Target, + packageFolder: AbsolutePath, + packageName _: String, + settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting], + moduleMap: ModuleMap?, + baseSettings: XcodeGraph.Settings, + targetSettings: [String: XcodeGraph.SettingsDictionary] + ) throws -> Self? { + let mainPath = try target.basePath(packageFolder: packageFolder) + let mainRelativePath = mainPath.relative(to: packageFolder) + + var dependencyHeaderSearchPaths: [String] = [] + if let moduleMap { + if moduleMap != .none, target.type != .system { + let publicHeadersPath = try target.publicHeadersPath(packageFolder: packageFolder) + let publicHeadersRelativePath = publicHeadersPath.relative(to: packageFolder) + dependencyHeaderSearchPaths.append("$(SRCROOT)/\(publicHeadersRelativePath.pathString)") + } + } + + var settingsDictionary: XcodeGraph.SettingsDictionary = [ + // Xcode settings configured by SPM by default + "ALWAYS_SEARCH_USER_PATHS": "YES", + "CLANG_ENABLE_OBJC_WEAK": "NO", + "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER": "NO", + "ENABLE_STRICT_OBJC_MSGSEND": "NO", + "FRAMEWORK_SEARCH_PATHS": ["$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks"], + "GCC_NO_COMMON_BLOCKS": "NO", + "USE_HEADERMAP": "NO", + // Disable warnings in generated projects + "GCC_WARN_INHIBIT_ALL_WARNINGS": "YES", + "SWIFT_SUPPRESS_WARNINGS": "YES", + ] + + let mapper = SettingsMapper( + headerSearchPaths: dependencyHeaderSearchPaths, + mainRelativePath: mainRelativePath, + settings: settings + ) + + let resolvedSettings = try mapper.mapSettings() + + settingsDictionary.merge(resolvedSettings) { $1 } + + if let moduleMapPath = moduleMap?.moduleMapPath { + settingsDictionary["MODULEMAP_FILE"] = .string("$(SRCROOT)/\(moduleMapPath.relative(to: packageFolder))") + } + + if let moduleMap { + switch moduleMap { + case .directory, .header, .custom: + settingsDictionary["DEFINES_MODULE"] = "NO" + switch settingsDictionary["OTHER_CFLAGS"] ?? .array(["$(inherited)"]) { + case let .array(values): + settingsDictionary["OTHER_CFLAGS"] = .array(values + ["-fmodule-name=\(target.name)"]) + case let .string(value): + settingsDictionary["OTHER_CFLAGS"] = .array( + value.split(separator: " ").map(String.init) + ["-fmodule-name=\(target.name)"] + ) + } + case .none: + break + } + } + + var mappedSettingsDictionary = ProjectDescription.SettingsDictionary.from(settingsDictionary: settingsDictionary) + + if let settingsToOverride = targetSettings[target.name] { + let projectDescriptionSettingsToOverride = ProjectDescription.SettingsDictionary + .from(settingsDictionary: settingsToOverride) + mappedSettingsDictionary.merge(projectDescriptionSettingsToOverride) + } + + let configurations: [ProjectDescription.Configuration] = try baseSettings.configurations + .map { buildConfiguration, configuration in + var configuration = configuration ?? Configuration(settings: [:]) + configuration.settings = configuration.settings.merging( + try mapper.settingsForBuildConfiguration(buildConfiguration.name), + uniquingKeysWith: { $1 } + ) + return .from( + buildConfiguration: buildConfiguration, + configuration: configuration, + packageFolder: packageFolder + ) + } + + return .settings( + base: .from(settingsDictionary: baseSettings.base) + .merging( + mappedSettingsDictionary, + uniquingKeysWith: { + switch ($0, $1) { + case let (.array(leftArray), .array(rightArray)): + return SettingValue.array(leftArray + rightArray) + default: + return $1 + } + } + ), + configurations: configurations + .sorted { $0.name.rawValue < $1.name.rawValue }, + defaultSettings: .from(defaultSettings: baseSettings.defaultSettings) + ) + } + + fileprivate struct PackageTarget: Hashable { + let package: String + let target: PackageInfo.Target + } +} + +extension ProjectDescription.PackagePlatform { + fileprivate func destinations() -> ProjectDescription.Destinations { + switch self { + case .iOS: + return [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] + case .macCatalyst: + return [.macCatalyst] + case .macOS: + return [.mac] + case .tvOS: + return [.appleTv] + case .watchOS: + return [.appleWatch] + case .visionOS: + return [.appleVision] + } + } +} + +extension ProjectDescription.Product { + fileprivate static func from(product: XcodeGraph.Product) -> Self { + switch product { + case .app: + return .app + case .staticLibrary: + return .staticLibrary + case .dynamicLibrary: + return .dynamicLibrary + case .framework: + return .framework + case .staticFramework: + return .staticFramework + case .unitTests: + return .unitTests + case .uiTests: + return .uiTests + case .bundle: + return .bundle + case .commandLineTool: + return .commandLineTool + case .appExtension: + return .appExtension + case .watch2App: + return .watch2App + case .watch2Extension: + return .watch2Extension + case .tvTopShelfExtension: + return .tvTopShelfExtension + case .messagesExtension: + return .messagesExtension + case .stickerPackExtension: + return .stickerPackExtension + case .appClip: + return .appClip + case .xpc: + return .xpc + case .systemExtension: + return .systemExtension + case .extensionKitExtension: + return .extensionKitExtension + case .macro: + return .macro + } + } +} + +extension ProjectDescription.SettingsDictionary { + public static func from(settingsDictionary: XcodeGraph.SettingsDictionary) -> Self { + settingsDictionary.mapValues { value in + switch value { + case let .string(stringValue): + return ProjectDescription.SettingValue.string(stringValue) + case let .array(arrayValue): + return ProjectDescription.SettingValue.array(arrayValue) + } + } + } +} + +extension ProjectDescription.Configuration { + public static func from( + buildConfiguration: BuildConfiguration, + configuration: XcodeGraph.Configuration?, + packageFolder: AbsolutePath + ) -> Self { + let name = ConfigurationName(stringLiteral: buildConfiguration.name) + let settings = ProjectDescription.SettingsDictionary.from(settingsDictionary: configuration?.settings ?? [:]) + let xcconfig = configuration?.xcconfig.map { Path.path($0.relative(to: packageFolder).pathString) } + switch buildConfiguration.variant { + case .debug: + return .debug(name: name, settings: settings, xcconfig: xcconfig) + case .release: + return .release(name: name, settings: settings, xcconfig: xcconfig) + } + } +} + +extension ProjectDescription.DefaultSettings { + fileprivate static func from(defaultSettings: XcodeGraph.DefaultSettings) -> Self { + switch defaultSettings { + case let .recommended(excluding): + return .recommended(excluding: excluding) + case let .essential(excluding): + return .essential(excluding: excluding) + case .none: + return .none + } + } +} + +extension ProjectDescription.Destinations { + fileprivate static func from(destinations: XcodeGraph.Destinations) -> Self { + Set( + destinations.map { + switch $0 { + case .iPhone: + return .iPhone + case .iPad: + return .iPad + case .mac: + return .mac + case .macWithiPadDesign: + return .macWithiPadDesign + case .macCatalyst: + return .macCatalyst + case .appleWatch: + return .appleWatch + case .appleTv: + return .appleTv + case .appleVision: + return .appleVision + case .appleVisionWithiPadDesign: + return .appleVisionWithiPadDesign + } + } + ) + } +} + +extension PackageInfo { + fileprivate func projectSettings( + swiftToolsVersion: TSCUtility.Version?, + buildConfigs: [BuildConfiguration]? = nil + ) -> ProjectDescription.Settings? { + var settingsDictionary: ProjectDescription.SettingsDictionary = [:] + + if let cLanguageStandard { + settingsDictionary["GCC_C_LANGUAGE_STANDARD"] = .string(cLanguageStandard) + } + + if let cxxLanguageStandard { + settingsDictionary["CLANG_CXX_LANGUAGE_STANDARD"] = .string(cxxLanguageStandard) + } + + if let swiftLanguageVersion = swiftVersion(for: swiftToolsVersion) { + settingsDictionary["SWIFT_VERSION"] = .string(swiftLanguageVersion) + } + + if let buildConfigs { + let configs = buildConfigs + .sorted() + .map { config -> ProjectDescription.Configuration in + switch config.variant { + case .debug: + return ProjectDescription.Configuration.debug(name: .configuration(config.name)) + case .release: + return ProjectDescription.Configuration.release(name: .configuration(config.name)) + } + } + return .settings(base: settingsDictionary, configurations: configs) + } else { + return settingsDictionary.isEmpty ? nil : .settings(base: settingsDictionary) + } + } + + private func swiftVersion(for configuredSwiftVersion: TSCUtility.Version?) -> String? { + /// Take the latest swift version compatible with the configured one + let maxAllowedSwiftLanguageVersion = swiftLanguageVersions? + .filter { + guard let configuredSwiftVersion else { + return true + } + return $0 <= configuredSwiftVersion + } + .sorted() + .last + + return maxAllowedSwiftLanguageVersion?.description + } +} + +extension PackageInfo.Target { + /// The path used as base for all the relative paths of the package (e.g. sources, resources, headers) + func basePath(packageFolder: AbsolutePath) throws -> AbsolutePath { + if let path { + return packageFolder.appending(try RelativePath(validating: path)) + } else { + let predefinedDirectories: [String] + switch type { + case .test: + predefinedDirectories = PackageInfoMapper.predefinedTestDirectories + default: + predefinedDirectories = PackageInfoMapper.predefinedSourceDirectories + } + let firstMatchingPath = predefinedDirectories + .map { packageFolder.appending(components: [$0, name]) } + .first(where: { FileHandler.shared.exists($0) }) + guard let mainPath = firstMatchingPath else { + throw PackageInfoMapperError.defaultPathNotFound(packageFolder, name, predefinedDirectories) + } + return mainPath + } + } + + func publicHeadersPath(packageFolder: AbsolutePath) throws -> AbsolutePath { + let mainPath = try basePath(packageFolder: packageFolder) + return mainPath.appending(try RelativePath(validating: publicHeadersPath ?? "include")) + } +} + +extension PackageInfo.Target.Dependency { + /// The literal name of the dependency. + var name: String { + switch self { + case let .target(name: name, _): + return name + case let .product(name: name, _, _, _): + return name + case let .byName(name: name, _): + return name + } + } +} + +extension PackageInfoMapper { + public enum ResolvedDependency: Equatable { + case target(name: String, condition: ProjectDescription.PlatformCondition? = nil) + case xcframework(path: Path, condition: ProjectDescription.PlatformCondition? = nil) + case externalTarget(package: String, target: String, condition: ProjectDescription.PlatformCondition? = nil) + + fileprivate var condition: ProjectDescription.PlatformCondition? { + switch self { + case let .target(_, condition): + return condition + case let .xcframework(_, condition): + return condition + case let .externalTarget(_, _, condition): + return condition + } + } + + fileprivate var targetName: String? { + switch self { + case let .target(targetName, _), let .externalTarget(_, targetName, _): + return targetName + case .xcframework: + return nil + } + } + + fileprivate static func fromTarget( + name: String, + targetDependencyToFramework: [String: Path], + condition packageConditionDescription: PackageInfo.PackageConditionDescription? + ) -> [Self] { + do { + let condition = try ProjectDescription.PlatformCondition.from(packageConditionDescription) + + if let framework = targetDependencyToFramework[name] { + return [.xcframework(path: framework, condition: condition)] + } else { + return [.target(name: PackageInfoMapper.sanitize(targetName: name), condition: condition)] + } + } catch { + return [] + } + } + } +} + +extension ProjectDescription.PlatformCondition { + struct OnlyConditionsWithUnsupportedPlatforms: Error {} + + /// Map from a package condition to ProjectDescription.PlatformCondition + /// - Parameter condition: condition representing platforms that a given dependency applies to + /// - Returns: set of PlatformFilters to be used with `GraphDependencyRefrence` + fileprivate static func from(_ condition: PackageInfo.PackageConditionDescription?) throws -> Self? { + guard let condition else { return nil } + let filters: [ProjectDescription.PlatformFilter] = condition.platformNames.compactMap { name in + switch name { + case "ios": + return .ios + case "maccatalyst": + return .catalyst + case "macos": + return .macos + case "tvos": + return .tvos + case "watchos": + return .watchos + case "visionos": + return .visionos + default: + return nil + } + } + + // If empty, we know there are no supported platforms and this dependency should not be included in the graph + if filters.isEmpty { + throw OnlyConditionsWithUnsupportedPlatforms() + } + + return .when(Set(filters)) + } +} + +extension PackageInfo.Platform { + var tuistPlatformName: String { + // catalyst is mapped to iOS platform in tuist + platformName == "maccatalyst" ? "ios" : platformName + } +} diff --git a/Sources/TuistLoader/SwiftPackageManager/SettingsMapper.swift b/Sources/TuistLoader/SwiftPackageManager/SettingsMapper.swift new file mode 100644 index 00000000000..3a063b9f765 --- /dev/null +++ b/Sources/TuistLoader/SwiftPackageManager/SettingsMapper.swift @@ -0,0 +1,168 @@ +import Foundation +import Path +import ProjectDescription +import TuistSupport +import XcodeGraph + +struct SettingsMapper { + init( + headerSearchPaths: [String], + mainRelativePath: RelativePath, + settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting] + ) { + self.headerSearchPaths = headerSearchPaths + self.settings = settings + self.mainRelativePath = mainRelativePath + } + + private let headerSearchPaths: [String] + private let mainRelativePath: RelativePath + private let settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting] + + func mapSettings() throws -> XcodeGraph.SettingsDictionary { + var resolvedSettings = try settingsDictionary() + + for platform in XcodeGraph.Platform.allCases.sorted(by: { $0.rawValue < $1.rawValue }) { + let platformSettings = try settingsDictionary(for: platform) + resolvedSettings.overlay(with: platformSettings, for: platform) + } + + return resolvedSettings + } + + func settingsForBuildConfiguration( + _ buildConfiguration: String + ) throws -> XcodeGraph.SettingsDictionary { + try map( + settings: settings.filter { setting in + return setting.hasConditions && setting.condition?.config?.uppercasingFirst == buildConfiguration + } + ) + } + + // swiftlint:disable:next function_body_length + func settingsDictionary(for platform: XcodeGraph.Platform? = nil) throws -> XcodeGraph.SettingsDictionary { + let platformSettings = try settings(for: platform?.rawValue) + + return try map( + settings: platformSettings, + headerSearchPaths: headerSearchPaths, + defines: ["SWIFT_PACKAGE": "1"], + swiftDefines: "SWIFT_PACKAGE" + ) + } + + private func map( + settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting], + headerSearchPaths: [String] = [], + defines: [String: String] = [:], + swiftDefines: String = "" + ) throws -> XcodeGraph.SettingsDictionary { + var headerSearchPaths = headerSearchPaths + var defines = defines + var swiftDefines = swiftDefines + var cFlags: [String] = [] + var cxxFlags: [String] = [] + var swiftFlags: [String] = [] + var linkerFlags: [String] = [] + + var settingsDictionary = XcodeGraph.SettingsDictionary() + for setting in settings { + switch (setting.tool, setting.name) { + case (.c, .headerSearchPath), (.cxx, .headerSearchPath): + headerSearchPaths.append("$(SRCROOT)/\(mainRelativePath.pathString)/\(setting.value[0])".quotedIfContainsSpaces) + case (.c, .define), (.cxx, .define): + let (name, value) = setting.extractDefine + defines[name] = value + case (.c, .unsafeFlags): + cFlags.append(contentsOf: setting.value) + case (.cxx, .unsafeFlags): + cxxFlags.append(contentsOf: setting.value) + case (.swift, .define): + swiftDefines.append(" \(setting.value[0])") + case (.swift, .unsafeFlags): + swiftFlags.append(contentsOf: setting.value) + case (.swift, .enableUpcomingFeature): + swiftFlags.append("-enable-upcoming-feature \"\(setting.value[0])\"") + case (.swift, .enableExperimentalFeature): + swiftFlags.append("-enable-experimental-feature \"\(setting.value[0])\"") + case (.linker, .unsafeFlags): + linkerFlags.append(contentsOf: setting.value) + case (.linker, .linkedFramework), (.linker, .linkedLibrary): + // Handled as dependency + continue + case (.c, .linkedFramework), (.c, .linkedLibrary), (.cxx, .linkedFramework), (.cxx, .linkedLibrary), + (.swift, .headerSearchPath), (.swift, .linkedFramework), (.swift, .linkedLibrary), + (.linker, .headerSearchPath), (.linker, .define), (_, .enableUpcomingFeature), + (_, .enableExperimentalFeature): + throw PackageInfoMapperError.unsupportedSetting(setting.tool, setting.name) + } + } + + if !headerSearchPaths.isEmpty { + settingsDictionary["HEADER_SEARCH_PATHS"] = .array(["$(inherited)"] + headerSearchPaths.map { $0 }) + } + + if !defines.isEmpty { + let sortedDefines = defines.sorted { $0.key < $1.key } + settingsDictionary["GCC_PREPROCESSOR_DEFINITIONS"] = .array(["$(inherited)"] + sortedDefines + .map { key, value in + "\(key)=\(value.spm_shellEscaped())" + }) + } + + if !swiftDefines.isEmpty { + settingsDictionary["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = "$(inherited) \(swiftDefines)" + } + + if !cFlags.isEmpty { + settingsDictionary["OTHER_CFLAGS"] = .array(["$(inherited)"] + cFlags) + } + + if !cxxFlags.isEmpty { + settingsDictionary["OTHER_CPLUSPLUSFLAGS"] = .array(["$(inherited)"] + cxxFlags) + } + + if !swiftFlags.isEmpty { + settingsDictionary["OTHER_SWIFT_FLAGS"] = .array(["$(inherited)"] + swiftFlags) + } + + if !linkerFlags.isEmpty { + settingsDictionary["OTHER_LDFLAGS"] = .array(["$(inherited)"] + linkerFlags) + } + + return settingsDictionary + } + + // `nil` means settings without a condition + private func settings(for platformName: String?) throws + -> [PackageInfo.Target.TargetBuildSettingDescription.Setting] + { + settings.filter { setting in + if let platformName, setting.hasConditions { + let hasMacCatalystPlatform = setting.condition?.platformNames.contains("maccatalyst") == true + let platformNames: [String] + if hasMacCatalystPlatform { + platformNames = (setting.condition?.platformNames ?? []) + ["ios"] + } else { + platformNames = setting.condition?.platformNames ?? [] + } + return platformNames.contains(platformName) + } else { + return !setting.hasConditions + } + } + } +} + +extension PackageInfo.Target.TargetBuildSettingDescription.Setting { + fileprivate var extractDefine: (name: String, value: String) { + let define = value[0] + if define.contains("=") { + let split = define.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + return (name: String(split[0]), value: String(split[1])) + } else { + return (name: define, value: "1") + } + } +} diff --git a/Sources/TuistLoader/SwiftPackageManager/SwiftPackageManagerModuleMapGenerator.swift b/Sources/TuistLoader/SwiftPackageManager/SwiftPackageManagerModuleMapGenerator.swift new file mode 100644 index 00000000000..f290f398391 --- /dev/null +++ b/Sources/TuistLoader/SwiftPackageManager/SwiftPackageManagerModuleMapGenerator.swift @@ -0,0 +1,180 @@ +import Path +import TuistCore +import TuistSupport + +/// The type of modulemap file +public enum ModuleMap: Equatable { + /// No headers and hence no modulemap file + case none + /// Custom modulemap file provided in SPM package + case custom(AbsolutePath, umbrellaHeaderPath: AbsolutePath?) + /// Umbrella header provided in SPM package + case header(AbsolutePath, moduleMapPath: AbsolutePath) + /// No umbrella header provided in SPM package, define umbrella directory + case directory(moduleMapPath: AbsolutePath, umbrellaDirectory: AbsolutePath) + + var moduleMapPath: AbsolutePath? { + switch self { + case let .custom(path, umbrellaHeaderPath: _): + return path + case let .header(_, moduleMapPath: path): + return path + case let .directory(moduleMapPath: path, umbrellaDirectory: _): + return path + case .none: + return nil + } + } + + /// Name of the module map file recognized by the Clang and Swift compilers. + public static let filename = "module.modulemap" +} + +/// Protocol that allows to generate a modulemap for an SPM target. +/// It implements the Swift Package Manager logic +/// [documented here](https://github.com/apple/swift-package-manager/blob/main/Documentation/Usage.md#creating-c-language-targets) +/// and +/// [implemented here](https://github.com/apple/swift-package-manager/blob/main/Sources/PackageLoading/ModuleMapGenerator.swift). +public protocol SwiftPackageManagerModuleMapGenerating { + func generate( + packageDirectory: AbsolutePath, + moduleName: String, + publicHeadersPath: AbsolutePath + ) throws -> ModuleMap +} + +public final class SwiftPackageManagerModuleMapGenerator: SwiftPackageManagerModuleMapGenerating { + private let contentHasher: ContentHashing + + public init(contentHasher: ContentHashing = ContentHasher()) { + self.contentHasher = contentHasher + } + + // swiftlint:disable:next function_body_length + public func generate( + packageDirectory: AbsolutePath, + moduleName: String, + publicHeadersPath: AbsolutePath + ) throws -> ModuleMap { + let sanitizedModuleName = moduleName.sanitizedModuleName + let umbrellaHeaderPath = publicHeadersPath.appending(component: sanitizedModuleName + ".h") + let nestedUmbrellaHeaderPath = publicHeadersPath + .appending(components: sanitizedModuleName, sanitizedModuleName + ".h") + let customModuleMapPath = try Self.customModuleMapPath(publicHeadersPath: publicHeadersPath) + let generatedModuleMapPath: AbsolutePath + + if publicHeadersPath.pathString.contains("\(Constants.SwiftPackageManager.packageBuildDirectoryName)/checkouts") { + generatedModuleMapPath = packageDirectory + .parentDirectory + .parentDirectory + .appending( + components: Constants.DerivedDirectory.dependenciesDerivedDirectory, + sanitizedModuleName, + "\(sanitizedModuleName).modulemap" + ) + } else { + generatedModuleMapPath = packageDirectory.appending( + components: Constants.DerivedDirectory.name, "\(sanitizedModuleName).modulemap" + ) + } + + if !FileHandler.shared.exists(generatedModuleMapPath.parentDirectory) { + try FileHandler.shared.createFolder(generatedModuleMapPath.parentDirectory) + } + + if FileHandler.shared.exists(umbrellaHeaderPath) { + if let customModuleMapPath { + return .custom(customModuleMapPath, umbrellaHeaderPath: umbrellaHeaderPath) + } + try writeIfDifferent( + moduleMapContent: umbrellaHeaderModuleMap( + umbrellaHeaderPath: umbrellaHeaderPath, + sanitizedModuleName: sanitizedModuleName + ), + to: generatedModuleMapPath, + atomically: true + ) + // If 'PublicHeadersDir/ModuleName.h' exists, then use it as the umbrella header. + return .header(umbrellaHeaderPath, moduleMapPath: generatedModuleMapPath) + } else if FileHandler.shared.exists(nestedUmbrellaHeaderPath) { + if let customModuleMapPath { + return .custom(customModuleMapPath, umbrellaHeaderPath: nestedUmbrellaHeaderPath) + } + try writeIfDifferent( + moduleMapContent: umbrellaHeaderModuleMap( + umbrellaHeaderPath: nestedUmbrellaHeaderPath, + sanitizedModuleName: sanitizedModuleName + ), + to: generatedModuleMapPath, + atomically: true + ) + // If 'PublicHeadersDir/ModuleName/ModuleName.h' exists, then use it as the umbrella header. + return .header(nestedUmbrellaHeaderPath, moduleMapPath: generatedModuleMapPath) + } else if let customModuleMapPath { + // User defined modulemap exists, use it + return .custom(customModuleMapPath, umbrellaHeaderPath: nil) + } else if FileHandler.shared.exists(publicHeadersPath) { + if FileHandler.shared.glob(publicHeadersPath, glob: "**/*.h").isEmpty { + return .none + } + // Consider the public headers folder as umbrella directory + let generatedModuleMapContent = + """ + module \(sanitizedModuleName) { + umbrella "\(publicHeadersPath.pathString)" + export * + } + """ + try writeIfDifferent(moduleMapContent: generatedModuleMapContent, to: generatedModuleMapPath, atomically: true) + + return .directory(moduleMapPath: generatedModuleMapPath, umbrellaDirectory: publicHeadersPath) + } else { + return .none + } + } + + /// Write our modulemap to disk if it is distinct from what already exists. + /// This addresses an issue with dependencies that are included in a precompiled header. + /// https://github.com/tuist/tuist/issues/6211 + /// - Parameters: + /// - moduleMapContent: contents of the moduleMap file to write + /// - path: destination to write file contents to + /// - atomically: whether to write atomically + func writeIfDifferent(moduleMapContent: String, to path: AbsolutePath, atomically: Bool) throws { + let newContentHash = try contentHasher.hash(moduleMapContent) + let currentContentHash = try? contentHasher.hash(path: path) + if currentContentHash != newContentHash { + try FileHandler.shared.write(moduleMapContent, path: path, atomically: true) + } + } + + static func customModuleMapPath(publicHeadersPath: AbsolutePath) throws -> AbsolutePath? { + guard FileHandler.shared.exists(publicHeadersPath) else { return nil } + + let moduleMapPath = try RelativePath(validating: ModuleMap.filename) + let publicHeadersFolderContent = try FileHandler.shared.contentsOfDirectory(publicHeadersPath) + + if publicHeadersFolderContent.contains(publicHeadersPath.appending(moduleMapPath)) { + return publicHeadersPath.appending(moduleMapPath) + } else if publicHeadersFolderContent.count == 1, + let nestedHeadersPath = publicHeadersFolderContent.first, + FileHandler.shared.isFolder(nestedHeadersPath), + FileHandler.shared.exists(nestedHeadersPath.appending(moduleMapPath)) + { + return nestedHeadersPath.appending(moduleMapPath) + } else { + return nil + } + } + + private func umbrellaHeaderModuleMap(umbrellaHeaderPath: AbsolutePath, sanitizedModuleName: String) -> String { + """ + framework module \(sanitizedModuleName) { + umbrella header "\(umbrellaHeaderPath.pathString)" + + export * + module * { export * } + } + """ + } +} diff --git a/Sources/TuistLoader/Up/Carthage.swift b/Sources/TuistLoader/Up/Carthage.swift deleted file mode 100644 index 9767a9098c6..00000000000 --- a/Sources/TuistLoader/Up/Carthage.swift +++ /dev/null @@ -1,151 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -/// Model that represents the content of the file that Carthage -/// generates for each resolved dependency. -struct CarthageVersionFile: Codable { - /// The git revision of the resolved dependency. - let commitish: String -} - -/// Protocol that defines an interface to interact with a local Carthage setup. -protocol Carthaging { - /// Bootstraps the dependencies in the given directory. - /// - /// - Parameters: - /// - path: Directory where the Carthage dependencies are defined. - /// - platforms: Platforms the dependencies will be bootstraped for. - /// - useXCFrameworks: Indicates whether Carthage produces XCFrameworks or regular frameworks. - /// - noUseBinaries: Indicates whether Carthage rebuilds the dependency from source instead of using downloaded binaries - /// when possible. - /// - dependencies: Dependencies to bootstrap - /// - Throws: An error if the dependencies bootstrap fails. - func bootstrap( - path: AbsolutePath, - platforms: [Platform], - useXCFrameworks: Bool, - noUseBinaries: Bool, - dependencies: [String] - ) throws - - /// Returns the list of outdated dependencies in the given directory. - /// - /// - Parameter path: Project directory. - /// - Returns: List of outdated dependencies. - func outdated(path: AbsolutePath) throws -> [String]? -} - -final class Carthage: Carthaging { - // swiftlint:disable force_try - /// Regex used to match and extract information from the lines in the Cartfile.resolved file. - static let resolvedLineRegex = try! NSRegularExpression(pattern: "(github|git|binary) \"([^\"]+)\" \"([^\"]+)\"", options: []) - // swiftlint:enable force_try - - /// Bootstraps the dependencies in the given directory. - /// - /// - Parameters: - /// - path: Directory where the Carthage dependencies are defined. - /// - platforms: Platforms the dependencies will be bootstraped for. - /// - useXCFrameworks: Indicates whether Carthage produces XCFrameworks or regular frameworks. - /// - noUseBinaries: Indicates whether Carthage rebuilds the dependency from source instead of using downloaded binaries - /// when possible. - /// - dependencies: Dependencies to bootstrap - /// - Throws: An error if the dependencies bootstrap fails. - func bootstrap( - path: AbsolutePath, - platforms: [Platform], - useXCFrameworks: Bool, - noUseBinaries: Bool, - dependencies: [String] - ) throws { - let carthagePath = try System.shared.which("carthage") - - var command: [String] = [carthagePath] - command.append("bootstrap") - command.append("--project-directory") - command.append(path.pathString) - - if useXCFrameworks { - command.append("--use-xcframeworks") - } - - if noUseBinaries { - command.append("--no-use-binaries") - } - - if !platforms.isEmpty { - command.append("--platform") - command.append(platforms.map(\.caseValue).joined(separator: ",")) - } - - command.append(contentsOf: dependencies) - - try System.shared.run(command) - } - - /// Returns the list of outdated dependencies in the given directory. - /// Reference: https://github.com/Carthage/workflows/blob/master/carthage-verify - /// - /// - Parameter path: Project directory. - /// - Returns: List of outdated dependencies. - func outdated(path: AbsolutePath) throws -> [String]? { - let cartfileResolvedPath = path.appending(component: "Cartfile.resolved") - - if !FileHandler.shared.exists(cartfileResolvedPath) { - return nil - } - - var outdated: [String] = [] - let cartfileResolved = try FileHandler.shared.readTextFile(cartfileResolvedPath) - let cartfileResolvedNSString = cartfileResolved as NSString - let jsonDecoder = JSONDecoder() - - try Carthage.resolvedLineRegex.matches( - in: cartfileResolved, - options: [], - range: NSRange( - location: 0, - length: cartfileResolved.count - ) - ).forEach { match in - let dependencyNameRange = match.range(at: 2) - var dependencyName = String(cartfileResolvedNSString.substring(with: dependencyNameRange).split(separator: "/").last!) - - let dependencyTypeRange = match.range(at: 1) - let dependencyType = DependencyType(rawValue: cartfileResolvedNSString.substring(with: dependencyTypeRange)) - if dependencyType == .binary { - dependencyName = (dependencyName as NSString).deletingPathExtension - } - - let dependencyRevisionRange = match.range(at: 3) - let dependencyRevision = cartfileResolvedNSString.substring(with: dependencyRevisionRange) - - let dependencyVersionFilePath = path - .appending(try RelativePath(validating: "Carthage/Build/.\(dependencyName).version")) - - // We consider missing dependencies outdated - if !FileHandler.shared.exists(dependencyVersionFilePath) { - outdated.append(dependencyName) - return - } - - let dependencyVersionData = try Data(contentsOf: dependencyVersionFilePath.url) - let dependencyVersionFile = try jsonDecoder.decode(CarthageVersionFile.self, from: dependencyVersionData) - - if dependencyVersionFile.commitish != dependencyRevision { - outdated.append(dependencyName) - } - } - - return outdated - } -} - -extension Carthage { - private enum DependencyType: String { - case github, git, binary - } -} diff --git a/Sources/TuistLoader/Utils/HelpersDirectoryLocator.swift b/Sources/TuistLoader/Utils/HelpersDirectoryLocator.swift index 6f88d26c7eb..2bd650c5d90 100644 --- a/Sources/TuistLoader/Utils/HelpersDirectoryLocator.swift +++ b/Sources/TuistLoader/Utils/HelpersDirectoryLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport diff --git a/Sources/TuistLoader/Utils/ManifestFilesLocator.swift b/Sources/TuistLoader/Utils/ManifestFilesLocator.swift index c64243eff73..0bbb62375ef 100644 --- a/Sources/TuistLoader/Utils/ManifestFilesLocator.swift +++ b/Sources/TuistLoader/Utils/ManifestFilesLocator.swift @@ -1,8 +1,10 @@ import Foundation -import TSCBasic +import Mockable +import Path import TuistCore import TuistSupport +@Mockable public protocol ManifestFilesLocating: AnyObject { /// It locates the manifest files in the locating directory. /// - Parameter locatingPath: Directory for which the manifest files will be obtained. @@ -34,10 +36,6 @@ public protocol ManifestFilesLocating: AnyObject { /// - Parameter locatingPath: Path from where to do the lookup. func locateConfig(at locatingPath: AbsolutePath) -> AbsolutePath? - /// 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( @@ -48,15 +46,20 @@ public protocol ManifestFilesLocating: AnyObject { public final class ManifestFilesLocator: ManifestFilesLocating { /// Utility to locate the root directory of the project let rootDirectoryLocator: RootDirectoryLocating + let fileHandler: FileHandling - public init(rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator()) { + public init( + rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator(), + fileHandler: FileHandling = FileHandler.shared + ) { self.rootDirectoryLocator = rootDirectoryLocator + self.fileHandler = fileHandler } public func locateManifests(at locatingPath: AbsolutePath) -> [(Manifest, AbsolutePath)] { Manifest.allCases.compactMap { manifest in let path = locatingPath.appending(component: manifest.fileName(locatingPath)) - if FileHandler.shared.exists(path) { return (manifest, path) } + if fileHandler.exists(path) { return (manifest, path) } return nil } } @@ -70,7 +73,7 @@ public final class ManifestFilesLocator: ManifestFilesLocating { let pluginPath = locatingPath.appending( component: Manifest.plugin.fileName(locatingPath) ) - guard FileHandler.shared.exists(pluginPath) else { return [] } + guard fileHandler.exists(pluginPath) else { return [] } return [pluginPath] } else { let path = rootDirectoryLocator.locate(from: locatingPath) ?? locatingPath @@ -100,16 +103,22 @@ public final class ManifestFilesLocator: ManifestFilesLocating { Manifest.workspace.fileName(path), Manifest.plugin.fileName(path), ] - let tuistManifestsFilePaths = FileHandler.shared.glob(path, glob: "**/*.swift") - .filter { fileNamesCandidates.contains($0.basename) } - .filter { hasValidManifestContent($0) } - let cachedTuistManifestsFilePaths = Set(tuistManifestsFilePaths) - cacheTuistManifestsFilePaths[path] = cachedTuistManifestsFilePaths - return cachedTuistManifestsFilePaths + + let tuistManifestsFilePaths = FileHandler.shared.files( + in: path, + nameFilter: fileNamesCandidates, + extensionFilter: ["swift"] + ).filter { + hasValidManifestContent($0) + } + + cacheTuistManifestsFilePaths[path] = tuistManifestsFilePaths + + return tuistManifestsFilePaths } private func hasValidManifestContent(_ path: AbsolutePath) -> Bool { - guard let content = try? FileHandler.shared.readTextFile(path) else { return false } + guard let content = try? fileHandler.readTextFile(path) else { return false } let tuistManifestSignature = "import ProjectDescription" return content.contains(tuistManifestSignature) || content.isEmpty @@ -149,7 +158,7 @@ public final class ManifestFilesLocator: ManifestFilesLocating { ) ), ] - .filter { FileHandler.shared.exists($0.path) } + .filter { fileHandler.exists($0.path) } } else { let path = rootDirectoryLocator.locate(from: locatingPath) ?? locatingPath @@ -179,31 +188,42 @@ public final class ManifestFilesLocator: ManifestFilesLocating { } public func locateConfig(at locatingPath: AbsolutePath) -> AbsolutePath? { - // swiftlint:disable:next force_try - let subPath = try! RelativePath(validating: "\(Constants.tuistDirectoryName)/\(Manifest.config.fileName(locatingPath))") - return traverseAndLocate(at: locatingPath, appending: subPath) - } - - public func locateDependencies(at locatingPath: AbsolutePath) -> AbsolutePath? { - let subPath = - // swiftlint:disable:next force_try - try! RelativePath(validating: "\(Constants.tuistDirectoryName)/\(Manifest.dependencies.fileName(locatingPath))") - return traverseAndLocate(at: locatingPath, appending: subPath) + guard let tuistDirectory = traverseAndLocateTuistDirectory(at: locatingPath) else { return nil } + let configSwiftPath = tuistDirectory.appending(component: Manifest.config.fileName(locatingPath)) + if fileHandler.exists(configSwiftPath) { return configSwiftPath } + return nil } 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) + guard let rootDirectory = rootDirectoryLocator.locate(from: locatingPath) else { return nil } + let defaultPackageSwiftPath = rootDirectory.appending( + components: [ + Constants.tuistDirectoryName, + Constants.SwiftPackageManager.packageSwiftName, + ] + ) + let rootPackageSwiftPath = rootDirectory + .appending(component: Constants.SwiftPackageManager.packageSwiftName) + if fileHandler.exists(defaultPackageSwiftPath) { + return defaultPackageSwiftPath + } else if fileHandler.exists(rootPackageSwiftPath) { + return rootPackageSwiftPath + } else { + return nil + } } // MARK: - Helpers + private func traverseAndLocateTuistDirectory(at locatingPath: AbsolutePath) -> AbsolutePath? { + // swiftlint:disable:next force_try + return traverseAndLocate(at: locatingPath, appending: try! RelativePath(validating: Constants.tuistDirectoryName)) + } + private func traverseAndLocate(at locatingPath: AbsolutePath, appending subpath: RelativePath) -> AbsolutePath? { let manifestPath = locatingPath.appending(subpath) - if FileHandler.shared.exists(manifestPath) { + if fileHandler.exists(manifestPath) { return manifestPath } else if locatingPath != .root { return traverseAndLocate(at: locatingPath.parentDirectory, appending: subpath) diff --git a/Sources/TuistLoader/Utils/ProjectDescription+TestData.swift b/Sources/TuistLoader/Utils/ProjectDescription+TestData.swift new file mode 100644 index 00000000000..d0fddceee82 --- /dev/null +++ b/Sources/TuistLoader/Utils/ProjectDescription+TestData.swift @@ -0,0 +1,233 @@ +import Foundation +import TuistSupport + +#if DEBUG + @testable import ProjectDescription + + extension Config { + public static func test( + fullHandle: String? = nil, + url: String = Constants.URLs.production.absoluteString, + generationOptions: Config.GenerationOptions = .options(), + plugins: [PluginLocation] = [] + ) -> Config { + Config( + fullHandle: fullHandle, + url: url, + plugins: plugins, + generationOptions: generationOptions + ) + } + } + + extension Template { + public static func test( + description: String = "Template", + attributes: [Attribute] = [], + items: [Template.Item] = [] + ) -> Template { + Template( + description: description, + attributes: attributes, + items: items + ) + } + } + + extension Workspace { + public static func test( + name: String = "Workspace", + projects: [Path] = [], + schemes: [Scheme] = [], + additionalFiles: [FileElement] = [] + ) -> Workspace { + Workspace( + name: name, + projects: projects, + schemes: schemes, + additionalFiles: additionalFiles + ) + } + } + + extension Project { + public static func test( + name: String = "Project", + organizationName: String? = nil, + settings: Settings? = nil, + targets: [Target] = [], + additionalFiles: [FileElement] = [] + ) -> Project { + Project( + name: name, + organizationName: organizationName, + settings: settings, + targets: targets, + additionalFiles: additionalFiles + ) + } + } + + extension Target { + public static func test( + name: String = "Target", + destinations: Destinations = .iOS, + product: Product = .framework, + productName: String? = nil, + bundleId: String = "com.some.bundle.id", + infoPlist: InfoPlist = .file(path: "Info.plist"), + sources: SourceFilesList = "Sources/**", + resources: ResourceFileElements = "Resources/**", + headers: Headers? = nil, + entitlements: Entitlements = .file(path: "Entitlements.entitlements"), + scripts: [TargetScript] = [], + dependencies: [TargetDependency] = [], + settings: Settings? = nil, + coreDataModels: [CoreDataModel] = [], + environment: [String: String] = [:] + ) -> Target { + .target( + name: name, + destinations: destinations, + product: product, + productName: productName, + bundleId: bundleId, + infoPlist: infoPlist, + sources: sources, + resources: resources, + headers: headers, + entitlements: entitlements, + scripts: scripts, + dependencies: dependencies, + settings: settings, + coreDataModels: coreDataModels, + environmentVariables: environment.mapValues { .init(stringLiteral: $0) } + ) + } + } + + extension TargetScript { + public static func test( + name: String = "Action", + tool: String = "", + order: Order = .pre, + arguments: [String] = [], + inputPaths: [FileListGlob] = [], + inputFileListPaths: [Path] = [], + outputPaths: [Path] = [], + outputFileListPaths: [Path] = [], + dependencyFile: Path? = nil + ) -> TargetScript { + TargetScript( + name: name, + script: .tool(path: tool, args: arguments), + order: order, + inputPaths: inputPaths, + inputFileListPaths: inputFileListPaths, + outputPaths: outputPaths, + outputFileListPaths: outputFileListPaths, + dependencyFile: dependencyFile + ) + } + } + + extension Scheme { + public static func test( + name: String = "Scheme", + shared: Bool = false, + buildAction: BuildAction? = nil, + testAction: TestAction? = nil, + runAction: RunAction? = nil + ) -> Scheme { + .scheme( + name: name, + shared: shared, + buildAction: buildAction, + testAction: testAction, + runAction: runAction + ) + } + } + + extension BuildAction { + public static func test(targets: [TargetReference] = []) -> BuildAction { + .buildAction( + targets: targets, + preActions: [ExecutionAction.test()], + postActions: [ExecutionAction.test()] + ) + } + } + + extension TestAction { + public static func test( + targets: [TestableTarget] = [], + arguments: Arguments? = nil, + configuration: ConfigurationName = .debug, + coverage: Bool = true + ) -> TestAction { + TestAction.targets( + targets, + arguments: arguments, + configuration: configuration, + preActions: [ExecutionAction.test()], + postActions: [ExecutionAction.test()], + options: .options(coverage: coverage) + ) + } + } + + extension RunAction { + public static func test( + configuration: ConfigurationName = .debug, + executable: TargetReference? = nil, + arguments: Arguments? = nil, + options: RunActionOptions = .options() + ) -> RunAction { + RunAction( + configuration: configuration, + executable: executable, + arguments: arguments, + options: options + ) + } + } + + extension ExecutionAction { + public static func test( + title: String = "Test Script", + scriptText: String = "echo Test", + target: TargetReference? = .target("Target") + ) -> ExecutionAction { + ExecutionAction( + title: title, + scriptText: scriptText, + target: target + ) + } + } + + extension Arguments { + public static func test( + environment: [String: String] = [:], + launchArguments: [LaunchArgument] = [] + ) -> Arguments { + Arguments( + environmentVariables: environment.mapValues { .init(stringLiteral: $0) }, + launchArguments: launchArguments + ) + } + } + + extension Plugin { + public static func test(name: String = "Plugin") -> Plugin { + Plugin(name: name) + } + } + + extension PackageSettings { + public static func test() -> PackageSettings { + PackageSettings() + } + } +#endif diff --git a/Sources/TuistLoader/Utils/ProjectDescriptionPaths.swift b/Sources/TuistLoader/Utils/ProjectDescriptionPaths.swift index 78ed322a8fc..6cd2333fc22 100644 --- a/Sources/TuistLoader/Utils/ProjectDescriptionPaths.swift +++ b/Sources/TuistLoader/Utils/ProjectDescriptionPaths.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// Project Description Paths /// diff --git a/Sources/TuistLoader/Utils/ResourceLocator.swift b/Sources/TuistLoader/Utils/ResourceLocator.swift index 3e02cc41f94..1c9bd20d84e 100644 --- a/Sources/TuistLoader/Utils/ResourceLocator.swift +++ b/Sources/TuistLoader/Utils/ResourceLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport public protocol ResourceLocating: AnyObject { @@ -58,7 +58,8 @@ public final class ResourceLocator: ResourceLocating { */ bundlePath.parentDirectory.appending(component: "lib"), ] - if let frameworkSearchPaths = ProcessEnv.vars["TUIST_FRAMEWORK_SEARCH_PATHS"]?.components(separatedBy: " ") + if let frameworkSearchPaths = ProcessInfo.processInfo.environment["TUIST_FRAMEWORK_SEARCH_PATHS"]? + .components(separatedBy: " ") .filter({ !$0.isEmpty }) { paths.append( diff --git a/Sources/TuistLoader/Utils/ResourceSynthesizerPathLocator.swift b/Sources/TuistLoader/Utils/ResourceSynthesizerPathLocator.swift index 7bfbe0b1fcb..77a9ddd50d7 100644 --- a/Sources/TuistLoader/Utils/ResourceSynthesizerPathLocator.swift +++ b/Sources/TuistLoader/Utils/ResourceSynthesizerPathLocator.swift @@ -1,7 +1,6 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport public protocol ResourceSynthesizerPathLocating { @@ -10,7 +9,7 @@ public protocol ResourceSynthesizerPathLocating { func templatePath( for pluginName: String, resourceName: String, - resourceSynthesizerPlugins: [PluginResourceSynthesizer] + resourceSynthesizerPlugins: [TuistCore.PluginResourceSynthesizer] ) throws -> AbsolutePath func templatePath( diff --git a/Sources/TuistLoader/Utils/StencilPathLocator.swift b/Sources/TuistLoader/Utils/StencilPathLocator.swift index ffbb979f8d5..6657875ffbf 100644 --- a/Sources/TuistLoader/Utils/StencilPathLocator.swift +++ b/Sources/TuistLoader/Utils/StencilPathLocator.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph public protocol StencilPathLocating { func locate(at: AbsolutePath) -> AbsolutePath? diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockConfigLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockConfigLoader.swift deleted file mode 100644 index 651d4ee9ac8..00000000000 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockConfigLoader.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph -import TuistLoader - -public final class MockConfigLoader: ConfigLoading { - public init() {} - - public var loadConfigStub: ((AbsolutePath) throws -> Config)? - public func loadConfig(path: AbsolutePath) throws -> Config { - try loadConfigStub?(path) ?? .default - } - - public var locateConfigStub: ((AbsolutePath) -> AbsolutePath?)? - public func locateConfig(at: TSCBasic.AbsolutePath) -> TSCBasic.AbsolutePath? { - locateConfigStub?(at) - } -} diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockDependenciesModelLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockDependenciesModelLoader.swift deleted file mode 100644 index 39aa90a3fa1..00000000000 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockDependenciesModelLoader.swift +++ /dev/null @@ -1,28 +0,0 @@ -import TSCBasic -import TuistGraph -import TuistSupport - -@testable import TuistLoader - -public class MockDependenciesModelLoader: DependenciesModelLoading { - public init() {} - - public var invokedLoadDependencies = false - public var invokedLoadDependenciesCount = 0 - public var invokedLoadDependenciesParameters: (AbsolutePath, Plugins)? - public var invokedLoadDependenciesParemetersList = [(AbsolutePath, Plugins)]() - public var loadDependenciesStub: ((AbsolutePath, Plugins) throws -> Dependencies)? - - public func loadDependencies(at path: AbsolutePath, with plugins: Plugins) throws -> Dependencies { - invokedLoadDependencies = true - invokedLoadDependenciesCount += 1 - invokedLoadDependenciesParameters = (path, plugins) - invokedLoadDependenciesParemetersList.append((path, plugins)) - - if let stub = loadDependenciesStub { - return try stub(path, plugins) - } else { - return Dependencies(carthage: nil, swiftPackageManager: nil, platforms: []) - } - } -} diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift deleted file mode 100644 index 46d349feed1..00000000000 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift +++ /dev/null @@ -1,100 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import struct TuistGraph.Plugins -import TuistSupport -@testable import TuistLoader -@testable import TuistSupportTesting - -public final class MockManifestLoader: ManifestLoading { - public var loadProjectCount: UInt = 0 - public var loadProjectStub: ((AbsolutePath) throws -> Project)? - - public var loadWorkspaceCount: UInt = 0 - public var loadWorkspaceStub: ((AbsolutePath) throws -> Workspace)? - - public var manifestsAtCount: UInt = 0 - public var manifestsAtStub: ((AbsolutePath) -> Set)? - - public var manifestPathCount: UInt = 0 - public var manifestPathStub: ((AbsolutePath, Manifest) throws -> AbsolutePath)? - - public var loadConfigCount: UInt = 0 - public var loadConfigStub: ((AbsolutePath) throws -> Config)? - - public var loadTemplateCount: UInt = 0 - public var loadTemplateStub: ((AbsolutePath) throws -> Template)? - - 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)? - - public init() {} - - public func loadProject(at path: AbsolutePath) throws -> Project { - try loadProjectStub?(path) ?? Project.test() - } - - public func loadWorkspace(at path: AbsolutePath) throws -> Workspace { - try loadWorkspaceStub?(path) ?? Workspace.test() - } - - public func manifests(at path: AbsolutePath) -> Set { - manifestsAtCount += 1 - return manifestsAtStub?(path) ?? Set() - } - - public func validateHasProjectOrWorkspaceManifest(at path: AbsolutePath) throws { - let manifests = manifests(at: path) - guard manifests.contains(.workspace) || manifests.contains(.project) else { - throw ManifestLoaderError.manifestNotFound(path) - } - } - - func manifestPath(at path: AbsolutePath, manifest: Manifest) throws -> AbsolutePath { - manifestPathCount += 1 - return try manifestPathStub?(path, manifest) ?? TemporaryDirectory(removeTreeOnDeinit: true).path - } - - public func loadConfig(at path: AbsolutePath) throws -> Config { - loadConfigCount += 1 - return try loadConfigStub?(path) ?? Config.test() - } - - public func loadTemplate(at path: AbsolutePath) throws -> Template { - loadTemplateCount += 1 - return try loadTemplateStub?(path) ?? Template.test() - } - - public func loadDependencies(at path: AbsolutePath) throws -> Dependencies { - loadDependenciesCount += 1 - 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() - } - - public var taskLoadArgumentsStub: ((AbsolutePath) throws -> [String])? - public func taskLoadArguments(at path: AbsolutePath) throws -> [String] { - try taskLoadArgumentsStub?(path) ?? [] - } - - public var registerPluginsCount: UInt = 0 - public var registerPluginsStub: ((Plugins) throws -> Void)? - public func register(plugins: Plugins) throws { - registerPluginsCount += 1 - try registerPluginsStub?(plugins) - } -} diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift index b065c46c515..ee0d51f97f7 100644 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift +++ b/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift @@ -1,6 +1,5 @@ -import TSCBasic -import TuistGraph -import TuistGraphTesting +import Path +import TuistCore import TuistSupport @testable import TuistLoader diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateGitLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateGitLoader.swift index 9b8c35813c7..da4ef5f2119 100644 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateGitLoader.swift +++ b/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateGitLoader.swift @@ -1,12 +1,12 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistLoader public final class MockTemplateGitLoader: TemplateGitLoading { public var loadTemplateStub: ((String) throws -> Template)? - public func loadTemplate(from templateURL: String, closure: (Template) throws -> Void) throws { + public func loadTemplate(from templateURL: String, closure: @escaping (Template) async throws -> Void) async throws { let template = try loadTemplateStub?(templateURL) ?? Template(description: "", attributes: [], items: []) - try closure(template) + try await closure(template) } } diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateLoader.swift index 99b3dc9475d..8d5f2baeae2 100644 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateLoader.swift +++ b/Sources/TuistLoaderTesting/Loaders/Mocks/MockTemplateLoader.swift @@ -1,11 +1,11 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistLoader public final class MockTemplateLoader: TemplateLoading { public var loadTemplateStub: ((AbsolutePath) throws -> Template)? - public func loadTemplate(at path: AbsolutePath) throws -> Template { + public func loadTemplate(at path: AbsolutePath, plugins _: Plugins) throws -> Template { try loadTemplateStub?(path) ?? Template(description: "", attributes: [], items: []) } } diff --git a/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift b/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift deleted file mode 100644 index fe138385ff4..00000000000 --- a/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift +++ /dev/null @@ -1,226 +0,0 @@ -import Foundation -@testable import ProjectDescription - -extension Config { - public static func test( - generationOptions: Config.GenerationOptions = .options(), - plugins: [PluginLocation] = [] - ) -> Config { - Config(plugins: plugins, generationOptions: generationOptions) - } -} - -extension Template { - public static func test( - description: String = "Template", - attributes: [Attribute] = [], - items: [Template.Item] = [] - ) -> Template { - Template( - description: description, - attributes: attributes, - items: items - ) - } -} - -extension Workspace { - public static func test( - name: String = "Workspace", - projects: [Path] = [], - schemes: [Scheme] = [], - additionalFiles: [FileElement] = [] - ) -> Workspace { - Workspace( - name: name, - projects: projects, - schemes: schemes, - additionalFiles: additionalFiles - ) - } -} - -extension Project { - public static func test( - name: String = "Project", - organizationName: String? = nil, - settings: Settings? = nil, - targets: [Target] = [], - additionalFiles: [FileElement] = [] - ) -> Project { - Project( - name: name, - organizationName: organizationName, - settings: settings, - targets: targets, - additionalFiles: additionalFiles - ) - } -} - -extension Target { - public static func test( - name: String = "Target", - platform: Platform = .iOS, - product: Product = .framework, - productName: String? = nil, - bundleId: String = "com.some.bundle.id", - infoPlist: InfoPlist = .file(path: "Info.plist"), - sources: SourceFilesList = "Sources/**", - resources: ResourceFileElements = "Resources/**", - headers: Headers? = nil, - entitlements: Entitlements = .file(path: "Entitlements.entitlements"), - scripts: [TargetScript] = [], - dependencies: [TargetDependency] = [], - settings: Settings? = nil, - coreDataModels: [CoreDataModel] = [], - environment: [String: String] = [:] - ) -> Target { - Target( - name: name, - platform: platform, - product: product, - productName: productName, - bundleId: bundleId, - infoPlist: infoPlist, - sources: sources, - resources: resources, - headers: headers, - entitlements: entitlements, - scripts: scripts, - dependencies: dependencies, - settings: settings, - coreDataModels: coreDataModels, - environmentVariables: environment.mapValues { .init(stringLiteral: $0) } - ) - } -} - -extension TargetScript { - public static func test( - name: String = "Action", - tool: String = "", - order: Order = .pre, - arguments: [String] = [], - inputPaths: [FileListGlob] = [], - inputFileListPaths: [Path] = [], - outputPaths: [Path] = [], - outputFileListPaths: [Path] = [], - dependencyFile: Path? = nil - ) -> TargetScript { - TargetScript( - name: name, - script: .tool(path: tool, args: arguments), - order: order, - inputPaths: inputPaths, - inputFileListPaths: inputFileListPaths, - outputPaths: outputPaths, - outputFileListPaths: outputFileListPaths, - dependencyFile: dependencyFile - ) - } -} - -extension Scheme { - public static func test( - name: String = "Scheme", - shared: Bool = false, - buildAction: BuildAction? = nil, - testAction: TestAction? = nil, - runAction: RunAction? = nil - ) -> Scheme { - Scheme( - name: name, - shared: shared, - buildAction: buildAction, - testAction: testAction, - runAction: runAction - ) - } -} - -extension BuildAction { - public static func test(targets: [TargetReference] = []) -> BuildAction { - BuildAction( - targets: targets, - preActions: [ExecutionAction.test()], - postActions: [ExecutionAction.test()] - ) - } -} - -extension TestAction { - public static func test( - targets: [TestableTarget] = [], - arguments: Arguments? = nil, - configuration: ConfigurationName = .debug, - coverage: Bool = true - ) -> TestAction { - TestAction.targets( - targets, - arguments: arguments, - configuration: configuration, - preActions: [ExecutionAction.test()], - postActions: [ExecutionAction.test()], - options: .options(coverage: coverage) - ) - } -} - -extension RunAction { - public static func test( - configuration: ConfigurationName = .debug, - executable: TargetReference? = nil, - arguments: Arguments? = nil - ) -> RunAction { - RunAction( - configuration: configuration, - executable: executable, - arguments: arguments - ) - } -} - -extension ExecutionAction { - public static func test( - title: String = "Test Script", - scriptText: String = "echo Test", - target: TargetReference? = TargetReference(projectPath: nil, target: "Target") - ) -> ExecutionAction { - ExecutionAction( - title: title, - scriptText: scriptText, - target: target - ) - } -} - -extension Arguments { - public static func test( - environment: [String: String] = [:], - launchArguments: [LaunchArgument] = [] - ) -> Arguments { - Arguments( - environmentVariables: environment.mapValues { .init(stringLiteral: $0) }, - launchArguments: launchArguments - ) - } -} - -extension Dependencies { - public static func test(carthageDependencies: CarthageDependencies? = nil) -> Dependencies { - Dependencies(carthage: carthageDependencies) - } -} - -extension Plugin { - public static func test(name: String = "Plugin") -> Plugin { - Plugin(name: name) - } -} - -extension PackageSettings { - public static func test() -> PackageSettings { - PackageSettings() - } -} diff --git a/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilder.swift b/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilder.swift index 9cb405d70b6..c3e2207dbac 100644 --- a/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilder.swift +++ b/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilder.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistSupport @testable import TuistLoader diff --git a/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilderFactory.swift b/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilderFactory.swift index 21464df7c70..a252b27946b 100644 --- a/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilderFactory.swift +++ b/Sources/TuistLoaderTesting/ProjectDescriptionHelpers/MockProjectDescriptionHelpersBuilderFactory.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph @testable import TuistLoader diff --git a/Sources/TuistLoaderTesting/Utils/Mocks/MockHelpersDirectoryLocator.swift b/Sources/TuistLoaderTesting/Utils/Mocks/MockHelpersDirectoryLocator.swift index 0343a32d255..24173d4c4d2 100644 --- a/Sources/TuistLoaderTesting/Utils/Mocks/MockHelpersDirectoryLocator.swift +++ b/Sources/TuistLoaderTesting/Utils/Mocks/MockHelpersDirectoryLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path @testable import TuistLoader public final class MockHelpersDirectoryLocator: HelpersDirectoryLocating { diff --git a/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift b/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift deleted file mode 100644 index e99ec97d4fd..00000000000 --- a/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistLoader - -public final class MockManifestFilesLocator: ManifestFilesLocating { - public var locateManifestsArgs: [AbsolutePath] = [] - public var locateManifestsStub: [(Manifest, AbsolutePath)]? - public var locateProjectManifestsStub: ((AbsolutePath, [String], Bool) -> [ManifestFilesLocator.ProjectManifest])? - public var locatePluginManifestsStub: [AbsolutePath]? - public var locatePluginManifestsArgs: [AbsolutePath] = [] - public var locateConfigStub: AbsolutePath? - public var locateConfigArgs: [AbsolutePath] = [] - public var locateDependenciesStub: AbsolutePath? - public var locateDependenciesArgs: [AbsolutePath] = [] - public var locatePackageManifestStub: AbsolutePath? - public var locatePackageManifestArgs: [AbsolutePath] = [] - - public init() {} - - public func locateManifests(at: AbsolutePath) -> [(Manifest, AbsolutePath)] { - locateManifestsArgs.append(at) - return locateManifestsStub ?? [(.project, at.appending(component: "Project.swift"))] - } - - public func locatePluginManifests( - at: AbsolutePath, - excluding _: [String], - onlyCurrentDirectory _: Bool - ) -> [AbsolutePath] { - locatePluginManifestsArgs.append(at) - return locatePluginManifestsStub ?? [at.appending(component: "Plugin.swift")] - } - - public func locateProjectManifests( - at locatingPath: AbsolutePath, - excluding: [String], - onlyCurrentDirectory: Bool - ) -> [ManifestFilesLocator.ProjectManifest] { - locateProjectManifestsStub?(locatingPath, excluding, onlyCurrentDirectory) ?? [ - ManifestFilesLocator.ProjectManifest( - manifest: .project, - path: locatingPath.appending(component: "Project.swift") - ), - ] - } - - public func locateConfig(at: AbsolutePath) -> AbsolutePath? { - locateConfigArgs.append(at) - return locateConfigStub ?? at.appending(components: "Tuist", "Config.swift") - } - - public func locateDependencies(at: AbsolutePath) -> AbsolutePath? { - 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/TuistLoaderTesting/Utils/Mocks/MockResourceLocator.swift b/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceLocator.swift index 8bc29ae1a55..8ac14413d6d 100644 --- a/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceLocator.swift +++ b/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path @testable import TuistLoader public final class MockResourceLocator: ResourceLocating { diff --git a/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceSynthesizerPathLocator.swift b/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceSynthesizerPathLocator.swift index 745e0c1bd18..3ce9c86d88c 100644 --- a/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceSynthesizerPathLocator.swift +++ b/Sources/TuistLoaderTesting/Utils/Mocks/MockResourceSynthesizerPathLocator.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore @testable import TuistLoader public final class MockResourceSynthesizerPathLocator: ResourceSynthesizerPathLocating { @@ -24,7 +24,7 @@ public final class MockResourceSynthesizerPathLocator: ResourceSynthesizerPathLo } public var locateStub: ((AbsolutePath) -> AbsolutePath?)? - public func locate(at: TSCBasic.AbsolutePath) -> TSCBasic.AbsolutePath? { + public func locate(at: Path.AbsolutePath) -> Path.AbsolutePath? { locateStub?(at) } } diff --git a/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift b/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift index 9222639d991..404a717898c 100644 --- a/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift +++ b/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift @@ -1,6 +1,6 @@ import Foundation +import Path import PathKit -import TSCBasic import TuistSupport import XcodeProj @@ -58,7 +58,7 @@ public class EmptyBuildSettingsChecker: EmptyBuildSettingsChecking { let nonEmptyBuildSettings = buildConfigurations.compactMap { config -> String? in if config.buildSettings.isEmpty { return nil } for (key, _) in config.buildSettings { - logger.info("The build setting '\(key)' of build configuration '\(config.name)' is not empty.") + logger.notice("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 d1ae3db8c7a..f009afc12d5 100644 --- a/Sources/TuistMigration/Utilities/SettingsToXCConfigExtractor.swift +++ b/Sources/TuistMigration/Utilities/SettingsToXCConfigExtractor.swift @@ -1,6 +1,6 @@ import Foundation +import Path import PathKit -import TSCBasic import TuistSupport import XcodeProj @@ -50,7 +50,7 @@ public class SettingsToXCConfigExtractor: SettingsToXCConfigExtracting { let buildConfigurations = try buildConfigurations(pbxproj: pbxproj, targetName: targetName) if buildConfigurations.isEmpty { - logger.info("The list of configurations is empty. Exiting...") + logger.notice("The list of configurations is empty. Exiting...") return } @@ -92,7 +92,7 @@ public class SettingsToXCConfigExtractor: SettingsToXCConfigExtracting { buildSettingsLines.sorted().joined(separator: "\n"), ].joined(separator: "\n\n") try FileHandler.shared.write(buildSettingsContent, path: xcconfigPath, atomically: true) - logger.info("Build settings successfully extracted into \(xcconfigPath.pathString)", metadata: .success) + logger.notice("Build settings successfully extracted into \(xcconfigPath.pathString)", metadata: .success) } private func buildConfigurations(pbxproj: PBXProj, targetName: String?) throws -> [XCBuildConfiguration] { diff --git a/Sources/TuistMigration/Utilities/TargetsExtractor.swift b/Sources/TuistMigration/Utilities/TargetsExtractor.swift index 62a01ac4275..850018c97d3 100644 --- a/Sources/TuistMigration/Utilities/TargetsExtractor.swift +++ b/Sources/TuistMigration/Utilities/TargetsExtractor.swift @@ -1,11 +1,11 @@ import Foundation -import PathKit +import Path import TSCBasic import TuistSupport import XcodeProj public enum TargetsExtractorError: FatalError, Equatable { - case missingXcodeProj(AbsolutePath) + case missingXcodeProj(Path.AbsolutePath) case noTargets case failedToExtractTargets(String) case failedToEncode @@ -37,7 +37,7 @@ public enum TargetsExtractorError: FatalError, Equatable { public protocol TargetsExtracting { /// - Parameters: /// - xcodeprojPath: Path to the Xcode project. - func targetsSortedByDependencies(xcodeprojPath: AbsolutePath) throws -> [TargetDependencyCount] + func targetsSortedByDependencies(xcodeprojPath: Path.AbsolutePath) throws -> [TargetDependencyCount] } public struct TargetDependencyCount: Encodable { @@ -53,9 +53,9 @@ public final class TargetsExtractor: TargetsExtracting { // MARK: - EmptyBuildSettingsChecking - public func targetsSortedByDependencies(xcodeprojPath: AbsolutePath) throws -> [TargetDependencyCount] { + public func targetsSortedByDependencies(xcodeprojPath: Path.AbsolutePath) throws -> [TargetDependencyCount] { guard FileHandler.shared.exists(xcodeprojPath) else { throw TargetsExtractorError.missingXcodeProj(xcodeprojPath) } - let pbxproj = try XcodeProj(path: Path(xcodeprojPath.pathString)).pbxproj + let pbxproj = try XcodeProj(pathString: xcodeprojPath.pathString).pbxproj let targets = pbxproj.nativeTargets + pbxproj.aggregateTargets + pbxproj.legacyTargets if targets.isEmpty { throw TargetsExtractorError.noTargets diff --git a/Sources/TuistMigrationTesting/Utilities/MockEmptyBuildSettingsChecker.swift b/Sources/TuistMigrationTesting/Utilities/MockEmptyBuildSettingsChecker.swift index 806780bbc17..4a1c1dd62a5 100644 --- a/Sources/TuistMigrationTesting/Utilities/MockEmptyBuildSettingsChecker.swift +++ b/Sources/TuistMigrationTesting/Utilities/MockEmptyBuildSettingsChecker.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistMigration public class MockEmptyBuildSettingsChecker: EmptyBuildSettingsChecking { diff --git a/Sources/TuistPlugin/PluginService.swift b/Sources/TuistPlugin/PluginService.swift index 662e9d92079..7c8c32c4a4a 100644 --- a/Sources/TuistPlugin/PluginService.swift +++ b/Sources/TuistPlugin/PluginService.swift @@ -1,7 +1,7 @@ +import FileSystem import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader import TuistScaffold import TuistSupport @@ -29,7 +29,7 @@ enum PluginServiceError: FatalError, Equatable { var description: String { switch self { case let .missingRemotePlugins(plugins): - return "Remote plugins \(plugins.joined(separator: ", ")) have not been fetched. Try running tuist fetch." + return "Remote plugins \(plugins.joined(separator: ", ")) have not been fetched. Try running 'tuist install'." case let .invalidURL(url): return "Invalid URL for the plugin's Github repository: \(url)." } @@ -69,6 +69,7 @@ public final class PluginService: PluginServicing { private let cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring private let fileArchivingFactory: FileArchivingFactorying private let fileClient: FileClienting + private let fileSystem: FileSystem /// Creates a `PluginService`. /// - Parameters: @@ -86,7 +87,8 @@ public final class PluginService: PluginServicing { gitHandler: GitHandling = GitHandler(), cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring = CacheDirectoriesProviderFactory(), fileArchivingFactory: FileArchivingFactorying = FileArchivingFactory(), - fileClient: FileClienting = FileClient() + fileClient: FileClienting = FileClient(), + fileSystem: FileSystem = FileSystem() ) { self.manifestLoader = manifestLoader self.templatesDirectoryLocator = templatesDirectoryLocator @@ -95,6 +97,7 @@ public final class PluginService: PluginServicing { self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory self.fileArchivingFactory = fileArchivingFactory self.fileClient = fileClient + self.fileSystem = fileSystem } public func remotePluginPaths(using config: Config) throws -> [RemotePluginPaths] { @@ -150,12 +153,12 @@ public final class PluginService: PluginServicing { return nil } } - let localPluginManifests = try localPluginPaths.map(manifestLoader.loadPlugin) + let localPluginManifests = try await localPluginPaths.concurrentMap(manifestLoader.loadPlugin) let remotePluginPaths = try remotePluginPaths(using: config) let remotePluginRepositoryPaths = remotePluginPaths.map(\.repositoryPath) - let remotePluginManifests = try remotePluginRepositoryPaths - .map(manifestLoader.loadPlugin) + let remotePluginManifests = try await remotePluginRepositoryPaths + .concurrentMap(manifestLoader.loadPlugin) let pluginPaths = localPluginPaths + remotePluginRepositoryPaths let missingRemotePlugins = zip(remotePluginManifests, remotePluginRepositoryPaths) .filter { !FileHandler.shared.exists($0.1) } @@ -237,10 +240,10 @@ public final class PluginService: PluginServicing { private func pluginCacheDirectory( url: String, gitId: String, - config: Config + config _: Config ) throws -> AbsolutePath { - let cacheDirectories = try cacheDirectoryProviderFactory.cacheDirectories(config: config) - let cacheDirectory = cacheDirectories.cacheDirectory(for: .plugins) + let cacheDirectories = try cacheDirectoryProviderFactory.cacheDirectories() + let cacheDirectory = try cacheDirectories.cacheDirectory(for: .plugins) let fingerprint = "\(url)-\(gitId)".md5 return cacheDirectory .appending(component: fingerprint) @@ -271,7 +274,7 @@ public final class PluginService: PluginServicing { let pluginRepositoryDirectory = pluginCacheDirectory.appending(component: PluginServiceConstants.repository) // If `Package.swift` exists for the plugin, a Github release should for the given `gitTag` should also exist guard FileHandler.shared - .exists(pluginRepositoryDirectory.appending(component: Constants.DependenciesDirectory.packageSwiftName)) + .exists(pluginRepositoryDirectory.appending(component: Constants.SwiftPackageManager.packageSwiftName)) else { return } let pluginReleaseDirectory = pluginCacheDirectory.appending(component: PluginServiceConstants.release) @@ -280,7 +283,7 @@ public final class PluginService: PluginServicing { return } - let plugin = try manifestLoader.loadPlugin(at: pluginRepositoryDirectory) + let plugin = try await manifestLoader.loadPlugin(at: pluginRepositoryDirectory) guard let releaseURL = getPluginDownloadUrl(gitUrl: url, gitTag: gitTag, pluginName: plugin.name, releaseUrl: releaseUrl) else { throw PluginServiceError.invalidURL(url) } @@ -290,37 +293,44 @@ public final class PluginService: PluginServicing { // Currently, we assume the release path exists. let downloadPath = try await self.fileClient.download(url: releaseURL) let downloadZipPath = downloadPath.removingLastComponent().appending(component: "release.zip") - defer { - try? FileHandler.shared.delete(downloadPath) - try? FileHandler.shared.delete(downloadZipPath) - } - if FileHandler.shared.exists(downloadZipPath) { - try FileHandler.shared.delete(downloadZipPath) - } - try FileHandler.shared.move(from: downloadPath, to: downloadZipPath) - - // Unzip let fileUnarchiver = try self.fileArchivingFactory.makeFileUnarchiver(for: downloadZipPath) - let unarchivedContents = try FileHandler.shared.contentsOfDirectory( - try fileUnarchiver.unzip() - ) - defer { - try? fileUnarchiver.delete() - } - try FileHandler.shared.createFolder(pluginReleaseDirectory) - for unarchivedContent in unarchivedContents { - try FileHandler.shared.move( - from: unarchivedContent, - to: pluginReleaseDirectory.appending(component: unarchivedContent.basename) + + var thrownError: Error? + + do { + if FileHandler.shared.exists(downloadZipPath) { + try await self.fileSystem.remove(downloadZipPath) + } + try FileHandler.shared.move(from: downloadPath, to: downloadZipPath) + + // Unzip + let unarchivedContents = try FileHandler.shared.contentsOfDirectory( + try fileUnarchiver.unzip() ) - } - // Mark files as executables (this information is lost during (un)archiving) - try FileHandler.shared.contentsOfDirectory(pluginReleaseDirectory) - .filter { $0.basename.hasPrefix("tuist-") } - .forEach { - try System.shared.chmod(.executable, path: $0, options: [.onlyFiles]) + try FileHandler.shared.createFolder(pluginReleaseDirectory) + for unarchivedContent in unarchivedContents { + try FileHandler.shared.move( + from: unarchivedContent, + to: pluginReleaseDirectory.appending(component: unarchivedContent.basename) + ) } + + // Mark files as executables (this information is lost during (un)archiving) + try FileHandler.shared.contentsOfDirectory(pluginReleaseDirectory) + .filter { $0.basename.hasPrefix("tuist-") } + .forEach { + try System.shared.chmod(.executable, path: $0, options: [.onlyFiles]) + } + } catch { + thrownError = error + } + + try? await fileUnarchiver.delete() + try? await self.fileSystem.remove(downloadPath) + try? await self.fileSystem.remove(downloadZipPath) + + if let thrownError { throw thrownError } } } diff --git a/Sources/TuistPluginTesting/MockPluginService.swift b/Sources/TuistPluginTesting/MockPluginService.swift index b053e4cc6dc..3849e1f12bb 100644 --- a/Sources/TuistPluginTesting/MockPluginService.swift +++ b/Sources/TuistPluginTesting/MockPluginService.swift @@ -1,4 +1,4 @@ -import TuistGraph +import TuistCore import TuistPlugin public final class MockPluginService: PluginServicing { diff --git a/Sources/TuistScaffold/TemplateGenerator.swift b/Sources/TuistScaffold/TemplateGenerator.swift index 0d923c75bcb..79313b14c1f 100644 --- a/Sources/TuistScaffold/TemplateGenerator.swift +++ b/Sources/TuistScaffold/TemplateGenerator.swift @@ -1,9 +1,9 @@ +import FileSystem import Foundation +import Path import PathKit import StencilSwiftKit -import TSCBasic import TuistCore -import TuistGraph import TuistSupport /// Interface for generating content defined in template manifest @@ -16,19 +16,23 @@ public protocol TemplateGenerating { func generate( template: Template, to destinationPath: AbsolutePath, - attributes: [String: String] - ) throws + attributes: [String: Template.Attribute.Value] + ) async throws } public final class TemplateGenerator: TemplateGenerating { + private let fileSystem: FileSystem + // Public initializer - public init() {} + public init(fileSystem: FileSystem = FileSystem()) { + self.fileSystem = fileSystem + } public func generate( template: Template, to destinationPath: AbsolutePath, - attributes: [String: String] - ) throws { + attributes: [String: Template.Attribute.Value] + ) async throws { let renderedItems = try renderItems( template: template, attributes: attributes @@ -38,7 +42,7 @@ public final class TemplateGenerator: TemplateGenerating { destinationPath: destinationPath ) - try generateItems( + try await generateItems( renderedItems: renderedItems, attributes: attributes, destinationPath: destinationPath @@ -50,13 +54,13 @@ public final class TemplateGenerator: TemplateGenerating { /// Renders items' paths in format path_to_dir/{{ attribute_name }} with `attributes` private func renderItems( template: Template, - attributes: [String: String] + attributes: [String: Template.Attribute.Value] ) throws -> [Template.Item] { let environment = stencilSwiftEnvironment() return try template.items.map { let renderedPathString = try environment.renderTemplate( string: $0.path.pathString, - context: attributes + context: attributes.toStringAny ) let path = try RelativePath(validating: renderedPathString) @@ -64,7 +68,7 @@ public final class TemplateGenerator: TemplateGenerating { if case let Template.Contents.file(path) = contents { let renderedPathString = try environment.renderTemplate( string: path.pathString, - context: attributes + context: attributes.toStringAny ) contents = .file( try AbsolutePath(validating: renderedPathString) @@ -73,7 +77,7 @@ public final class TemplateGenerator: TemplateGenerating { if case let Template.Contents.directory(path) = contents { let renderedPathString = try environment.renderTemplate( string: path.pathString, - context: attributes + context: attributes.toStringAny ) contents = .directory( try AbsolutePath(validating: renderedPathString) @@ -103,9 +107,9 @@ public final class TemplateGenerator: TemplateGenerating { /// Generate all `renderedItems` private func generateItems( renderedItems: [Template.Item], - attributes: [String: String], + attributes: [String: Template.Attribute.Value], destinationPath: AbsolutePath - ) throws { + ) async throws { let environment = stencilSwiftEnvironment() for renderedItem in renderedItems { let renderedContents: String? @@ -113,7 +117,7 @@ public final class TemplateGenerator: TemplateGenerating { case let .string(contents): renderedContents = try environment.renderTemplate( string: contents, - context: attributes + context: attributes.toStringAny ) case let .file(path): let injectedLoaderEnvironment = stencilSwiftEnvironment(templatePaths: [Path(path.dirname)]) @@ -122,7 +126,7 @@ public final class TemplateGenerator: TemplateGenerating { if path.extension == "stencil" { renderedContents = try injectedLoaderEnvironment.renderTemplate( string: fileContents, - context: attributes + context: attributes.toStringAny ) } else { renderedContents = fileContents @@ -136,7 +140,7 @@ public final class TemplateGenerator: TemplateGenerating { try FileHandler.shared.createFolder(destinationDirectoryPath.parentDirectory) } if FileHandler.shared.exists(destinationDirectoryPath) { - try FileHandler.shared.delete(destinationDirectoryPath) + try await fileSystem.remove(destinationDirectoryPath) } try FileHandler.shared.copy(from: path, to: destinationDirectoryPath) renderedContents = nil @@ -155,3 +159,13 @@ public final class TemplateGenerator: TemplateGenerating { } } } + +extension [String: Template.Attribute.Value] { + fileprivate var toStringAny: [String: Any] { + reduce([:]) { partialResult, attribute in + var result: [String: Any] = partialResult + result[attribute.key] = attribute.value.rawValue + return result + } + } +} diff --git a/Sources/TuistScaffold/TemplatesDirectoryLocator.swift b/Sources/TuistScaffold/TemplatesDirectoryLocator.swift index a7e82b7f2bd..bf9322d1ee9 100644 --- a/Sources/TuistScaffold/TemplatesDirectoryLocator.swift +++ b/Sources/TuistScaffold/TemplatesDirectoryLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport @@ -32,7 +32,7 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { public func locateTuistTemplates() -> AbsolutePath? { #if DEBUG let maybeBundlePath: AbsolutePath? - if let sourceRoot = ProcessEnv.vars["TUIST_CONFIG_SRCROOT"] { + if let sourceRoot = ProcessInfo.processInfo.environment["TUIST_CONFIG_SRCROOT"] { maybeBundlePath = try? AbsolutePath(validating: sourceRoot).appending(component: "Templates") } else { // Used only for debug purposes to find templates in your tuist working directory @@ -55,10 +55,9 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { bin/ tuist share/ - tuist/ - Templates + Templates */ - bundlePath.parentDirectory.appending(try! RelativePath(validating: "share/tuist")), + bundlePath.parentDirectory.appending(try! RelativePath(validating: "share")), // swiftlint:disable:previous force_try ] let candidates = paths.map { path in diff --git a/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplateGenerator.swift b/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplateGenerator.swift index 426bda08843..7027ca5eea9 100644 --- a/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplateGenerator.swift +++ b/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplateGenerator.swift @@ -1,14 +1,17 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore -@testable import TuistCore @testable import TuistScaffold public final class MockTemplateGenerator: TemplateGenerating { - public var generateStub: ((Template, AbsolutePath, [String: String]) throws -> Void)? + public var generateStub: ((Template, AbsolutePath, [String: TuistCore.Template.Attribute.Value]) throws -> Void)? - public func generate(template: Template, to destinationPath: AbsolutePath, attributes: [String: String]) throws { + public func generate( + template: Template, + to destinationPath: AbsolutePath, + attributes: [String: TuistCore.Template.Attribute.Value] + ) throws { try generateStub?(template, destinationPath, attributes) } } diff --git a/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift b/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift index 8a897c273b5..60ec2db63fb 100644 --- a/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift +++ b/Sources/TuistScaffoldTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path @testable import TuistScaffold public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating { diff --git a/Sources/TuistServer/Cache/CacheAnalytics.swift b/Sources/TuistServer/Cache/CacheAnalytics.swift new file mode 100644 index 00000000000..733ea23e02a --- /dev/null +++ b/Sources/TuistServer/Cache/CacheAnalytics.swift @@ -0,0 +1,31 @@ +import Foundation +import Mockable +import Path +import TuistSupport +import XcodeGraph + +@Mockable +public protocol CacheAnalyticsStoring: AnyObject { + var localCacheTargetsHits: [String] { get set } + var remoteCacheTargetsHits: [String] { get set } + var cacheableTargets: [String] { get set } + var testTargets: [String] { get set } + var localTestTargetHits: [String] { get set } + var remoteTestTargetHits: [String] { get set } + /// Map of a relative path of a project that has a map of a target name to its hash + var targetHashes: [GraphTarget: String] { get set } + var graphPath: AbsolutePath? { get set } +} + +public final class CacheAnalyticsStore: CacheAnalyticsStoring { + public var localCacheTargetsHits: [String] = [] + public var remoteCacheTargetsHits: [String] = [] + public var cacheableTargets: [String] = [] + public var testTargets: [String] = [] + public var localTestTargetHits: [String] = [] + public var remoteTestTargetHits: [String] = [] + public var targetHashes: [GraphTarget: String] = [:] + public var graphPath: AbsolutePath? + + public static let shared = CacheAnalyticsStore() +} diff --git a/Sources/TuistServer/Cache/CacheStorageFactorying.swift b/Sources/TuistServer/Cache/CacheStorageFactorying.swift new file mode 100644 index 00000000000..14073faf33c --- /dev/null +++ b/Sources/TuistServer/Cache/CacheStorageFactorying.swift @@ -0,0 +1,10 @@ +import Foundation +import Mockable +import TuistCore +import TuistSupport +import XcodeGraph + +@Mockable +public protocol CacheStorageFactorying { + func cacheStorage(config: Config) throws -> CacheStoring +} diff --git a/Sources/TuistServer/Cache/CacheStoring.swift b/Sources/TuistServer/Cache/CacheStoring.swift new file mode 100644 index 00000000000..f5497fbbd5f --- /dev/null +++ b/Sources/TuistServer/Cache/CacheStoring.swift @@ -0,0 +1,76 @@ +import Foundation +import Mockable +import Path +import TuistCore +import XcodeGraph + +public struct CacheStorableTarget: Hashable, Equatable { + public let target: GraphTarget + public let hash: String + + public var name: String { target.target.name } + + public init(target: GraphTarget, hash: String) { + self.target = target + self.hash = hash + } + + public func hash(into hasher: inout Hasher) { + hasher.combine("cache-storable") + hasher.combine(hash) + } +} + +public struct CacheStorableItem: Hashable, Equatable { + public let name: String + public let hash: String + public init(name: String, hash: String) { + self.name = name + self.hash = hash + } + + public func hash(into hasher: inout Hasher) { + hasher.combine("cache-storable") + hasher.combine(hash) + } +} + +@Mockable +public protocol CacheStoring { + func fetch( + _ items: Set, + cacheCategory: RemoteCacheCategory + ) async throws -> [CacheStorableItem: AbsolutePath] + func store( + _ items: [CacheStorableItem: [AbsolutePath]], + cacheCategory: RemoteCacheCategory + ) async throws +} + +extension CacheStoring { + public func fetch( + _ targets: Set, + cacheCategory: RemoteCacheCategory + ) async throws -> [CacheStorableTarget: AbsolutePath] { + Dictionary( + uniqueKeysWithValues: try await fetch( + Set(targets.map { CacheStorableItem(name: $0.name, hash: $0.hash) }), + cacheCategory: cacheCategory + ) + .compactMap { item, path -> (CacheStorableTarget, AbsolutePath)? in + guard let target = targets.first(where: { $0.hash == item.hash }) else { return nil } + return (target, path) + } + ) + } + + public func store( + _ targets: [CacheStorableTarget: [AbsolutePath]], + cacheCategory: RemoteCacheCategory + ) async throws { + let items = Dictionary(uniqueKeysWithValues: targets.map { target, paths -> (CacheStorableItem, [AbsolutePath]) in + (CacheStorableItem(name: target.name, hash: target.hash), paths) + }) + try await store(items, cacheCategory: cacheCategory) + } +} diff --git a/Sources/TuistServer/Cache/EmptyCacheStorage.swift b/Sources/TuistServer/Cache/EmptyCacheStorage.swift new file mode 100644 index 00000000000..520c4d9178c --- /dev/null +++ b/Sources/TuistServer/Cache/EmptyCacheStorage.swift @@ -0,0 +1,17 @@ +import Foundation +import Path +import TuistCore + +/// Empty `CacheStoring` implementation as we noop cache storing in the opensource repository +public final class EmptyCacheStorage: CacheStoring { + public init() {} + + public func fetch( + _: Set, + cacheCategory _: RemoteCacheCategory + ) async throws -> [CacheStorableItem: AbsolutePath] { + [:] + } + + public func store(_: [CacheStorableItem: [AbsolutePath]], cacheCategory _: RemoteCacheCategory) async throws {} +} diff --git a/Sources/TuistServer/Cache/EmptyCacheStorageFactory.swift b/Sources/TuistServer/Cache/EmptyCacheStorageFactory.swift new file mode 100644 index 00000000000..3cdb04c36a2 --- /dev/null +++ b/Sources/TuistServer/Cache/EmptyCacheStorageFactory.swift @@ -0,0 +1,10 @@ +import Foundation +import TuistCore + +public final class EmptyCacheStorageFactory: CacheStorageFactorying { + public init() {} + + public func cacheStorage(config _: Config) throws -> any CacheStoring { + EmptyCacheStorage() + } +} diff --git a/Sources/TuistServer/Client/Client+Server.swift b/Sources/TuistServer/Client/Client+Server.swift new file mode 100644 index 00000000000..a3282402690 --- /dev/null +++ b/Sources/TuistServer/Client/Client+Server.swift @@ -0,0 +1,35 @@ +import Foundation +import OpenAPIRuntime +import OpenAPIURLSession + +extension Client { + private static let commonMiddlewares: [any ClientMiddleware] = [ + ServerClientRequestIdMiddleware(), + ServerClientVerboseLoggingMiddleware(), + ServerClientOutputWarningsMiddleware(), + ] + + /// Tuist client for authenticated sessions + static func authenticated(serverURL: URL) -> Client { + .init( + serverURL: serverURL, + transport: URLSessionTransport(configuration: .init(session: .tuistShared)), + middlewares: commonMiddlewares + [ + ServerClientRequestIdMiddleware(), + ServerClientCLIMetadataHeadersMiddleware(), + ServerClientAuthenticationMiddleware(), + ServerClientVerboseLoggingMiddleware(), + ServerClientOutputWarningsMiddleware(), + ] + ) + } + + /// Tuist client for unauthenticated sessions + static func unauthenticated(serverURL: URL) -> Client { + .init( + serverURL: serverURL, + transport: URLSessionTransport(configuration: .init(session: .tuistShared)), + middlewares: commonMiddlewares + ) + } +} diff --git a/Sources/TuistServer/Client/ServerClientAuthenticationMiddleware.swift b/Sources/TuistServer/Client/ServerClientAuthenticationMiddleware.swift new file mode 100644 index 00000000000..7954b62d3c3 --- /dev/null +++ b/Sources/TuistServer/Client/ServerClientAuthenticationMiddleware.swift @@ -0,0 +1,110 @@ +import Foundation +import OpenAPIRuntime +import TuistSupport + +public enum ServerClientAuthenticationError: FatalError, Equatable { + case notAuthenticated + + public var type: ErrorType { + switch self { + case .notAuthenticated: + return .abort + } + } + + public var description: String { + switch self { + case .notAuthenticated: + return "No valid Tuist credentials found. Authenticate by running `tuist auth`." + } + } +} + +/// Injects an authorization header to every request. +struct ServerClientAuthenticationMiddleware: ClientMiddleware { + private let serverAuthenticationController: ServerAuthenticationControlling + private let serverCredentialsStore: ServerCredentialsStoring + private let refreshAuthTokenService: RefreshAuthTokenServicing + private let dateService: DateServicing + + init() { + self.init( + serverAuthenticationController: ServerAuthenticationController(), + serverCredentialsStore: ServerCredentialsStore(), + refreshAuthTokenService: RefreshAuthTokenService(), + dateService: DateService() + ) + } + + init( + serverAuthenticationController: ServerAuthenticationControlling, + serverCredentialsStore: ServerCredentialsStoring, + refreshAuthTokenService: RefreshAuthTokenServicing, + dateService: DateServicing + ) { + self.serverAuthenticationController = serverAuthenticationController + self.serverCredentialsStore = serverCredentialsStore + self.refreshAuthTokenService = refreshAuthTokenService + self.dateService = dateService + } + + func intercept( + _ request: Request, + baseURL: URL, + operationID _: String, + next: (Request, URL) async throws -> Response + ) async throws -> Response { + var request = request + guard let token = try serverAuthenticationController.authenticationToken(serverURL: baseURL) + else { + throw ServerClientAuthenticationError.notAuthenticated + } + + let tokenValue: String + switch token { + case let .project(token): + tokenValue = token + case let .user(legacyToken: legacyToken, accessToken: accessToken, refreshToken: refreshToken): + if let legacyToken { + tokenValue = legacyToken + } else if let accessToken, let refreshToken { + // We consider a token to be expired if the expiration date is in the past or 30 seconds from now + let isExpired = accessToken.expiryDate + .timeIntervalSince(dateService.now()) < 30 + + if isExpired { + do { + let newTokens = try await RetryProvider() + .runWithRetries { + return try await refreshAuthTokenService.refreshTokens( + serverURL: baseURL, + refreshToken: refreshToken.token + ) + } + try serverCredentialsStore + .store( + credentials: ServerCredentials( + token: nil, + accessToken: newTokens.accessToken, + refreshToken: newTokens.refreshToken + ), + serverURL: baseURL + ) + tokenValue = newTokens.accessToken + } catch { + throw ServerClientAuthenticationError.notAuthenticated + } + } else { + tokenValue = accessToken.token + } + } else { + throw ServerClientAuthenticationError.notAuthenticated + } + } + + request.headerFields.append(.init( + name: "Authorization", value: "Bearer \(tokenValue)" + )) + return try await next(request, baseURL) + } +} diff --git a/Sources/TuistServer/Client/ServerClientCLIMetadataHeadersMiddleware.swift b/Sources/TuistServer/Client/ServerClientCLIMetadataHeadersMiddleware.swift new file mode 100644 index 00000000000..4c74de3d24a --- /dev/null +++ b/Sources/TuistServer/Client/ServerClientCLIMetadataHeadersMiddleware.swift @@ -0,0 +1,21 @@ +import Foundation +import OpenAPIRuntime +import TuistSupport + +/// This middleware includes the release date of the CLI in the headers so that we can show +/// warnings if the on-premise installation is too old. +struct ServerClientCLIMetadataHeadersMiddleware: ClientMiddleware { + let releaseDate = "2024.08.19" + + func intercept( + _ request: Request, + baseURL: URL, + operationID _: String, + next: (Request, URL) async throws -> Response + ) async throws -> Response { + var request = request + request.headerFields.append(.init(name: "x-tuist-cli-release-date", value: releaseDate)) + request.headerFields.append(.init(name: "x-tuist-cli-version", value: Constants.version)) + return try await next(request, baseURL) + } +} diff --git a/Sources/TuistServer/Client/ServerClientOutputWarningsMiddleware.swift b/Sources/TuistServer/Client/ServerClientOutputWarningsMiddleware.swift new file mode 100644 index 00000000000..4643e3b4299 --- /dev/null +++ b/Sources/TuistServer/Client/ServerClientOutputWarningsMiddleware.swift @@ -0,0 +1,62 @@ +import Foundation +import OpenAPIRuntime +import TuistSupport + +enum CloudClientOutputWarningsMiddlewareError: FatalError { + var type: TuistSupport.ErrorType { + switch self { + case .couldntConvertToData: + return .bug + case .invalidSchema: + return .bug + } + } + + var description: String { + switch self { + case .couldntConvertToData: + "We couldn't convert Tuist warnings into a data instance" + case .invalidSchema: + "The Tuist warnings returned by the server have an unexpected schema" + } + } + + case couldntConvertToData + case invalidSchema +} + +/// A middleware that gets any warning returned in a "x-cloud-warning" header +/// and outputs it to the user. +struct ServerClientOutputWarningsMiddleware: ClientMiddleware { + let warningController: WarningControlling + + init(warningController: WarningControlling = WarningController.shared) { + self.warningController = warningController + } + + func intercept( + _ request: Request, + baseURL: URL, + operationID _: String, + next: (Request, URL) async throws -> Response + ) async throws -> Response { + let response = try await next(request, baseURL) + guard let warnings = response.headerFields.first(where: { $0.name.lowercased() == "x-tuist-cloud-warnings" })?.value + else { + return response + } + guard let warningsData = warnings.data(using: .utf8), let data = Data(base64Encoded: warningsData) else { + throw CloudClientOutputWarningsMiddlewareError.couldntConvertToData + } + + guard let json = try JSONSerialization + .jsonObject(with: data) as? [String] + else { + throw CloudClientOutputWarningsMiddlewareError.invalidSchema + } + + json.forEach { logger.warning("\($0)") } + + return response + } +} diff --git a/Sources/TuistServer/Client/ServerClientRequestIdMiddleware.swift b/Sources/TuistServer/Client/ServerClientRequestIdMiddleware.swift new file mode 100644 index 00000000000..1afede18068 --- /dev/null +++ b/Sources/TuistServer/Client/ServerClientRequestIdMiddleware.swift @@ -0,0 +1,18 @@ +import Foundation +import OpenAPIRuntime +import TuistSupport + +/// A middleware that gets any warning returned in a "x-cloud-warning" header +/// and outputs it to the user. +struct ServerClientRequestIdMiddleware: ClientMiddleware { + func intercept( + _ request: Request, + baseURL: URL, + operationID _: String, + next: (Request, URL) async throws -> Response + ) async throws -> Response { + var request = request + request.headerFields.append(.init(name: "x-request-id", value: UUID().uuidString)) + return try await next(request, baseURL) + } +} diff --git a/Sources/TuistServer/Client/ServerClientVerboseLoggingMiddleware.swift b/Sources/TuistServer/Client/ServerClientVerboseLoggingMiddleware.swift new file mode 100644 index 00000000000..d27dfad46c5 --- /dev/null +++ b/Sources/TuistServer/Client/ServerClientVerboseLoggingMiddleware.swift @@ -0,0 +1,42 @@ +import Foundation +import OpenAPIRuntime +import TuistSupport + +/// A middleware that outputs in debug mode the request and responses sent and received from the server +struct ServerClientVerboseLoggingMiddleware: ClientMiddleware { + func intercept( + _ request: Request, + baseURL: URL, + operationID _: String, + next: (Request, URL) async throws -> Response + ) async throws -> Response { + let requestJsonBody: Any? = if let body = request.body { + try? JSONSerialization.jsonObject(with: body) + } else { + nil + } + + logger.debug(""" + Sending HTTP request to Tuist: + - Method: \(request.method.rawValue) + - URL: \(baseURL.absoluteString) + - Path: \(request.path) + - Query: \(request.query ?? "") + - Body: \(requestJsonBody ?? "") + - Headers: \(request.headerFields) + """) + + let response = try await next(request, baseURL) + + let responseJsonBody: Any? = try? JSONSerialization.jsonObject(with: response.body) + + logger.debug(""" + Received HTTP response from Tuist: + - Status: \(response.statusCode) + - Body: \(responseJsonBody ?? "") + - Headers: \(response.headerFields) + """) + + return response + } +} diff --git a/Sources/TuistServer/Log/Logger.swift b/Sources/TuistServer/Log/Logger.swift new file mode 100644 index 00000000000..0bee730bfa2 --- /dev/null +++ b/Sources/TuistServer/Log/Logger.swift @@ -0,0 +1,3 @@ +import TuistSupport + +let logger = Logger(label: "io.tuist.app") diff --git a/Sources/TuistServer/Models/InvocationRecord.swift b/Sources/TuistServer/Models/InvocationRecord.swift new file mode 100644 index 00000000000..74960422a60 --- /dev/null +++ b/Sources/TuistServer/Models/InvocationRecord.swift @@ -0,0 +1,28 @@ +import Foundation + +struct InvocationRecord: Codable { + let actions: XCObjectArray +} + +struct ActionRecord: Codable { + let schemeCommandName: XCObjectItem + let actionResult: ActionResult +} + +struct ActionResult: Codable { + let testsRef: Reference? + + struct Reference: Codable { + let id: XCObjectItem + } +} + +struct XCObjectArray: Codable { + // swiftlint:disable:next identifier_name + let _values: [T] +} + +struct XCObjectItem: Codable { + // swiftlint:disable:next identifier_name + let _value: T +} diff --git a/Sources/TuistServer/Models/PreviewUpload.swift b/Sources/TuistServer/Models/PreviewUpload.swift new file mode 100644 index 00000000000..9cbafa14c76 --- /dev/null +++ b/Sources/TuistServer/Models/PreviewUpload.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct PreviewUpload { + public let previewId: String + public let uploadId: String +} diff --git a/Sources/TuistServer/Models/SSOOrganization.swift b/Sources/TuistServer/Models/SSOOrganization.swift new file mode 100644 index 00000000000..dbea82785e2 --- /dev/null +++ b/Sources/TuistServer/Models/SSOOrganization.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum SSOOrganization: Codable, Equatable { + case google(String) +} diff --git a/Sources/TuistServer/Models/ServerAuthenticationTokens.swift b/Sources/TuistServer/Models/ServerAuthenticationTokens.swift new file mode 100644 index 00000000000..a7a1a6f6d63 --- /dev/null +++ b/Sources/TuistServer/Models/ServerAuthenticationTokens.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct ServerAuthenticationTokens { + public init(accessToken: String, refreshToken: String) { + self.accessToken = accessToken + self.refreshToken = refreshToken + } + + public let accessToken: String + public let refreshToken: String +} diff --git a/Sources/TuistServer/Models/ServerCacheArtifact.swift b/Sources/TuistServer/Models/ServerCacheArtifact.swift new file mode 100644 index 00000000000..d79e1962b8e --- /dev/null +++ b/Sources/TuistServer/Models/ServerCacheArtifact.swift @@ -0,0 +1,57 @@ +import Foundation +import TuistSupport + +enum ServerCacheArtifactError: FatalError, Equatable { + case invalidURL(String) + + var description: String { + switch self { + case let .invalidURL(url): + return "Invalid URL for the remote cache artifact: \(url)." + } + } + + var type: ErrorType { + switch self { + case .invalidURL: + return .bug + } + } +} + +/// Server cache artifact +public struct ServerCacheArtifact: Codable { + public init( + url: URL, + expiresAt: Int + ) { + self.url = url + self.expiresAt = expiresAt + } + + public let url: URL + public let expiresAt: Int +} + +extension ServerCacheArtifact { + init(_ cacheArtifact: Components.Schemas.CacheArtifactDownloadURL) throws { + guard let url = URL(string: cacheArtifact.data.url) + else { throw ServerCacheArtifactError.invalidURL(cacheArtifact.data.url) } + self.url = url + expiresAt = Int(cacheArtifact.data.expires_at) + } +} + +#if DEBUG + extension ServerCacheArtifact { + public static func test( + url: URL = Constants.URLs.production, + expiresAt: Int = 0 + ) -> Self { + .init( + url: url, + expiresAt: expiresAt + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerCommandEvent.swift b/Sources/TuistServer/Models/ServerCommandEvent.swift new file mode 100644 index 00000000000..0d6feaf3d38 --- /dev/null +++ b/Sources/TuistServer/Models/ServerCommandEvent.swift @@ -0,0 +1,78 @@ +import Foundation + +/// Server command event +public struct ServerCommandEvent: Codable { + public let id: Int + public let name: String + public let url: URL + + public init( + id: Int, + name: String, + url: URL + ) { + self.id = id + self.name = name + self.url = url + } + + public struct Artifact: Equatable { + let type: ArtifactType + let name: String? + + init( + type: ArtifactType, + name: String? = nil + ) { + self.type = type + self.name = name + } + + enum ArtifactType { + case resultBundle, invocationRecord, resultBundleObject + } + } +} + +extension ServerCommandEvent { + init(_ commandEvent: Components.Schemas.CommandEvent) { + id = Int(commandEvent.id) + name = commandEvent.name + url = URL(string: commandEvent.url)! + } +} + +extension Components.Schemas.CommandEventArtifact { + init(_ artifact: ServerCommandEvent.Artifact) { + self = .init(name: artifact.name, _type: .init(artifact.type)) + } +} + +extension Components.Schemas.CommandEventArtifact._typePayload { + init(_ type: ServerCommandEvent.Artifact.ArtifactType) { + switch type { + case .resultBundle: + self = .result_bundle + case .invocationRecord: + self = .invocation_record + case .resultBundleObject: + self = .result_bundle_object + } + } +} + +#if MOCKING + extension ServerCommandEvent { + public static func test( + id: Int = 0, + name: String = "generate", + url: URL = URL(string: "https://cloud.tuist.io/tuist-org/tuist/runs/10")! + ) -> Self { + .init( + id: id, + name: name, + url: url + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerInvitation.swift b/Sources/TuistServer/Models/ServerInvitation.swift new file mode 100644 index 00000000000..30d67679ea7 --- /dev/null +++ b/Sources/TuistServer/Models/ServerInvitation.swift @@ -0,0 +1,54 @@ +import Foundation + +/// Server invitation +public struct ServerInvitation: Codable { + public init( + id: Int, + inviteeEmail: String, + inviter: ServerUser, + organizationId: Int, + token: String + ) { + self.id = id + self.inviteeEmail = inviteeEmail + self.inviter = inviter + self.organizationId = organizationId + self.token = token + } + + public let id: Int + public let inviteeEmail: String + public let inviter: ServerUser + public let organizationId: Int + public let token: String +} + +extension ServerInvitation { + init(_ invitation: Components.Schemas.Invitation) { + id = Int(invitation.id) + inviteeEmail = invitation.invitee_email + inviter = ServerUser(invitation.inviter) + organizationId = Int(invitation.organization_id) + token = invitation.token + } +} + +#if MOCKING + extension ServerInvitation { + public static func test( + id: Int = 0, + inviteeEmail: String = "test@tuist.io", + inviter: ServerUser = .test(), + organizationId: Int = 0, + token: String = "token" + ) -> Self { + .init( + id: id, + inviteeEmail: inviteeEmail, + inviter: inviter, + organizationId: organizationId, + token: token + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerModule.swift b/Sources/TuistServer/Models/ServerModule.swift new file mode 100644 index 00000000000..ffbbbda70a2 --- /dev/null +++ b/Sources/TuistServer/Models/ServerModule.swift @@ -0,0 +1,27 @@ +import Foundation +import Path + +/// Server module +public struct ServerModule: Codable { + public init( + hash: String, + projectRelativePath: RelativePath, + name: String + ) { + self.hash = hash + self.projectRelativePath = projectRelativePath + self.name = name + } + + public let hash: String + public let projectRelativePath: RelativePath + public let name: String +} + +extension Components.Schemas.Module { + init(_ module: ServerModule) { + hash = module.hash + project_identifier = module.projectRelativePath.pathString + name = module.name + } +} diff --git a/Sources/TuistServer/Models/ServerOrganization.swift b/Sources/TuistServer/Models/ServerOrganization.swift new file mode 100644 index 00000000000..2c2b024228f --- /dev/null +++ b/Sources/TuistServer/Models/ServerOrganization.swift @@ -0,0 +1,164 @@ +import Foundation + +/// Server organization +public struct ServerOrganization: Codable { + public enum Plan: String, Codable { + case air + case pro + case enterprise + case none + + public init(_ organization: Components.Schemas.Organization.planPayload) { + switch organization { + case .air: + self = .air + case .pro: + self = .pro + case .enterprise: + self = .enterprise + case .none, .undocumented: + self = .none + } + } + } + + public struct Member: Codable { + public enum Role: Codable, RawRepresentable { + case user, admin + + public init?(rawValue: String) { + switch rawValue { + case "user": + self = .user + case "admin": + self = .admin + default: + self = .user + } + } + + public var rawValue: String { + switch self { + case .user: + return "user" + case .admin: + return "admin" + } + } + } + + public let id: Int + public let name: String + public let email: String + public let role: Role + + public init( + id: Int, + name: String, + email: String, + role: Role + ) { + self.id = id + self.name = name + self.email = email + self.role = role + } + } + + public let id: Int + public let name: String + public let plan: Plan + public let members: [Member] + public let invitations: [ServerInvitation] + public let ssoOrganization: SSOOrganization? + + public init( + id: Int, + name: String, + plan: Plan, + members: [Member], + invitations: [ServerInvitation], + ssoOrganization: SSOOrganization? + ) { + self.id = id + self.name = name + self.plan = plan + self.members = members + self.invitations = invitations + self.ssoOrganization = ssoOrganization + } +} + +extension ServerOrganization { + init(_ organization: Components.Schemas.Organization) { + id = Int(organization.id) + name = organization.name + plan = Plan(organization.plan) + members = organization.members.map(Member.init) + invitations = organization.invitations.map(ServerInvitation.init) + if let ssoProvider = organization.sso_provider, + let ssoOrganizationId = organization.sso_organization_id + { + switch ssoProvider { + case .google: + ssoOrganization = .google(ssoOrganizationId) + case .undocumented: + ssoOrganization = nil + } + } else { + ssoOrganization = nil + } + } +} + +extension ServerOrganization.Member { + init(_ organizationMember: Components.Schemas.OrganizationMember) { + id = Int(organizationMember.id) + name = organizationMember.name + email = organizationMember.email + switch organizationMember.role { + case .admin: + role = .admin + case .user, .undocumented: + role = .user + } + } +} + +#if MOCKING + extension ServerOrganization { + public static func test( + id: Int = 0, + name: String = "test", + plan: Plan = .air, + members: [Member] = [], + invitations: [ServerInvitation] = [], + ssoOrganization: SSOOrganization? = nil + ) -> Self { + .init( + id: id, + name: name, + plan: plan, + members: members, + invitations: invitations, + ssoOrganization: ssoOrganization + ) + } + } + + extension ServerOrganization.Member { + public static func test( + id: Int = 0, + name: String = "test", + email: String = "test@email.io", + role: Role = .user + ) -> Self { + .init( + id: id, + name: name, + email: email, + role: role + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerOrganizationUsage.swift b/Sources/TuistServer/Models/ServerOrganizationUsage.swift new file mode 100644 index 00000000000..e352bfa46ca --- /dev/null +++ b/Sources/TuistServer/Models/ServerOrganizationUsage.swift @@ -0,0 +1,31 @@ +import Foundation +import Path + +/// Server organization usage +public struct ServerOrganizationUsage: Codable { + public init( + currentMonthRemoteCacheHits: Int + ) { + self.currentMonthRemoteCacheHits = currentMonthRemoteCacheHits + } + + public let currentMonthRemoteCacheHits: Int +} + +extension ServerOrganizationUsage { + init(_ organizationUsage: Components.Schemas.OrganizationUsage) { + currentMonthRemoteCacheHits = Int(organizationUsage.current_month_remote_cache_hits) + } +} + +#if DEBUG + extension ServerOrganizationUsage { + public static func test( + currentMonthRemoteCacheHits: Int = 100 + ) -> Self { + .init( + currentMonthRemoteCacheHits: currentMonthRemoteCacheHits + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerProject.swift b/Sources/TuistServer/Models/ServerProject.swift new file mode 100644 index 00000000000..5e2e3a769e6 --- /dev/null +++ b/Sources/TuistServer/Models/ServerProject.swift @@ -0,0 +1,42 @@ +import Foundation + +/// Server project +public struct ServerProject: Codable { + public init( + id: Int, + fullName: String, + defaultBranch: String + ) { + self.id = id + self.fullName = fullName + self.defaultBranch = defaultBranch + } + + public let id: Int + public let fullName: String + public let defaultBranch: String +} + +extension ServerProject { + init(_ project: Components.Schemas.Project) { + id = Int(project.id) + fullName = project.full_name + defaultBranch = project.default_branch + } +} + +#if MOCKING + extension ServerProject { + public static func test( + id: Int = 0, + fullName: String = "test/test", + defaultBranch: String = "main" + ) -> Self { + .init( + id: id, + fullName: fullName, + defaultBranch: defaultBranch + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerProjectToken.swift b/Sources/TuistServer/Models/ServerProjectToken.swift new file mode 100644 index 00000000000..d4a59388221 --- /dev/null +++ b/Sources/TuistServer/Models/ServerProjectToken.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct ServerProjectToken { + public let id: String + public let insertedAt: Date +} + +extension ServerProjectToken { + init(_ projectToken: Components.Schemas.ProjectToken) { + id = projectToken.id + insertedAt = projectToken.inserted_at + } +} + +#if DEBUG + extension ServerProjectToken { + public static func test( + id: String = "project-token-id", + insertedAt: Date = Date() + ) -> Self { + self.init( + id: id, + insertedAt: insertedAt + ) + } + } +#endif diff --git a/Sources/TuistServer/Models/ServerUser.swift b/Sources/TuistServer/Models/ServerUser.swift new file mode 100644 index 00000000000..f02dcb0cfec --- /dev/null +++ b/Sources/TuistServer/Models/ServerUser.swift @@ -0,0 +1,42 @@ +import Foundation + +/// Server user +public struct ServerUser: Codable { + public let id: Int + public let name: String + public let email: String + + public init( + id: Int, + name: String, + email: String + ) { + self.id = id + self.name = name + self.email = email + } +} + +extension ServerUser { + init(_ user: Components.Schemas.User) { + id = Int(user.id) + name = user.name + email = user.email + } +} + +#if MOCKING + extension ServerUser { + public static func test( + id: Int = 0, + name: String = "test", + email: String = "test@email.io" + ) -> Self { + .init( + id: id, + name: name, + email: email + ) + } + } +#endif diff --git a/Sources/TuistServer/Network/URLSession+Server.swift b/Sources/TuistServer/Network/URLSession+Server.swift new file mode 100644 index 00000000000..f206c16f3fa --- /dev/null +++ b/Sources/TuistServer/Network/URLSession+Server.swift @@ -0,0 +1,27 @@ +import Foundation + +private func tuistURLSessionConfiguration() -> URLSessionConfiguration { + let configuration: URLSessionConfiguration = .ephemeral + /** + Our API design leads to an inefficient usage of the transport layer, which leads to Fly having to spin + new machines suddenly, and that causes URLSession to time out. The high timeouts here are temporary + until we change the server-side API to lead to a more efficient using of the transport layer. + + I noticed on Fly https://fly.io/apps/tuist-cloud/metrics that the peaks can reach up to + 100 seconds, hence why I set the limit to 120. + */ + configuration.timeoutIntervalForRequest = 120 // 2 minutes + configuration.timeoutIntervalForResource = 300 // 5 minutes + configuration.allowsCellularAccess = true + configuration.allowsConstrainedNetworkAccess = true + configuration.allowsExpensiveNetworkAccess = true + return configuration +} + +private var _tuistURLSession: URLSession = .init(configuration: tuistURLSessionConfiguration()) + +extension URLSession { + public static var tuistShared: URLSession { + _tuistURLSession + } +} diff --git a/Sources/TuistServer/OpenAPI/CacheCategoryParameter+Extras.swift b/Sources/TuistServer/OpenAPI/CacheCategoryParameter+Extras.swift new file mode 100644 index 00000000000..88c4189a2a4 --- /dev/null +++ b/Sources/TuistServer/OpenAPI/CacheCategoryParameter+Extras.swift @@ -0,0 +1,12 @@ +import TuistCore + +extension Components.Schemas.CacheCategory { + init(_ cacheCategory: RemoteCacheCategory) { + switch cacheCategory { + case .binaries: + self = .builds + case .selectiveTests: + self = .tests + } + } +} diff --git a/Sources/TuistServer/OpenAPI/Client.swift b/Sources/TuistServer/OpenAPI/Client.swift new file mode 100644 index 00000000000..784128cd4c1 --- /dev/null +++ b/Sources/TuistServer/OpenAPI/Client.swift @@ -0,0 +1,3572 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import Foundation +#else +import Foundation +#endif +public struct Client: APIProtocol { + /// The underlying HTTP client. + private let client: UniversalClient + /// Creates a new client. + /// - Parameters: + /// - serverURL: The server URL that the client connects to. Any server + /// URLs defined in the OpenAPI document are available as static methods + /// on the ``Servers`` type. + /// - configuration: A set of configuration values for the client. + /// - transport: A transport that performs HTTP operations. + /// - middlewares: A list of middlewares to call before the transport. + public init( + serverURL: URL, + configuration: Configuration = .init(), + transport: any ClientTransport, + middlewares: [any ClientMiddleware] = [] + ) { + self.client = .init( + serverURL: serverURL, + configuration: configuration, + transport: transport, + middlewares: middlewares + ) + } + private var converter: Converter { client.converter } + /// Create a a new command analytics event + /// + /// - Remark: HTTP `POST /api/analytics`. + /// - Remark: Generated from `#/paths//api/analytics/post(createCommandEvent)`. + public func createCommandEvent(_ input: Operations.createCommandEvent.Input) async throws + -> Operations.createCommandEvent.Output + { + try await client.send( + input: input, + forOperation: Operations.createCommandEvent.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/analytics", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "project_id", + value: input.query.project_id + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.createCommandEvent.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createCommandEvent.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.CommandEvent.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.createCommandEvent.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createCommandEvent.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.createCommandEvent.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createCommandEvent.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Authenticate with email and password. + /// + /// This endpoint returns API tokens for a given email and password. + /// + /// - Remark: HTTP `POST /api/auth`. + /// - Remark: Generated from `#/paths//api/auth/post(authenticate)`. + public func authenticate(_ input: Operations.authenticate.Input) async throws + -> Operations.authenticate.Output + { + try await client.send( + input: input, + forOperation: Operations.authenticate.id, + serializer: { input in + let path = try converter.renderedRequestPath(template: "/api/auth", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.authenticate.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.authenticate.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.AuthenticationTokens.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.authenticate.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.authenticate.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Get a specific device code. + /// + /// This endpoint returns a token for a given device code if the device code is authenticated. + /// + /// - Remark: HTTP `GET /api/auth/device_code/{device_code}`. + /// - Remark: Generated from `#/paths//api/auth/device_code/{device_code}/get(getDeviceCode)`. + public func getDeviceCode(_ input: Operations.getDeviceCode.Input) async throws + -> Operations.getDeviceCode.Output + { + try await client.send( + input: input, + forOperation: Operations.getDeviceCode.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/auth/device_code/{}", + parameters: [input.path.device_code] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.getDeviceCode.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.getDeviceCode.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.getDeviceCode.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 202: + let headers: Operations.getDeviceCode.Output.Accepted.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.getDeviceCode.Output.Accepted.Body = + try converter.getResponseBodyAsJSON( + OpenAPIRuntime.OpenAPIObjectContainer.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .accepted(.init(headers: headers, body: body)) + case 400: + let headers: Operations.getDeviceCode.Output.BadRequest.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.getDeviceCode.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Request new tokens. + /// + /// This endpoint returns new tokens for a given refresh token if the refresh token is valid. + /// + /// - Remark: HTTP `POST /api/auth/refresh_token`. + /// - Remark: Generated from `#/paths//api/auth/refresh_token/post(refreshToken)`. + public func refreshToken(_ input: Operations.refreshToken.Input) async throws + -> Operations.refreshToken.Output + { + try await client.send( + input: input, + forOperation: Operations.refreshToken.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/auth/refresh_token", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.refreshToken.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.refreshToken.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.AuthenticationTokens.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.refreshToken.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.refreshToken.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Downloads an artifact from the cache. + /// + /// This endpoint returns a signed URL that can be used to download an artifact from the cache. + /// + /// - Remark: HTTP `GET /api/cache`. + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)`. + public func downloadCacheArtifact(_ input: Operations.downloadCacheArtifact.Input) async throws + -> Operations.downloadCacheArtifact.Output + { + try await client.send( + input: input, + forOperation: Operations.downloadCacheArtifact.id, + serializer: { input in + let path = try converter.renderedRequestPath(template: "/api/cache", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "cache_category", + value: input.query.cache_category + ) + try converter.setQueryItemAsText( + in: &request, + name: "project_id", + value: input.query.project_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "hash", + value: input.query.hash + ) + try converter.setQueryItemAsText( + in: &request, + name: "name", + value: input.query.name + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.downloadCacheArtifact.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadCacheArtifact.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.CacheArtifactDownloadURL.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.downloadCacheArtifact.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadCacheArtifact.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 402: + let headers: Operations.downloadCacheArtifact.Output.PaymentRequired.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadCacheArtifact.Output.PaymentRequired.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .paymentRequired(.init(headers: headers, body: body)) + case 403: + let headers: Operations.downloadCacheArtifact.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadCacheArtifact.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.downloadCacheArtifact.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadCacheArtifact.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It checks if an artifact exists in the cache. + /// + /// This endpoint checks if an artifact exists in the cache. It returns a 404 status code if the artifact does not exist. + /// + /// - Remark: HTTP `GET /api/cache/exists`. + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)`. + public func cacheArtifactExists(_ input: Operations.cacheArtifactExists.Input) async throws + -> Operations.cacheArtifactExists.Output + { + try await client.send( + input: input, + forOperation: Operations.cacheArtifactExists.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/cache/exists", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "cache_category", + value: input.query.cache_category + ) + try converter.setQueryItemAsText( + in: &request, + name: "project_id", + value: input.query.project_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "hash", + value: input.query.hash + ) + try converter.setQueryItemAsText( + in: &request, + name: "name", + value: input.query.name + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.cacheArtifactExists.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cacheArtifactExists.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.cacheArtifactExists.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.cacheArtifactExists.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cacheArtifactExists.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 402: + let headers: Operations.cacheArtifactExists.Output.PaymentRequired.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cacheArtifactExists.Output.PaymentRequired.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .paymentRequired(.init(headers: headers, body: body)) + case 403: + let headers: Operations.cacheArtifactExists.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cacheArtifactExists.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.cacheArtifactExists.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cacheArtifactExists.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Operations.cacheArtifactExists.Output.NotFound.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. The cache will then be able to serve the artifact. + /// + /// - Remark: HTTP `POST /api/cache/multipart/complete`. + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)`. + public func completeCacheArtifactMultipartUpload( + _ input: Operations.completeCacheArtifactMultipartUpload.Input + ) async throws -> Operations.completeCacheArtifactMultipartUpload.Output { + try await client.send( + input: input, + forOperation: Operations.completeCacheArtifactMultipartUpload.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/cache/multipart/complete", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "cache_category", + value: input.query.cache_category + ) + try converter.setQueryItemAsText( + in: &request, + name: "project_id", + value: input.query.project_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "hash", + value: input.query.hash + ) + try converter.setQueryItemAsText( + in: &request, + name: "upload_id", + value: input.query.upload_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "name", + value: input.query.name + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.completeCacheArtifactMultipartUpload.Output.Ok.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body + .jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 402: + let headers: + Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .paymentRequired(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.completeCacheArtifactMultipartUpload.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeCacheArtifactMultipartUpload.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.completeCacheArtifactMultipartUpload.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completeCacheArtifactMultipartUpload.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/cache/multipart/generate-url`. + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)`. + public func generateCacheArtifactMultipartUploadURL( + _ input: Operations.generateCacheArtifactMultipartUploadURL.Input + ) async throws -> Operations.generateCacheArtifactMultipartUploadURL.Output { + try await client.send( + input: input, + forOperation: Operations.generateCacheArtifactMultipartUploadURL.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/cache/multipart/generate-url", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "cache_category", + value: input.query.cache_category + ) + try converter.setQueryItemAsText( + in: &request, + name: "project_id", + value: input.query.project_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "hash", + value: input.query.hash + ) + try converter.setQueryItemAsText( + in: &request, + name: "part_number", + value: input.query.part_number + ) + try converter.setQueryItemAsText( + in: &request, + name: "upload_id", + value: input.query.upload_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "name", + value: input.query.name + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.Ok.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.generateCacheArtifactMultipartUploadURL.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.ArtifactMultipartUploadURL.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 402: + let headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.PaymentRequired + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateCacheArtifactMultipartUploadURL.Output.PaymentRequired + .Body = try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .paymentRequired(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It initiates a multipart upload in the cache. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/cache/multipart/start`. + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)`. + public func startCacheArtifactMultipartUpload( + _ input: Operations.startCacheArtifactMultipartUpload.Input + ) async throws -> Operations.startCacheArtifactMultipartUpload.Output { + try await client.send( + input: input, + forOperation: Operations.startCacheArtifactMultipartUpload.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/cache/multipart/start", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "cache_category", + value: input.query.cache_category + ) + try converter.setQueryItemAsText( + in: &request, + name: "project_id", + value: input.query.project_id + ) + try converter.setQueryItemAsText( + in: &request, + name: "hash", + value: input.query.hash + ) + try converter.setQueryItemAsText( + in: &request, + name: "name", + value: input.query.name + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.startCacheArtifactMultipartUpload.Output.Ok.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startCacheArtifactMultipartUpload.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.ArtifactUploadID.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.startCacheArtifactMultipartUpload.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.startCacheArtifactMultipartUpload.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 402: + let headers: + Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .paymentRequired(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.startCacheArtifactMultipartUpload.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startCacheArtifactMultipartUpload.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.startCacheArtifactMultipartUpload.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startCacheArtifactMultipartUpload.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Lists the organizations + /// + /// Returns all the organizations the authenticated subject is part of. + /// + /// - Remark: HTTP `GET /api/organizations`. + /// - Remark: Generated from `#/paths//api/organizations/get(listOrganizations)`. + public func listOrganizations(_ input: Operations.listOrganizations.Input) async throws + -> Operations.listOrganizations.Output + { + try await client.send( + input: input, + forOperation: Operations.listOrganizations.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.listOrganizations.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listOrganizations.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.listOrganizations.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.listOrganizations.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listOrganizations.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.listOrganizations.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listOrganizations.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Creates an organization + /// + /// Creates an organization with the given name. + /// + /// - Remark: HTTP `POST /api/organizations`. + /// - Remark: Generated from `#/paths//api/organizations/post(createOrganization)`. + public func createOrganization(_ input: Operations.createOrganization.Input) async throws + -> Operations.createOrganization.Output + { + try await client.send( + input: input, + forOperation: Operations.createOrganization.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.createOrganization.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createOrganization.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Organization.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 400: + let headers: Operations.createOrganization.Output.BadRequest.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createOrganization.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Shows an organization + /// + /// Returns the organization with the given identifier. + /// + /// - Remark: HTTP `GET /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)`. + public func showOrganization(_ input: Operations.showOrganization.Input) async throws + -> Operations.showOrganization.Output + { + try await client.send( + input: input, + forOperation: Operations.showOrganization.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.showOrganization.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganization.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Organization.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.showOrganization.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganization.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.showOrganization.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganization.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.showOrganization.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganization.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Updates an organization + /// + /// Updates an organization with given parameters. + /// + /// - Remark: HTTP `PATCH /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))`. + public func updateOrganization__2_(_ input: Operations.updateOrganization__2_.Input) + async throws -> Operations.updateOrganization__2_.Output + { + try await client.send( + input: input, + forOperation: Operations.updateOrganization__2_.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .patch) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.updateOrganization__2_.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization__2_.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Organization.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 400: + let headers: Operations.updateOrganization__2_.Output.BadRequest.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization__2_.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.updateOrganization__2_.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization__2_.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.updateOrganization__2_.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization__2_.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.updateOrganization__2_.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization__2_.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Updates an organization + /// + /// Updates an organization with given parameters. + /// + /// - Remark: HTTP `PUT /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)`. + public func updateOrganization(_ input: Operations.updateOrganization.Input) async throws + -> Operations.updateOrganization.Output + { + try await client.send( + input: input, + forOperation: Operations.updateOrganization.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.updateOrganization.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Organization.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 400: + let headers: Operations.updateOrganization.Output.BadRequest.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.updateOrganization.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.updateOrganization.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.updateOrganization.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganization.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Deletes an organization + /// + /// Deletes the organization with the given name. + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)`. + public func deleteOrganization(_ input: Operations.deleteOrganization.Input) async throws + -> Operations.deleteOrganization.Output + { + try await client.send( + input: input, + forOperation: Operations.deleteOrganization.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .delete) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.deleteOrganization.Output.NoContent.Headers = .init() + return .noContent(.init(headers: headers, body: nil)) + case 401: + let headers: Operations.deleteOrganization.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.deleteOrganization.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.deleteOrganization.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.deleteOrganization.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.deleteOrganization.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.deleteOrganization.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Creates an invitation + /// + /// Invites a user with a given email to a given organization. + /// + /// - Remark: HTTP `POST /api/organizations/{organization_name}/invitations`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)`. + public func createInvitation(_ input: Operations.createInvitation.Input) async throws + -> Operations.createInvitation.Output + { + try await client.send( + input: input, + forOperation: Operations.createInvitation.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}/invitations", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.createInvitation.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createInvitation.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Invitation.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 400: + let headers: Operations.createInvitation.Output.BadRequest.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createInvitation.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.createInvitation.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createInvitation.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.createInvitation.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createInvitation.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.createInvitation.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createInvitation.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Cancels an invitation + /// + /// Cancels an invitation for a given invitee email and an organization. + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}/invitations`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)`. + public func cancelInvitation(_ input: Operations.cancelInvitation.Input) async throws + -> Operations.cancelInvitation.Output + { + try await client.send( + input: input, + forOperation: Operations.cancelInvitation.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}/invitations", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .delete) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.cancelInvitation.Output.NoContent.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cancelInvitation.Output.NoContent.Body = + try converter.getResponseBodyAsJSON( + OpenAPIRuntime.OpenAPIValueContainer.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .noContent(.init(headers: headers, body: body)) + case 401: + let headers: Operations.cancelInvitation.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cancelInvitation.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.cancelInvitation.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cancelInvitation.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.cancelInvitation.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cancelInvitation.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Updates a member in an organization + /// + /// Updates a member in a given organization + /// + /// - Remark: HTTP `PUT /api/organizations/{organization_name}/members/{user_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)`. + public func updateOrganizationMember(_ input: Operations.updateOrganizationMember.Input) + async throws -> Operations.updateOrganizationMember.Output + { + try await client.send( + input: input, + forOperation: Operations.updateOrganizationMember.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}/members/{}", + parameters: [input.path.organization_name, input.path.user_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.updateOrganizationMember.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganizationMember.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.OrganizationMember.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 400: + let headers: Operations.updateOrganizationMember.Output.BadRequest.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganizationMember.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.updateOrganizationMember.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganizationMember.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.updateOrganizationMember.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganizationMember.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.updateOrganizationMember.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateOrganizationMember.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Removes a member from an organization + /// + /// Removes a member with a given username from a given organization + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}/members/{user_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)`. + public func removeOrganizationMember(_ input: Operations.removeOrganizationMember.Input) + async throws -> Operations.removeOrganizationMember.Output + { + try await client.send( + input: input, + forOperation: Operations.removeOrganizationMember.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}/members/{}", + parameters: [input.path.organization_name, input.path.user_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .delete) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.removeOrganizationMember.Output.NoContent.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.removeOrganizationMember.Output.NoContent.Body = + try converter.getResponseBodyAsJSON( + OpenAPIRuntime.OpenAPIValueContainer.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .noContent(.init(headers: headers, body: body)) + case 400: + let headers: Operations.removeOrganizationMember.Output.BadRequest.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.removeOrganizationMember.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.removeOrganizationMember.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.removeOrganizationMember.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.removeOrganizationMember.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.removeOrganizationMember.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.removeOrganizationMember.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.removeOrganizationMember.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Shows the usage of an organization + /// + /// Returns the usage of the organization with the given identifier. (e.g. number of remote cache hits) + /// + /// - Remark: HTTP `GET /api/organizations/{organization_name}/usage`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)`. + public func showOrganizationUsage(_ input: Operations.showOrganizationUsage.Input) async throws + -> Operations.showOrganizationUsage.Output + { + try await client.send( + input: input, + forOperation: Operations.showOrganizationUsage.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/organizations/{}/usage", + parameters: [input.path.organization_name] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.showOrganizationUsage.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganizationUsage.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.OrganizationUsage.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.showOrganizationUsage.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganizationUsage.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.showOrganizationUsage.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganizationUsage.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.showOrganizationUsage.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showOrganizationUsage.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// List projects the authenticated user has access to. + /// + /// - Remark: HTTP `GET /api/projects`. + /// - Remark: Generated from `#/paths//api/projects/get(listProjects)`. + public func listProjects(_ input: Operations.listProjects.Input) async throws + -> Operations.listProjects.Output + { + try await client.send( + input: input, + forOperation: Operations.listProjects.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.listProjects.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listProjects.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.listProjects.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.listProjects.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listProjects.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Create a new project. + /// + /// - Remark: HTTP `POST /api/projects`. + /// - Remark: Generated from `#/paths//api/projects/post(createProject)`. + public func createProject(_ input: Operations.createProject.Input) async throws + -> Operations.createProject.Output + { + try await client.send( + input: input, + forOperation: Operations.createProject.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.createProject.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProject.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Project.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 400: + let headers: Operations.createProject.Output.BadRequest.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProject.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.createProject.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProject.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.createProject.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProject.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Returns a project based on the handle. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)`. + public func showProject(_ input: Operations.showProject.Input) async throws + -> Operations.showProject.Output + { + try await client.send( + input: input, + forOperation: Operations.showProject.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.showProject.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showProject.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Project.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.showProject.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showProject.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.showProject.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showProject.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.showProject.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.showProject.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Updates a project + /// + /// Updates an project with given parameters. + /// + /// - Remark: HTTP `PUT /api/projects/{account_handle}/{project_handle}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)`. + public func updateProject(_ input: Operations.updateProject.Input) async throws + -> Operations.updateProject.Output + { + try await client.send( + input: input, + forOperation: Operations.updateProject.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.updateProject.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateProject.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Project.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.updateProject.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateProject.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.updateProject.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateProject.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.updateProject.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.updateProject.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Cleans cache for a given project + /// + /// - Remark: HTTP `PUT /api/projects/{account_handle}/{project_handle}/cache/clean`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)`. + public func cleanCache(_ input: Operations.cleanCache.Input) async throws + -> Operations.cleanCache.Output + { + try await client.send( + input: input, + forOperation: Operations.cleanCache.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/cache/clean", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.cleanCache.Output.NoContent.Headers = .init() + return .noContent(.init(headers: headers, body: nil)) + case 401: + let headers: Operations.cleanCache.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cleanCache.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.cleanCache.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cleanCache.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.cleanCache.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.cleanCache.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/complete`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)`. + public func completePreviewsMultipartUpload( + _ input: Operations.completePreviewsMultipartUpload.Input + ) async throws -> Operations.completePreviewsMultipartUpload.Output { + try await client.send( + input: input, + forOperation: Operations.completePreviewsMultipartUpload.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/previews/complete", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.completePreviewsMultipartUpload.Output.Ok.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completePreviewsMultipartUpload.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.completePreviewsMultipartUpload.Output.Ok.Body.jsonPayload + .self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.completePreviewsMultipartUpload.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completePreviewsMultipartUpload.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.completePreviewsMultipartUpload.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completePreviewsMultipartUpload.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.completePreviewsMultipartUpload.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completePreviewsMultipartUpload.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/generate-url`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)`. + public func generatePreviewsMultipartUploadURL( + _ input: Operations.generatePreviewsMultipartUploadURL.Input + ) async throws -> Operations.generatePreviewsMultipartUploadURL.Output { + try await client.send( + input: input, + forOperation: Operations.generatePreviewsMultipartUploadURL.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/previews/generate-url", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.generatePreviewsMultipartUploadURL.Output.Ok.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.generatePreviewsMultipartUploadURL.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.ArtifactMultipartUploadURL.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.generatePreviewsMultipartUploadURL.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.generatePreviewsMultipartUploadURL.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.generatePreviewsMultipartUploadURL.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.generatePreviewsMultipartUploadURL.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It initiates a multipart upload for a preview artifact. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/start`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)`. + public func startPreviewsMultipartUpload(_ input: Operations.startPreviewsMultipartUpload.Input) + async throws -> Operations.startPreviewsMultipartUpload.Output + { + try await client.send( + input: input, + forOperation: Operations.startPreviewsMultipartUpload.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/previews/start", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.startPreviewsMultipartUpload.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startPreviewsMultipartUpload.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.startPreviewsMultipartUpload.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.startPreviewsMultipartUpload.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startPreviewsMultipartUpload.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.startPreviewsMultipartUpload.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startPreviewsMultipartUpload.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.startPreviewsMultipartUpload.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startPreviewsMultipartUpload.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Downloads a preview. + /// + /// This endpoint returns a signed URL that can be used to download a preview. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}/previews/{preview_id}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)`. + public func downloadPreview(_ input: Operations.downloadPreview.Input) async throws + -> Operations.downloadPreview.Output + { + try await client.send( + input: input, + forOperation: Operations.downloadPreview.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/previews/{}", + parameters: [ + input.path.account_handle, input.path.project_handle, input.path.preview_id, + ] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.downloadPreview.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadPreview.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.ArtifactDownloadURL.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.downloadPreview.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadPreview.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.downloadPreview.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadPreview.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.downloadPreview.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.downloadPreview.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// List all project tokens. + /// + /// This endpoint returns all tokens for a given project. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}/tokens`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)`. + public func listProjectTokens(_ input: Operations.listProjectTokens.Input) async throws + -> Operations.listProjectTokens.Output + { + try await client.send( + input: input, + forOperation: Operations.listProjectTokens.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/tokens", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.listProjectTokens.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listProjectTokens.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.listProjectTokens.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.listProjectTokens.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listProjectTokens.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.listProjectTokens.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listProjectTokens.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.listProjectTokens.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.listProjectTokens.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Create a new project token. + /// + /// This endpoint returns a new project token. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/tokens`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)`. + public func createProjectToken(_ input: Operations.createProjectToken.Input) async throws + -> Operations.createProjectToken.Output + { + try await client.send( + input: input, + forOperation: Operations.createProjectToken.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/tokens", + parameters: [input.path.account_handle, input.path.project_handle] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.createProjectToken.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProjectToken.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Operations.createProjectToken.Output.Ok.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: Operations.createProjectToken.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProjectToken.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.createProjectToken.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProjectToken.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.createProjectToken.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.createProjectToken.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Revokes a project token. + /// + /// - Remark: HTTP `DELETE /api/projects/{account_handle}/{project_handle}/tokens/{id}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)`. + public func revokeProjectToken(_ input: Operations.revokeProjectToken.Input) async throws + -> Operations.revokeProjectToken.Output + { + try await client.send( + input: input, + forOperation: Operations.revokeProjectToken.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}/{}/tokens/{}", + parameters: [ + input.path.account_handle, input.path.project_handle, input.path.id, + ] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .delete) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.revokeProjectToken.Output.NoContent.Headers = .init() + return .noContent(.init(headers: headers, body: nil)) + case 400: + let headers: Operations.revokeProjectToken.Output.BadRequest.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.revokeProjectToken.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .badRequest(.init(headers: headers, body: body)) + case 401: + let headers: Operations.revokeProjectToken.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.revokeProjectToken.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.revokeProjectToken.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.revokeProjectToken.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.revokeProjectToken.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.revokeProjectToken.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Deletes a project with a given id. + /// + /// - Remark: HTTP `DELETE /api/projects/{id}`. + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)`. + public func deleteProject(_ input: Operations.deleteProject.Input) async throws + -> Operations.deleteProject.Output + { + try await client.send( + input: input, + forOperation: Operations.deleteProject.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/projects/{}", + parameters: [input.path.id] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .delete) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.deleteProject.Output.NoContent.Headers = .init() + return .noContent(.init(headers: headers, body: nil)) + case 401: + let headers: Operations.deleteProject.Output.Unauthorized.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.deleteProject.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: Operations.deleteProject.Output.Forbidden.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.deleteProject.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: Operations.deleteProject.Output.NotFound.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.deleteProject.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/complete`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)`. + public func completeAnalyticsArtifactMultipartUpload( + _ input: Operations.completeAnalyticsArtifactMultipartUpload.Input + ) async throws -> Operations.completeAnalyticsArtifactMultipartUpload.Output { + try await client.send( + input: input, + forOperation: Operations.completeAnalyticsArtifactMultipartUpload.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/runs/{}/complete", + parameters: [input.path.run_id] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NoContent.Headers = + .init() + return .noContent(.init(headers: headers, body: nil)) + case 401: + let headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Completes artifacts uploads for a given command event + /// + /// Given a command event, it marks all artifact uploads as finished and does extra processing of a given command run, such as test flakiness detection. + /// + /// - Remark: HTTP `PUT /api/runs/{run_id}/complete_artifacts_uploads`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)`. + public func completeAnalyticsArtifactsUploads( + _ input: Operations.completeAnalyticsArtifactsUploads.Input + ) async throws -> Operations.completeAnalyticsArtifactsUploads.Output { + try await client.send( + input: input, + forOperation: Operations.completeAnalyticsArtifactsUploads.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/runs/{}/complete_artifacts_uploads", + parameters: [input.path.run_id] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: + Operations.completeAnalyticsArtifactsUploads.Output.NoContent.Headers = + .init() + return .noContent(.init(headers: headers, body: nil)) + case 401: + let headers: + Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.completeAnalyticsArtifactsUploads.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completeAnalyticsArtifactsUploads.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.completeAnalyticsArtifactsUploads.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.completeAnalyticsArtifactsUploads.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/generate-url`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)`. + public func generateAnalyticsArtifactMultipartUploadURL( + _ input: Operations.generateAnalyticsArtifactMultipartUploadURL.Input + ) async throws -> Operations.generateAnalyticsArtifactMultipartUploadURL.Output { + try await client.send( + input: input, + forOperation: Operations.generateAnalyticsArtifactMultipartUploadURL.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/runs/{}/generate-url", + parameters: [input.path.run_id] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.ArtifactMultipartUploadURL.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Unauthorized + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Unauthorized + .Body = try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound + .Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// It initiates a multipart upload for a command event artifact. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/start`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)`. + public func startAnalyticsArtifactMultipartUpload( + _ input: Operations.startAnalyticsArtifactMultipartUpload.Input + ) async throws -> Operations.startAnalyticsArtifactMultipartUpload.Output { + try await client.send( + input: input, + forOperation: Operations.startAnalyticsArtifactMultipartUpload.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/api/runs/{}/start", + parameters: [input.path.run_id] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + request.body = try converter.setOptionalRequestBodyAsJSON( + input.body, + headerFields: &request.headerFields, + transforming: { wrapped in + switch wrapped { + case let .json(value): + return .init( + value: value, + contentType: "application/json; charset=utf-8" + ) + } + } + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.Ok.Headers = .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: Operations.startAnalyticsArtifactMultipartUpload.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.ArtifactUploadID.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .ok(.init(headers: headers, body: body)) + case 401: + let headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .unauthorized(.init(headers: headers, body: body)) + case 403: + let headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .forbidden(.init(headers: headers, body: body)) + case 404: + let headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound.Headers = + .init() + try converter.validateContentTypeIfPresent( + in: response.headerFields, + substring: "application/json" + ) + let body: + Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + return .notFound(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } +} diff --git a/Sources/TuistServer/OpenAPI/Types.swift b/Sources/TuistServer/OpenAPI/Types.swift new file mode 100644 index 00000000000..d59f1c0af2a --- /dev/null +++ b/Sources/TuistServer/OpenAPI/Types.swift @@ -0,0 +1,9779 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import Foundation +#else +import Foundation +#endif +/// A type that performs HTTP operations defined by the OpenAPI document. +public protocol APIProtocol: Sendable { + /// Create a a new command analytics event + /// + /// - Remark: HTTP `POST /api/analytics`. + /// - Remark: Generated from `#/paths//api/analytics/post(createCommandEvent)`. + func createCommandEvent(_ input: Operations.createCommandEvent.Input) async throws + -> Operations.createCommandEvent.Output + /// Authenticate with email and password. + /// + /// This endpoint returns API tokens for a given email and password. + /// + /// - Remark: HTTP `POST /api/auth`. + /// - Remark: Generated from `#/paths//api/auth/post(authenticate)`. + func authenticate(_ input: Operations.authenticate.Input) async throws + -> Operations.authenticate.Output + /// Get a specific device code. + /// + /// This endpoint returns a token for a given device code if the device code is authenticated. + /// + /// - Remark: HTTP `GET /api/auth/device_code/{device_code}`. + /// - Remark: Generated from `#/paths//api/auth/device_code/{device_code}/get(getDeviceCode)`. + func getDeviceCode(_ input: Operations.getDeviceCode.Input) async throws + -> Operations.getDeviceCode.Output + /// Request new tokens. + /// + /// This endpoint returns new tokens for a given refresh token if the refresh token is valid. + /// + /// - Remark: HTTP `POST /api/auth/refresh_token`. + /// - Remark: Generated from `#/paths//api/auth/refresh_token/post(refreshToken)`. + func refreshToken(_ input: Operations.refreshToken.Input) async throws + -> Operations.refreshToken.Output + /// Downloads an artifact from the cache. + /// + /// This endpoint returns a signed URL that can be used to download an artifact from the cache. + /// + /// - Remark: HTTP `GET /api/cache`. + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)`. + func downloadCacheArtifact(_ input: Operations.downloadCacheArtifact.Input) async throws + -> Operations.downloadCacheArtifact.Output + /// It checks if an artifact exists in the cache. + /// + /// This endpoint checks if an artifact exists in the cache. It returns a 404 status code if the artifact does not exist. + /// + /// - Remark: HTTP `GET /api/cache/exists`. + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)`. + @available(*, deprecated) func cacheArtifactExists( + _ input: Operations.cacheArtifactExists.Input + ) async throws -> Operations.cacheArtifactExists.Output + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. The cache will then be able to serve the artifact. + /// + /// - Remark: HTTP `POST /api/cache/multipart/complete`. + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)`. + func completeCacheArtifactMultipartUpload( + _ input: Operations.completeCacheArtifactMultipartUpload.Input + ) async throws -> Operations.completeCacheArtifactMultipartUpload.Output + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/cache/multipart/generate-url`. + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)`. + func generateCacheArtifactMultipartUploadURL( + _ input: Operations.generateCacheArtifactMultipartUploadURL.Input + ) async throws -> Operations.generateCacheArtifactMultipartUploadURL.Output + /// It initiates a multipart upload in the cache. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/cache/multipart/start`. + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)`. + func startCacheArtifactMultipartUpload( + _ input: Operations.startCacheArtifactMultipartUpload.Input + ) async throws -> Operations.startCacheArtifactMultipartUpload.Output + /// Lists the organizations + /// + /// Returns all the organizations the authenticated subject is part of. + /// + /// - Remark: HTTP `GET /api/organizations`. + /// - Remark: Generated from `#/paths//api/organizations/get(listOrganizations)`. + func listOrganizations(_ input: Operations.listOrganizations.Input) async throws + -> Operations.listOrganizations.Output + /// Creates an organization + /// + /// Creates an organization with the given name. + /// + /// - Remark: HTTP `POST /api/organizations`. + /// - Remark: Generated from `#/paths//api/organizations/post(createOrganization)`. + func createOrganization(_ input: Operations.createOrganization.Input) async throws + -> Operations.createOrganization.Output + /// Shows an organization + /// + /// Returns the organization with the given identifier. + /// + /// - Remark: HTTP `GET /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)`. + func showOrganization(_ input: Operations.showOrganization.Input) async throws + -> Operations.showOrganization.Output + /// Updates an organization + /// + /// Updates an organization with given parameters. + /// + /// - Remark: HTTP `PATCH /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))`. + func updateOrganization__2_(_ input: Operations.updateOrganization__2_.Input) async throws + -> Operations.updateOrganization__2_.Output + /// Updates an organization + /// + /// Updates an organization with given parameters. + /// + /// - Remark: HTTP `PUT /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)`. + func updateOrganization(_ input: Operations.updateOrganization.Input) async throws + -> Operations.updateOrganization.Output + /// Deletes an organization + /// + /// Deletes the organization with the given name. + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)`. + func deleteOrganization(_ input: Operations.deleteOrganization.Input) async throws + -> Operations.deleteOrganization.Output + /// Creates an invitation + /// + /// Invites a user with a given email to a given organization. + /// + /// - Remark: HTTP `POST /api/organizations/{organization_name}/invitations`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)`. + func createInvitation(_ input: Operations.createInvitation.Input) async throws + -> Operations.createInvitation.Output + /// Cancels an invitation + /// + /// Cancels an invitation for a given invitee email and an organization. + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}/invitations`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)`. + func cancelInvitation(_ input: Operations.cancelInvitation.Input) async throws + -> Operations.cancelInvitation.Output + /// Updates a member in an organization + /// + /// Updates a member in a given organization + /// + /// - Remark: HTTP `PUT /api/organizations/{organization_name}/members/{user_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)`. + func updateOrganizationMember(_ input: Operations.updateOrganizationMember.Input) async throws + -> Operations.updateOrganizationMember.Output + /// Removes a member from an organization + /// + /// Removes a member with a given username from a given organization + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}/members/{user_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)`. + func removeOrganizationMember(_ input: Operations.removeOrganizationMember.Input) async throws + -> Operations.removeOrganizationMember.Output + /// Shows the usage of an organization + /// + /// Returns the usage of the organization with the given identifier. (e.g. number of remote cache hits) + /// + /// - Remark: HTTP `GET /api/organizations/{organization_name}/usage`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)`. + func showOrganizationUsage(_ input: Operations.showOrganizationUsage.Input) async throws + -> Operations.showOrganizationUsage.Output + /// List projects the authenticated user has access to. + /// + /// - Remark: HTTP `GET /api/projects`. + /// - Remark: Generated from `#/paths//api/projects/get(listProjects)`. + func listProjects(_ input: Operations.listProjects.Input) async throws + -> Operations.listProjects.Output + /// Create a new project. + /// + /// - Remark: HTTP `POST /api/projects`. + /// - Remark: Generated from `#/paths//api/projects/post(createProject)`. + func createProject(_ input: Operations.createProject.Input) async throws + -> Operations.createProject.Output + /// Returns a project based on the handle. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)`. + func showProject(_ input: Operations.showProject.Input) async throws + -> Operations.showProject.Output + /// Updates a project + /// + /// Updates an project with given parameters. + /// + /// - Remark: HTTP `PUT /api/projects/{account_handle}/{project_handle}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)`. + func updateProject(_ input: Operations.updateProject.Input) async throws + -> Operations.updateProject.Output + /// Cleans cache for a given project + /// + /// - Remark: HTTP `PUT /api/projects/{account_handle}/{project_handle}/cache/clean`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)`. + func cleanCache(_ input: Operations.cleanCache.Input) async throws + -> Operations.cleanCache.Output + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/complete`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)`. + func completePreviewsMultipartUpload(_ input: Operations.completePreviewsMultipartUpload.Input) + async throws -> Operations.completePreviewsMultipartUpload.Output + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/generate-url`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)`. + func generatePreviewsMultipartUploadURL( + _ input: Operations.generatePreviewsMultipartUploadURL.Input + ) async throws -> Operations.generatePreviewsMultipartUploadURL.Output + /// It initiates a multipart upload for a preview artifact. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/start`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)`. + func startPreviewsMultipartUpload(_ input: Operations.startPreviewsMultipartUpload.Input) + async throws -> Operations.startPreviewsMultipartUpload.Output + /// Downloads a preview. + /// + /// This endpoint returns a signed URL that can be used to download a preview. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}/previews/{preview_id}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)`. + func downloadPreview(_ input: Operations.downloadPreview.Input) async throws + -> Operations.downloadPreview.Output + /// List all project tokens. + /// + /// This endpoint returns all tokens for a given project. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}/tokens`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)`. + func listProjectTokens(_ input: Operations.listProjectTokens.Input) async throws + -> Operations.listProjectTokens.Output + /// Create a new project token. + /// + /// This endpoint returns a new project token. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/tokens`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)`. + func createProjectToken(_ input: Operations.createProjectToken.Input) async throws + -> Operations.createProjectToken.Output + /// Revokes a project token. + /// + /// - Remark: HTTP `DELETE /api/projects/{account_handle}/{project_handle}/tokens/{id}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)`. + func revokeProjectToken(_ input: Operations.revokeProjectToken.Input) async throws + -> Operations.revokeProjectToken.Output + /// Deletes a project with a given id. + /// + /// - Remark: HTTP `DELETE /api/projects/{id}`. + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)`. + func deleteProject(_ input: Operations.deleteProject.Input) async throws + -> Operations.deleteProject.Output + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/complete`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)`. + func completeAnalyticsArtifactMultipartUpload( + _ input: Operations.completeAnalyticsArtifactMultipartUpload.Input + ) async throws -> Operations.completeAnalyticsArtifactMultipartUpload.Output + /// Completes artifacts uploads for a given command event + /// + /// Given a command event, it marks all artifact uploads as finished and does extra processing of a given command run, such as test flakiness detection. + /// + /// - Remark: HTTP `PUT /api/runs/{run_id}/complete_artifacts_uploads`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)`. + func completeAnalyticsArtifactsUploads( + _ input: Operations.completeAnalyticsArtifactsUploads.Input + ) async throws -> Operations.completeAnalyticsArtifactsUploads.Output + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/generate-url`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)`. + func generateAnalyticsArtifactMultipartUploadURL( + _ input: Operations.generateAnalyticsArtifactMultipartUploadURL.Input + ) async throws -> Operations.generateAnalyticsArtifactMultipartUploadURL.Output + /// It initiates a multipart upload for a command event artifact. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/start`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)`. + func startAnalyticsArtifactMultipartUpload( + _ input: Operations.startAnalyticsArtifactMultipartUpload.Input + ) async throws -> Operations.startAnalyticsArtifactMultipartUpload.Output +} +/// Server URLs defined in the OpenAPI document. +public enum Servers { + public static func server1() throws -> URL { + try URL(validatingOpenAPIServerURL: "http://localhost:8080") + } +} +/// Types generated from the components section of the OpenAPI document. +public enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + public enum Schemas { + /// - Remark: Generated from `#/components/schemas/AbsentCacheArtifact`. + public struct AbsentCacheArtifact: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AbsentCacheArtifact/errorPayload`. + public struct errorPayloadPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AbsentCacheArtifact/errorPayload/code`. + public var code: Swift.String? + /// - Remark: Generated from `#/components/schemas/AbsentCacheArtifact/errorPayload/message`. + public var message: Swift.String? + /// Creates a new `errorPayloadPayload`. + /// + /// - Parameters: + /// - code: + /// - message: + public init(code: Swift.String? = nil, message: Swift.String? = nil) { + self.code = code + self.message = message + } + public enum CodingKeys: String, CodingKey { + case code + case message + } + } + /// - Remark: Generated from `#/components/schemas/AbsentCacheArtifact/error`. + public typealias errorPayload = [Components.Schemas.AbsentCacheArtifact + .errorPayloadPayload] + /// - Remark: Generated from `#/components/schemas/AbsentCacheArtifact/error`. + public var error: Components.Schemas.AbsentCacheArtifact.errorPayload? + /// Creates a new `AbsentCacheArtifact`. + /// + /// - Parameters: + /// - error: + public init(error: Components.Schemas.AbsentCacheArtifact.errorPayload? = nil) { + self.error = error + } + public enum CodingKeys: String, CodingKey { case error } + } + /// The URL to download an artifact. + /// + /// - Remark: Generated from `#/components/schemas/ArtifactDownloadURL`. + public struct ArtifactDownloadURL: Codable, Equatable, Hashable, Sendable { + /// The UNIX timestamp when the URL expires. + /// + /// - Remark: Generated from `#/components/schemas/ArtifactDownloadURL/expires_at`. + public var expires_at: Swift.Int + /// The URL to download the artifact. + /// + /// - Remark: Generated from `#/components/schemas/ArtifactDownloadURL/url`. + public var url: Swift.String + /// Creates a new `ArtifactDownloadURL`. + /// + /// - Parameters: + /// - expires_at: The UNIX timestamp when the URL expires. + /// - url: The URL to download the artifact. + public init(expires_at: Swift.Int, url: Swift.String) { + self.expires_at = expires_at + self.url = url + } + public enum CodingKeys: String, CodingKey { + case expires_at + case url + } + } + /// Represents an multipart upload's part identified by the upload id and the part number + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadPart`. + public struct ArtifactMultipartUploadPart: Codable, Equatable, Hashable, Sendable { + /// The part number of the multipart upload. + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadPart/part_number`. + public var part_number: Swift.Int + /// The upload ID. + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadPart/upload_id`. + public var upload_id: Swift.String + /// Creates a new `ArtifactMultipartUploadPart`. + /// + /// - Parameters: + /// - part_number: The part number of the multipart upload. + /// - upload_id: The upload ID. + public init(part_number: Swift.Int, upload_id: Swift.String) { + self.part_number = part_number + self.upload_id = upload_id + } + public enum CodingKeys: String, CodingKey { + case part_number + case upload_id + } + } + /// It represents a part that has been uploaded using multipart uploads. The part is identified by its number and the etag + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts`. + public struct ArtifactMultipartUploadParts: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts/partsPayload`. + public struct partsPayloadPayload: Codable, Equatable, Hashable, Sendable { + /// The ETag of the part + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts/partsPayload/etag`. + public var etag: Swift.String + /// The part number + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts/partsPayload/part_number`. + public var part_number: Swift.Int + /// Creates a new `partsPayloadPayload`. + /// + /// - Parameters: + /// - etag: The ETag of the part + /// - part_number: The part number + public init(etag: Swift.String, part_number: Swift.Int) { + self.etag = etag + self.part_number = part_number + } + public enum CodingKeys: String, CodingKey { + case etag + case part_number + } + } + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts/parts`. + public typealias partsPayload = [Components.Schemas.ArtifactMultipartUploadParts + .partsPayloadPayload] + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts/parts`. + public var parts: Components.Schemas.ArtifactMultipartUploadParts.partsPayload + /// The upload ID + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadParts/upload_id`. + public var upload_id: Swift.String + /// Creates a new `ArtifactMultipartUploadParts`. + /// + /// - Parameters: + /// - parts: + /// - upload_id: The upload ID + public init( + parts: Components.Schemas.ArtifactMultipartUploadParts.partsPayload, + upload_id: Swift.String + ) { + self.parts = parts + self.upload_id = upload_id + } + public enum CodingKeys: String, CodingKey { + case parts + case upload_id + } + } + /// The URL to upload a multipart part + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadURL`. + public struct ArtifactMultipartUploadURL: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadURL/data`. + public struct dataPayload: Codable, Equatable, Hashable, Sendable { + /// The URL to upload the part + /// + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadURL/data/url`. + public var url: Swift.String + /// Creates a new `dataPayload`. + /// + /// - Parameters: + /// - url: The URL to upload the part + public init(url: Swift.String) { self.url = url } + public enum CodingKeys: String, CodingKey { case url } + } + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadURL/data`. + public var data: Components.Schemas.ArtifactMultipartUploadURL.dataPayload + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadURL/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/components/schemas/ArtifactMultipartUploadURL/status`. + public var status: Components.Schemas.ArtifactMultipartUploadURL.statusPayload + /// Creates a new `ArtifactMultipartUploadURL`. + /// + /// - Parameters: + /// - data: + /// - status: + public init( + data: Components.Schemas.ArtifactMultipartUploadURL.dataPayload, + status: Components.Schemas.ArtifactMultipartUploadURL.statusPayload + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + /// The upload has been initiated and a ID is returned to upload the various parts using multi-part uploads + /// + /// - Remark: Generated from `#/components/schemas/ArtifactUploadID`. + public struct ArtifactUploadID: Codable, Equatable, Hashable, Sendable { + /// Data that contains ID that's associated with the multipart upload to use when uploading parts + /// + /// - Remark: Generated from `#/components/schemas/ArtifactUploadID/data`. + public struct dataPayload: Codable, Equatable, Hashable, Sendable { + /// The upload ID + /// + /// - Remark: Generated from `#/components/schemas/ArtifactUploadID/data/upload_id`. + public var upload_id: Swift.String + /// Creates a new `dataPayload`. + /// + /// - Parameters: + /// - upload_id: The upload ID + public init(upload_id: Swift.String) { self.upload_id = upload_id } + public enum CodingKeys: String, CodingKey { case upload_id } + } + /// Data that contains ID that's associated with the multipart upload to use when uploading parts + /// + /// - Remark: Generated from `#/components/schemas/ArtifactUploadID/data`. + public var data: Components.Schemas.ArtifactUploadID.dataPayload + /// - Remark: Generated from `#/components/schemas/ArtifactUploadID/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/components/schemas/ArtifactUploadID/status`. + public var status: Components.Schemas.ArtifactUploadID.statusPayload + /// Creates a new `ArtifactUploadID`. + /// + /// - Parameters: + /// - data: Data that contains ID that's associated with the multipart upload to use when uploading parts + /// - status: + public init( + data: Components.Schemas.ArtifactUploadID.dataPayload, + status: Components.Schemas.ArtifactUploadID.statusPayload + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + /// A pair of access token to authenticate requests and refresh token to generate new access tokens when they expire. + /// + /// - Remark: Generated from `#/components/schemas/AuthenticationTokens`. + public struct AuthenticationTokens: Codable, Equatable, Hashable, Sendable { + /// API access token. + /// + /// - Remark: Generated from `#/components/schemas/AuthenticationTokens/access_token`. + public var access_token: Swift.String + /// A token to generate new API access tokens when they expire. + /// + /// - Remark: Generated from `#/components/schemas/AuthenticationTokens/refresh_token`. + public var refresh_token: Swift.String + /// Creates a new `AuthenticationTokens`. + /// + /// - Parameters: + /// - access_token: API access token. + /// - refresh_token: A token to generate new API access tokens when they expire. + public init(access_token: Swift.String, refresh_token: Swift.String) { + self.access_token = access_token + self.refresh_token = refresh_token + } + public enum CodingKeys: String, CodingKey { + case access_token + case refresh_token + } + } + /// The URL to download a build. + /// + /// - Remark: Generated from `#/components/schemas/BuildDownloadURL`. + public struct BuildDownloadURL: Codable, Equatable, Hashable, Sendable { + /// The UNIX timestamp when the URL expires. + /// + /// - Remark: Generated from `#/components/schemas/BuildDownloadURL/expires_at`. + public var expires_at: Swift.Int + /// The URL to download the artifact from the cache. + /// + /// - Remark: Generated from `#/components/schemas/BuildDownloadURL/url`. + public var url: Swift.String + /// Creates a new `BuildDownloadURL`. + /// + /// - Parameters: + /// - expires_at: The UNIX timestamp when the URL expires. + /// - url: The URL to download the artifact from the cache. + public init(expires_at: Swift.Int, url: Swift.String) { + self.expires_at = expires_at + self.url = url + } + public enum CodingKeys: String, CodingKey { + case expires_at + case url + } + } + /// The URL to download the artifact from the cache. + /// + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL`. + public struct CacheArtifactDownloadURL: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL/data`. + public struct dataPayload: Codable, Equatable, Hashable, Sendable { + /// The UNIX timestamp when the URL expires. + /// + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL/data/expires_at`. + public var expires_at: Swift.Int + /// The URL to download the artifact from the cache. + /// + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL/data/url`. + public var url: Swift.String + /// Creates a new `dataPayload`. + /// + /// - Parameters: + /// - expires_at: The UNIX timestamp when the URL expires. + /// - url: The URL to download the artifact from the cache. + public init(expires_at: Swift.Int, url: Swift.String) { + self.expires_at = expires_at + self.url = url + } + public enum CodingKeys: String, CodingKey { + case expires_at + case url + } + } + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL/data`. + public var data: Components.Schemas.CacheArtifactDownloadURL.dataPayload + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/components/schemas/CacheArtifactDownloadURL/status`. + public var status: Components.Schemas.CacheArtifactDownloadURL.statusPayload + /// Creates a new `CacheArtifactDownloadURL`. + /// + /// - Parameters: + /// - data: + /// - status: + public init( + data: Components.Schemas.CacheArtifactDownloadURL.dataPayload, + status: Components.Schemas.CacheArtifactDownloadURL.statusPayload + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + /// The artifact exists in the cache and can be downloaded + /// + /// - Remark: Generated from `#/components/schemas/CacheArtifactExistence`. + public struct CacheArtifactExistence: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/CacheArtifactExistence/data`. + public var data: OpenAPIRuntime.OpenAPIObjectContainer? + /// - Remark: Generated from `#/components/schemas/CacheArtifactExistence/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/components/schemas/CacheArtifactExistence/status`. + public var status: Components.Schemas.CacheArtifactExistence.statusPayload? + /// Creates a new `CacheArtifactExistence`. + /// + /// - Parameters: + /// - data: + /// - status: + public init( + data: OpenAPIRuntime.OpenAPIObjectContainer? = nil, + status: Components.Schemas.CacheArtifactExistence.statusPayload? = nil + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + /// This response confirms that the upload has been completed successfully. The cache will now be able to serve the artifact. + /// + /// - Remark: Generated from `#/components/schemas/CacheArtifactMultipartUploadCompletion`. + public struct CacheArtifactMultipartUploadCompletion: Codable, Equatable, Hashable, Sendable + { + /// - Remark: Generated from `#/components/schemas/CacheArtifactMultipartUploadCompletion/data`. + public var data: OpenAPIRuntime.OpenAPIObjectContainer? + /// - Remark: Generated from `#/components/schemas/CacheArtifactMultipartUploadCompletion/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/components/schemas/CacheArtifactMultipartUploadCompletion/status`. + public var status: + Components.Schemas.CacheArtifactMultipartUploadCompletion.statusPayload? + /// Creates a new `CacheArtifactMultipartUploadCompletion`. + /// + /// - Parameters: + /// - data: + /// - status: + public init( + data: OpenAPIRuntime.OpenAPIObjectContainer? = nil, + status: Components.Schemas.CacheArtifactMultipartUploadCompletion.statusPayload? = + nil + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + /// The category of the cache. + /// + /// - Remark: Generated from `#/components/schemas/CacheCategory`. + @frozen + public enum CacheCategory: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case tests + case builds + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "tests": self = .tests + case "builds": self = .builds + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .tests: return "tests" + case .builds: return "builds" + } + } + public static var allCases: [CacheCategory] { [.tests, .builds] } + } + /// The schema for the command analytics event. + /// + /// - Remark: Generated from `#/components/schemas/CommandEvent`. + public struct CommandEvent: Codable, Equatable, Hashable, Sendable { + /// ID of the command event + /// + /// - Remark: Generated from `#/components/schemas/CommandEvent/id`. + public var id: Swift.Double + /// Name of the command + /// + /// - Remark: Generated from `#/components/schemas/CommandEvent/name`. + public var name: Swift.String + /// URL to the command event + /// + /// - Remark: Generated from `#/components/schemas/CommandEvent/url`. + public var url: Swift.String + /// Creates a new `CommandEvent`. + /// + /// - Parameters: + /// - id: ID of the command event + /// - name: Name of the command + /// - url: URL to the command event + public init(id: Swift.Double, name: Swift.String, url: Swift.String) { + self.id = id + self.name = name + self.url = url + } + public enum CodingKeys: String, CodingKey { + case id + case name + case url + } + } + /// It represents an artifact that's associated with a command event (e.g. result bundles) + /// + /// - Remark: Generated from `#/components/schemas/CommandEventArtifact`. + public struct CommandEventArtifact: Codable, Equatable, Hashable, Sendable { + /// The name of the file. It's used only for certain types such as result_bundle_object + /// + /// - Remark: Generated from `#/components/schemas/CommandEventArtifact/name`. + public var name: Swift.String? + /// The command event artifact type. It can be: + /// - result_bundle: A result bundle artifact that represents the whole `.xcresult` bundle + /// - invocation_record: An invocation record artifact. This is a root bundle object of the result bundle + /// - result_bundle_object: A result bundle object. There are many different bundle objects per result bundle. + /// + /// + /// - Remark: Generated from `#/components/schemas/CommandEventArtifact/type`. + @frozen + public enum _typePayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case result_bundle + case invocation_record + case result_bundle_object + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "result_bundle": self = .result_bundle + case "invocation_record": self = .invocation_record + case "result_bundle_object": self = .result_bundle_object + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .result_bundle: return "result_bundle" + case .invocation_record: return "invocation_record" + case .result_bundle_object: return "result_bundle_object" + } + } + public static var allCases: [_typePayload] { + [.result_bundle, .invocation_record, .result_bundle_object] + } + } + /// The command event artifact type. It can be: + /// - result_bundle: A result bundle artifact that represents the whole `.xcresult` bundle + /// - invocation_record: An invocation record artifact. This is a root bundle object of the result bundle + /// - result_bundle_object: A result bundle object. There are many different bundle objects per result bundle. + /// + /// + /// - Remark: Generated from `#/components/schemas/CommandEventArtifact/type`. + public var _type: Components.Schemas.CommandEventArtifact._typePayload + /// Creates a new `CommandEventArtifact`. + /// + /// - Parameters: + /// - name: The name of the file. It's used only for certain types such as result_bundle_object + /// - _type: The command event artifact type. It can be: + public init( + name: Swift.String? = nil, + _type: Components.Schemas.CommandEventArtifact._typePayload + ) { + self.name = name + self._type = _type + } + public enum CodingKeys: String, CodingKey { + case name + case _type = "type" + } + } + /// Token to authenticate the user with. + /// + /// - Remark: Generated from `#/components/schemas/DeviceCodeAuthenticationTokens`. + public struct DeviceCodeAuthenticationTokens: Codable, Equatable, Hashable, Sendable { + /// A short-lived token to authenticate API requests as user. + /// + /// - Remark: Generated from `#/components/schemas/DeviceCodeAuthenticationTokens/access_token`. + public var access_token: Swift.String? + /// A token to generate new access tokens when they expire. + /// + /// - Remark: Generated from `#/components/schemas/DeviceCodeAuthenticationTokens/refresh_token`. + public var refresh_token: Swift.String? + /// User authentication token + /// + /// - Remark: Generated from `#/components/schemas/DeviceCodeAuthenticationTokens/token`. + @available(*, deprecated) public var token: Swift.String? + /// Creates a new `DeviceCodeAuthenticationTokens`. + /// + /// - Parameters: + /// - access_token: A short-lived token to authenticate API requests as user. + /// - refresh_token: A token to generate new access tokens when they expire. + /// - token: User authentication token + public init( + access_token: Swift.String? = nil, + refresh_token: Swift.String? = nil, + token: Swift.String? = nil + ) { + self.access_token = access_token + self.refresh_token = refresh_token + self.token = token + } + public enum CodingKeys: String, CodingKey { + case access_token + case refresh_token + case token + } + } + /// - Remark: Generated from `#/components/schemas/Error`. + public struct _Error: Codable, Equatable, Hashable, Sendable { + /// The error message + /// + /// - Remark: Generated from `#/components/schemas/Error/message`. + public var message: Swift.String + /// Creates a new `_Error`. + /// + /// - Parameters: + /// - message: The error message + public init(message: Swift.String) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + /// - Remark: Generated from `#/components/schemas/Invitation`. + public struct Invitation: Codable, Equatable, Hashable, Sendable { + /// The invitation's unique identifier + /// + /// - Remark: Generated from `#/components/schemas/Invitation/id`. + public var id: Swift.Double + /// The email of the invitee + /// + /// - Remark: Generated from `#/components/schemas/Invitation/invitee_email`. + public var invitee_email: Swift.String + /// - Remark: Generated from `#/components/schemas/Invitation/inviter`. + public var inviter: Components.Schemas.User + /// The id of the organization the invitee is invited to + /// + /// - Remark: Generated from `#/components/schemas/Invitation/organization_id`. + public var organization_id: Swift.Double + /// The token to accept the invitation + /// + /// - Remark: Generated from `#/components/schemas/Invitation/token`. + public var token: Swift.String + /// Creates a new `Invitation`. + /// + /// - Parameters: + /// - id: The invitation's unique identifier + /// - invitee_email: The email of the invitee + /// - inviter: + /// - organization_id: The id of the organization the invitee is invited to + /// - token: The token to accept the invitation + public init( + id: Swift.Double, + invitee_email: Swift.String, + inviter: Components.Schemas.User, + organization_id: Swift.Double, + token: Swift.String + ) { + self.id = id + self.invitee_email = invitee_email + self.inviter = inviter + self.organization_id = organization_id + self.token = token + } + public enum CodingKeys: String, CodingKey { + case id + case invitee_email + case inviter + case organization_id + case token + } + } + /// - Remark: Generated from `#/components/schemas/Module`. + public struct Module: Codable, Equatable, Hashable, Sendable { + /// A hash that represents the module. + /// + /// - Remark: Generated from `#/components/schemas/Module/hash`. + public var hash: Swift.String + /// A name of the module + /// + /// - Remark: Generated from `#/components/schemas/Module/name`. + public var name: Swift.String + /// Project's relative path from the root of the repository + /// + /// - Remark: Generated from `#/components/schemas/Module/project_identifier`. + public var project_identifier: Swift.String + /// Creates a new `Module`. + /// + /// - Parameters: + /// - hash: A hash that represents the module. + /// - name: A name of the module + /// - project_identifier: Project's relative path from the root of the repository + public init(hash: Swift.String, name: Swift.String, project_identifier: Swift.String) { + self.hash = hash + self.name = name + self.project_identifier = project_identifier + } + public enum CodingKeys: String, CodingKey { + case hash + case name + case project_identifier + } + } + /// An organization + /// + /// - Remark: Generated from `#/components/schemas/Organization`. + public struct Organization: Codable, Equatable, Hashable, Sendable { + /// The organization's unique identifier + /// + /// - Remark: Generated from `#/components/schemas/Organization/id`. + public var id: Swift.Double + /// A list of organization invitations + /// + /// - Remark: Generated from `#/components/schemas/Organization/invitations`. + public var invitations: [Components.Schemas.Invitation] + /// A list of organization members + /// + /// - Remark: Generated from `#/components/schemas/Organization/members`. + public var members: [Components.Schemas.OrganizationMember] + /// The organization's name + /// + /// - Remark: Generated from `#/components/schemas/Organization/name`. + public var name: Swift.String + /// The plan associated with the organization + /// + /// - Remark: Generated from `#/components/schemas/Organization/plan`. + @frozen + public enum planPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case air + case pro + case enterprise + case none + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "air": self = .air + case "pro": self = .pro + case "enterprise": self = .enterprise + case "none": self = .none + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .air: return "air" + case .pro: return "pro" + case .enterprise: return "enterprise" + case .none: return "none" + } + } + public static var allCases: [planPayload] { [.air, .pro, .enterprise, .none] } + } + /// The plan associated with the organization + /// + /// - Remark: Generated from `#/components/schemas/Organization/plan`. + public var plan: Components.Schemas.Organization.planPayload + /// The organization ID associated with the SSO provider + /// + /// - Remark: Generated from `#/components/schemas/Organization/sso_organization_id`. + public var sso_organization_id: Swift.String? + /// The SSO provider set up for the organization + /// + /// - Remark: Generated from `#/components/schemas/Organization/sso_provider`. + @frozen + public enum sso_providerPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case google + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "google": self = .google + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .google: return "google" + } + } + public static var allCases: [sso_providerPayload] { [.google] } + } + /// The SSO provider set up for the organization + /// + /// - Remark: Generated from `#/components/schemas/Organization/sso_provider`. + public var sso_provider: Components.Schemas.Organization.sso_providerPayload? + /// Creates a new `Organization`. + /// + /// - Parameters: + /// - id: The organization's unique identifier + /// - invitations: A list of organization invitations + /// - members: A list of organization members + /// - name: The organization's name + /// - plan: The plan associated with the organization + /// - sso_organization_id: The organization ID associated with the SSO provider + /// - sso_provider: The SSO provider set up for the organization + public init( + id: Swift.Double, + invitations: [Components.Schemas.Invitation], + members: [Components.Schemas.OrganizationMember], + name: Swift.String, + plan: Components.Schemas.Organization.planPayload, + sso_organization_id: Swift.String? = nil, + sso_provider: Components.Schemas.Organization.sso_providerPayload? = nil + ) { + self.id = id + self.invitations = invitations + self.members = members + self.name = name + self.plan = plan + self.sso_organization_id = sso_organization_id + self.sso_provider = sso_provider + } + public enum CodingKeys: String, CodingKey { + case id + case invitations + case members + case name + case plan + case sso_organization_id + case sso_provider + } + } + /// The list of organizations the authenticated subject is part of. + /// + /// - Remark: Generated from `#/components/schemas/OrganizationList`. + public struct OrganizationList: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/OrganizationList/organizations`. + public var organizations: [Components.Schemas.Organization] + /// Creates a new `OrganizationList`. + /// + /// - Parameters: + /// - organizations: + public init(organizations: [Components.Schemas.Organization]) { + self.organizations = organizations + } + public enum CodingKeys: String, CodingKey { case organizations } + } + /// An organization member + /// + /// - Remark: Generated from `#/components/schemas/OrganizationMember`. + public struct OrganizationMember: Codable, Equatable, Hashable, Sendable { + /// The organization member's email + /// + /// - Remark: Generated from `#/components/schemas/OrganizationMember/email`. + public var email: Swift.String + /// The organization member's unique identifier + /// + /// - Remark: Generated from `#/components/schemas/OrganizationMember/id`. + public var id: Swift.Double + /// The organization member's name + /// + /// - Remark: Generated from `#/components/schemas/OrganizationMember/name`. + public var name: Swift.String + /// The organization member's role + /// + /// - Remark: Generated from `#/components/schemas/OrganizationMember/role`. + @frozen + public enum rolePayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case admin + case user + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "admin": self = .admin + case "user": self = .user + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .admin: return "admin" + case .user: return "user" + } + } + public static var allCases: [rolePayload] { [.admin, .user] } + } + /// The organization member's role + /// + /// - Remark: Generated from `#/components/schemas/OrganizationMember/role`. + public var role: Components.Schemas.OrganizationMember.rolePayload + /// Creates a new `OrganizationMember`. + /// + /// - Parameters: + /// - email: The organization member's email + /// - id: The organization member's unique identifier + /// - name: The organization member's name + /// - role: The organization member's role + public init( + email: Swift.String, + id: Swift.Double, + name: Swift.String, + role: Components.Schemas.OrganizationMember.rolePayload + ) { + self.email = email + self.id = id + self.name = name + self.role = role + } + public enum CodingKeys: String, CodingKey { + case email + case id + case name + case role + } + } + /// The usage of an organization. + /// + /// - Remark: Generated from `#/components/schemas/OrganizationUsage`. + public struct OrganizationUsage: Codable, Equatable, Hashable, Sendable { + /// The number of remote cache hits in the current month + /// + /// - Remark: Generated from `#/components/schemas/OrganizationUsage/current_month_remote_cache_hits`. + public var current_month_remote_cache_hits: Swift.Double + /// Creates a new `OrganizationUsage`. + /// + /// - Parameters: + /// - current_month_remote_cache_hits: The number of remote cache hits in the current month + public init(current_month_remote_cache_hits: Swift.Double) { + self.current_month_remote_cache_hits = current_month_remote_cache_hits + } + public enum CodingKeys: String, CodingKey { case current_month_remote_cache_hits } + } + /// The upload has been initiated and preview and upload unique identifier are returned to upload the various parts using multi-part uploads + /// + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload`. + public struct PreviewArtifactUpload: Codable, Equatable, Hashable, Sendable { + /// Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + /// + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload/data`. + public struct dataPayload: Codable, Equatable, Hashable, Sendable { + /// The id of the preview. + /// + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload/data/preview_id`. + public var preview_id: Swift.String + /// The upload ID + /// + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload/data/upload_id`. + public var upload_id: Swift.String + /// Creates a new `dataPayload`. + /// + /// - Parameters: + /// - preview_id: The id of the preview. + /// - upload_id: The upload ID + public init(preview_id: Swift.String, upload_id: Swift.String) { + self.preview_id = preview_id + self.upload_id = upload_id + } + public enum CodingKeys: String, CodingKey { + case preview_id + case upload_id + } + } + /// Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + /// + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload/data`. + public var data: Components.Schemas.PreviewArtifactUpload.dataPayload + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/components/schemas/PreviewArtifactUpload/status`. + public var status: Components.Schemas.PreviewArtifactUpload.statusPayload + /// Creates a new `PreviewArtifactUpload`. + /// + /// - Parameters: + /// - data: Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + /// - status: + public init( + data: Components.Schemas.PreviewArtifactUpload.dataPayload, + status: Components.Schemas.PreviewArtifactUpload.statusPayload + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + /// The preview multipart upload has been completed + /// + /// - Remark: Generated from `#/components/schemas/PreviewUploadCompletion`. + public struct PreviewUploadCompletion: Codable, Equatable, Hashable, Sendable { + /// The URL to download the preview + /// + /// - Remark: Generated from `#/components/schemas/PreviewUploadCompletion/url`. + public var url: Swift.String + /// Creates a new `PreviewUploadCompletion`. + /// + /// - Parameters: + /// - url: The URL to download the preview + public init(url: Swift.String) { self.url = url } + public enum CodingKeys: String, CodingKey { case url } + } + /// - Remark: Generated from `#/components/schemas/Project`. + public struct Project: Codable, Equatable, Hashable, Sendable { + /// The default branch of the project. + /// + /// - Remark: Generated from `#/components/schemas/Project/default_branch`. + public var default_branch: Swift.String + /// The full name of the project (e.g. tuist/tuist) + /// + /// - Remark: Generated from `#/components/schemas/Project/full_name`. + public var full_name: Swift.String + /// ID of the project + /// + /// - Remark: Generated from `#/components/schemas/Project/id`. + public var id: Swift.Double + /// The token that should be used to authenticate the project. For CI only. + /// + /// - Remark: Generated from `#/components/schemas/Project/token`. + @available(*, deprecated) public var token: Swift.String + /// Creates a new `Project`. + /// + /// - Parameters: + /// - default_branch: The default branch of the project. + /// - full_name: The full name of the project (e.g. tuist/tuist) + /// - id: ID of the project + /// - token: The token that should be used to authenticate the project. For CI only. + public init( + default_branch: Swift.String, + full_name: Swift.String, + id: Swift.Double, + token: Swift.String + ) { + self.default_branch = default_branch + self.full_name = full_name + self.id = id + self.token = token + } + public enum CodingKeys: String, CodingKey { + case default_branch + case full_name + case id + case token + } + } + /// A new project token. + /// + /// - Remark: Generated from `#/components/schemas/ProjectFullToken`. + public struct ProjectFullToken: Codable, Equatable, Hashable, Sendable { + /// The generated project token. + /// + /// - Remark: Generated from `#/components/schemas/ProjectFullToken/token`. + public var token: Swift.String + /// Creates a new `ProjectFullToken`. + /// + /// - Parameters: + /// - token: The generated project token. + public init(token: Swift.String) { self.token = token } + public enum CodingKeys: String, CodingKey { case token } + } + /// A token to authenticate API requests as a project. + /// + /// - Remark: Generated from `#/components/schemas/ProjectToken`. + public struct ProjectToken: Codable, Equatable, Hashable, Sendable { + /// The token unique identifier. + /// + /// - Remark: Generated from `#/components/schemas/ProjectToken/id`. + public var id: Swift.String + /// The timestamp of when the token was created. + /// + /// - Remark: Generated from `#/components/schemas/ProjectToken/inserted_at`. + public var inserted_at: Foundation.Date + /// Creates a new `ProjectToken`. + /// + /// - Parameters: + /// - id: The token unique identifier. + /// - inserted_at: The timestamp of when the token was created. + public init(id: Swift.String, inserted_at: Foundation.Date) { + self.id = id + self.inserted_at = inserted_at + } + public enum CodingKeys: String, CodingKey { + case id + case inserted_at + } + } + /// A list of project tokens. + /// + /// - Remark: Generated from `#/components/schemas/Tokens`. + public struct Tokens: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/Tokens/tokens`. + public var tokens: [Components.Schemas.ProjectToken] + /// Creates a new `Tokens`. + /// + /// - Parameters: + /// - tokens: + public init(tokens: [Components.Schemas.ProjectToken]) { self.tokens = tokens } + public enum CodingKeys: String, CodingKey { case tokens } + } + /// A user. + /// + /// - Remark: Generated from `#/components/schemas/User`. + public struct User: Codable, Equatable, Hashable, Sendable { + /// The user's email + /// + /// - Remark: Generated from `#/components/schemas/User/email`. + public var email: Swift.String + /// The user's unique identifier + /// + /// - Remark: Generated from `#/components/schemas/User/id`. + public var id: Swift.Double + /// The user's name + /// + /// - Remark: Generated from `#/components/schemas/User/name`. + public var name: Swift.String + /// Creates a new `User`. + /// + /// - Parameters: + /// - email: The user's email + /// - id: The user's unique identifier + /// - name: The user's name + public init(email: Swift.String, id: Swift.Double, name: Swift.String) { + self.email = email + self.id = id + self.name = name + } + public enum CodingKeys: String, CodingKey { + case email + case id + case name + } + } + } + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + public enum Parameters {} + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + public enum RequestBodies {} + /// Types generated from the `#/components/responses` section of the OpenAPI document. + public enum Responses {} + /// Types generated from the `#/components/headers` section of the OpenAPI document. + public enum Headers {} +} +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +public enum Operations { + /// Create a a new command analytics event + /// + /// - Remark: HTTP `POST /api/analytics`. + /// - Remark: Generated from `#/paths//api/analytics/post(createCommandEvent)`. + public enum createCommandEvent { + public static let id: String = "createCommandEvent" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.createCommandEvent.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var project_id: Swift.String + /// Creates a new `Query`. + /// + /// - Parameters: + /// - project_id: + public init(project_id: Swift.String) { self.project_id = project_id } + } + public var query: Operations.createCommandEvent.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.createCommandEvent.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.createCommandEvent.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Command event params + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The client id of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/client_id`. + public var client_id: Swift.String + /// The arguments of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/command_arguments`. + public var command_arguments: [Swift.String]? + /// The duration of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/duration`. + public var duration: Swift.Double + /// The error message of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/error_message`. + public var error_message: Swift.String? + /// Whether the command was run in a CI environment. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/is_ci`. + public var is_ci: Swift.Bool + /// The version of macOS that ran the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/macos_version`. + public var macos_version: Swift.String + /// The name of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/name`. + public var name: Swift.String + /// Extra parameters. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params`. + public struct paramsPayload: Codable, Equatable, Hashable, Sendable { + /// A list of cacheable targets. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params/cacheable_targets`. + public var cacheable_targets: [Swift.String]? + /// A list of local cache target hits. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params/local_cache_target_hits`. + public var local_cache_target_hits: [Swift.String]? + /// A list of local targets whose tests were skipped. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params/local_test_target_hits`. + public var local_test_target_hits: [Swift.String]? + /// A list of remote cache target hits. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params/remote_cache_target_hits`. + public var remote_cache_target_hits: [Swift.String]? + /// A list of remote targets whose tests were skipped. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params/remote_test_target_hits`. + public var remote_test_target_hits: [Swift.String]? + /// The list of targets that were tested. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params/test_targets`. + public var test_targets: [Swift.String]? + /// Creates a new `paramsPayload`. + /// + /// - Parameters: + /// - cacheable_targets: A list of cacheable targets. + /// - local_cache_target_hits: A list of local cache target hits. + /// - local_test_target_hits: A list of local targets whose tests were skipped. + /// - remote_cache_target_hits: A list of remote cache target hits. + /// - remote_test_target_hits: A list of remote targets whose tests were skipped. + /// - test_targets: The list of targets that were tested. + public init( + cacheable_targets: [Swift.String]? = nil, + local_cache_target_hits: [Swift.String]? = nil, + local_test_target_hits: [Swift.String]? = nil, + remote_cache_target_hits: [Swift.String]? = nil, + remote_test_target_hits: [Swift.String]? = nil, + test_targets: [Swift.String]? = nil + ) { + self.cacheable_targets = cacheable_targets + self.local_cache_target_hits = local_cache_target_hits + self.local_test_target_hits = local_test_target_hits + self.remote_cache_target_hits = remote_cache_target_hits + self.remote_test_target_hits = remote_test_target_hits + self.test_targets = test_targets + } + public enum CodingKeys: String, CodingKey { + case cacheable_targets + case local_cache_target_hits + case local_test_target_hits + case remote_cache_target_hits + case remote_test_target_hits + case test_targets + } + } + /// Extra parameters. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/params`. + public var params: + Operations.createCommandEvent.Input.Body.jsonPayload.paramsPayload? + /// The status of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case success + case failure + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + case "failure": self = .failure + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + case .failure: return "failure" + } + } + public static var allCases: [statusPayload] { [.success, .failure] } + } + /// The status of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/status`. + public var status: + Operations.createCommandEvent.Input.Body.jsonPayload.statusPayload? + /// The subcommand of the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/subcommand`. + public var subcommand: Swift.String? + /// The version of Swift that ran the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/swift_version`. + public var swift_version: Swift.String + /// The version of Tuist that ran the command. + /// + /// - Remark: Generated from `#/paths/api/analytics/POST/json/tuist_version`. + public var tuist_version: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - client_id: The client id of the command. + /// - command_arguments: The arguments of the command. + /// - duration: The duration of the command. + /// - error_message: The error message of the command. + /// - is_ci: Whether the command was run in a CI environment. + /// - macos_version: The version of macOS that ran the command. + /// - name: The name of the command. + /// - params: Extra parameters. + /// - status: The status of the command. + /// - subcommand: The subcommand of the command. + /// - swift_version: The version of Swift that ran the command. + /// - tuist_version: The version of Tuist that ran the command. + public init( + client_id: Swift.String, + command_arguments: [Swift.String]? = nil, + duration: Swift.Double, + error_message: Swift.String? = nil, + is_ci: Swift.Bool, + macos_version: Swift.String, + name: Swift.String, + params: Operations.createCommandEvent.Input.Body.jsonPayload + .paramsPayload? = nil, + status: Operations.createCommandEvent.Input.Body.jsonPayload + .statusPayload? = nil, + subcommand: Swift.String? = nil, + swift_version: Swift.String, + tuist_version: Swift.String + ) { + self.client_id = client_id + self.command_arguments = command_arguments + self.duration = duration + self.error_message = error_message + self.is_ci = is_ci + self.macos_version = macos_version + self.name = name + self.params = params + self.status = status + self.subcommand = subcommand + self.swift_version = swift_version + self.tuist_version = tuist_version + } + public enum CodingKeys: String, CodingKey { + case client_id + case command_arguments + case duration + case error_message + case is_ci + case macos_version + case name + case params + case status + case subcommand + case swift_version + case tuist_version + } + } + case json(Operations.createCommandEvent.Input.Body.jsonPayload) + } + public var body: Operations.createCommandEvent.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.createCommandEvent.Input.Path = .init(), + query: Operations.createCommandEvent.Input.Query, + headers: Operations.createCommandEvent.Input.Headers = .init(), + cookies: Operations.createCommandEvent.Input.Cookies = .init(), + body: Operations.createCommandEvent.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createCommandEvent.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.CommandEvent) + } + /// Received HTTP response body + public var body: Operations.createCommandEvent.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createCommandEvent.Output.Ok.Headers = .init(), + body: Operations.createCommandEvent.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The command event was created + /// + /// - Remark: Generated from `#/paths//api/analytics/post(createCommandEvent)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.createCommandEvent.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createCommandEvent.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createCommandEvent.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createCommandEvent.Output.Unauthorized.Headers = .init(), + body: Operations.createCommandEvent.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/analytics/post(createCommandEvent)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.createCommandEvent.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createCommandEvent.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createCommandEvent.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createCommandEvent.Output.Forbidden.Headers = .init(), + body: Operations.createCommandEvent.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// You don't have permission to create command events for the project. + /// + /// - Remark: Generated from `#/paths//api/analytics/post(createCommandEvent)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.createCommandEvent.Output.Forbidden) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Authenticate with email and password. + /// + /// This endpoint returns API tokens for a given email and password. + /// + /// - Remark: HTTP `POST /api/auth`. + /// - Remark: Generated from `#/paths//api/auth/post(authenticate)`. + public enum authenticate { + public static let id: String = "authenticate" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.authenticate.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.authenticate.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.authenticate.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.authenticate.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Authentication params. + /// + /// - Remark: Generated from `#/paths/api/auth/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The email to authenticate with. + /// + /// - Remark: Generated from `#/paths/api/auth/POST/json/email`. + public var email: Swift.String + /// The password to authenticate with. + /// + /// - Remark: Generated from `#/paths/api/auth/POST/json/password`. + public var password: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - email: The email to authenticate with. + /// - password: The password to authenticate with. + public init(email: Swift.String, password: Swift.String) { + self.email = email + self.password = password + } + public enum CodingKeys: String, CodingKey { + case email + case password + } + } + case json(Operations.authenticate.Input.Body.jsonPayload) + } + public var body: Operations.authenticate.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.authenticate.Input.Path = .init(), + query: Operations.authenticate.Input.Query = .init(), + headers: Operations.authenticate.Input.Headers = .init(), + cookies: Operations.authenticate.Input.Cookies = .init(), + body: Operations.authenticate.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.authenticate.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.AuthenticationTokens) + } + /// Received HTTP response body + public var body: Operations.authenticate.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.authenticate.Output.Ok.Headers = .init(), + body: Operations.authenticate.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// Successfully authenticated and returned new API tokens. + /// + /// - Remark: Generated from `#/paths//api/auth/post(authenticate)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.authenticate.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.authenticate.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.authenticate.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.authenticate.Output.Unauthorized.Headers = .init(), + body: Operations.authenticate.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// Invalid email or password. + /// + /// - Remark: Generated from `#/paths//api/auth/post(authenticate)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.authenticate.Output.Unauthorized) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Get a specific device code. + /// + /// This endpoint returns a token for a given device code if the device code is authenticated. + /// + /// - Remark: HTTP `GET /api/auth/device_code/{device_code}`. + /// - Remark: Generated from `#/paths//api/auth/device_code/{device_code}/get(getDeviceCode)`. + public enum getDeviceCode { + public static let id: String = "getDeviceCode" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var device_code: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - device_code: + public init(device_code: Swift.String) { self.device_code = device_code } + } + public var path: Operations.getDeviceCode.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.getDeviceCode.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.getDeviceCode.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.getDeviceCode.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.getDeviceCode.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.getDeviceCode.Input.Path, + query: Operations.getDeviceCode.Input.Query = .init(), + headers: Operations.getDeviceCode.Input.Headers = .init(), + cookies: Operations.getDeviceCode.Input.Cookies = .init(), + body: Operations.getDeviceCode.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.getDeviceCode.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Token to authenticate the user with. + /// + /// - Remark: Generated from `#/paths/api/auth/device_code/{device_code}/GET/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// A short-lived token to authenticate API requests as user. + /// + /// - Remark: Generated from `#/paths/api/auth/device_code/{device_code}/GET/json/access_token`. + public var access_token: Swift.String? + /// A token to generate new access tokens when they expire. + /// + /// - Remark: Generated from `#/paths/api/auth/device_code/{device_code}/GET/json/refresh_token`. + public var refresh_token: Swift.String? + /// User authentication token + /// + /// - Remark: Generated from `#/paths/api/auth/device_code/{device_code}/GET/json/token`. + @available(*, deprecated) public var token: Swift.String? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - access_token: A short-lived token to authenticate API requests as user. + /// - refresh_token: A token to generate new access tokens when they expire. + /// - token: User authentication token + public init( + access_token: Swift.String? = nil, + refresh_token: Swift.String? = nil, + token: Swift.String? = nil + ) { + self.access_token = access_token + self.refresh_token = refresh_token + self.token = token + } + public enum CodingKeys: String, CodingKey { + case access_token + case refresh_token + case token + } + } + case json(Operations.getDeviceCode.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.getDeviceCode.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.getDeviceCode.Output.Ok.Headers = .init(), + body: Operations.getDeviceCode.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The device code is authenticated + /// + /// - Remark: Generated from `#/paths//api/auth/device_code/{device_code}/get(getDeviceCode)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.getDeviceCode.Output.Ok) + public struct Accepted: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.getDeviceCode.Output.Accepted.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(OpenAPIRuntime.OpenAPIObjectContainer) + } + /// Received HTTP response body + public var body: Operations.getDeviceCode.Output.Accepted.Body + /// Creates a new `Accepted`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.getDeviceCode.Output.Accepted.Headers = .init(), + body: Operations.getDeviceCode.Output.Accepted.Body + ) { + self.headers = headers + self.body = body + } + } + /// The device code is not authenticated + /// + /// - Remark: Generated from `#/paths//api/auth/device_code/{device_code}/get(getDeviceCode)/responses/202`. + /// + /// HTTP response code: `202 accepted`. + case accepted(Operations.getDeviceCode.Output.Accepted) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.getDeviceCode.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.getDeviceCode.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.getDeviceCode.Output.BadRequest.Headers = .init(), + body: Operations.getDeviceCode.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The request was not accepted, e.g., when the device code is expired + /// + /// - Remark: Generated from `#/paths//api/auth/device_code/{device_code}/get(getDeviceCode)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.getDeviceCode.Output.BadRequest) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Request new tokens. + /// + /// This endpoint returns new tokens for a given refresh token if the refresh token is valid. + /// + /// - Remark: HTTP `POST /api/auth/refresh_token`. + /// - Remark: Generated from `#/paths//api/auth/refresh_token/post(refreshToken)`. + public enum refreshToken { + public static let id: String = "refreshToken" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.refreshToken.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.refreshToken.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.refreshToken.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.refreshToken.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Token params + /// + /// - Remark: Generated from `#/paths/api/auth/refresh_token/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// User refresh token + /// + /// - Remark: Generated from `#/paths/api/auth/refresh_token/POST/json/refresh_token`. + public var refresh_token: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - refresh_token: User refresh token + public init(refresh_token: Swift.String) { self.refresh_token = refresh_token } + public enum CodingKeys: String, CodingKey { case refresh_token } + } + case json(Operations.refreshToken.Input.Body.jsonPayload) + } + public var body: Operations.refreshToken.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.refreshToken.Input.Path = .init(), + query: Operations.refreshToken.Input.Query = .init(), + headers: Operations.refreshToken.Input.Headers = .init(), + cookies: Operations.refreshToken.Input.Cookies = .init(), + body: Operations.refreshToken.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.refreshToken.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.AuthenticationTokens) + } + /// Received HTTP response body + public var body: Operations.refreshToken.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.refreshToken.Output.Ok.Headers = .init(), + body: Operations.refreshToken.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// Succcessfully generated new API tokens. + /// + /// - Remark: Generated from `#/paths//api/auth/refresh_token/post(refreshToken)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.refreshToken.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.refreshToken.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.refreshToken.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.refreshToken.Output.Unauthorized.Headers = .init(), + body: Operations.refreshToken.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to issue new tokens + /// + /// - Remark: Generated from `#/paths//api/auth/refresh_token/post(refreshToken)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.refreshToken.Output.Unauthorized) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Downloads an artifact from the cache. + /// + /// This endpoint returns a signed URL that can be used to download an artifact from the cache. + /// + /// - Remark: HTTP `GET /api/cache`. + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)`. + public enum downloadCacheArtifact { + public static let id: String = "downloadCacheArtifact" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.downloadCacheArtifact.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var cache_category: Components.Schemas.CacheCategory? + public var project_id: Swift.String + public var hash: Swift.String + public var name: Swift.String + /// Creates a new `Query`. + /// + /// - Parameters: + /// - cache_category: + /// - project_id: + /// - hash: + /// - name: + public init( + cache_category: Components.Schemas.CacheCategory? = nil, + project_id: Swift.String, + hash: Swift.String, + name: Swift.String + ) { + self.cache_category = cache_category + self.project_id = project_id + self.hash = hash + self.name = name + } + } + public var query: Operations.downloadCacheArtifact.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.downloadCacheArtifact.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.downloadCacheArtifact.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.downloadCacheArtifact.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.downloadCacheArtifact.Input.Path = .init(), + query: Operations.downloadCacheArtifact.Input.Query, + headers: Operations.downloadCacheArtifact.Input.Headers = .init(), + cookies: Operations.downloadCacheArtifact.Input.Cookies = .init(), + body: Operations.downloadCacheArtifact.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadCacheArtifact.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.CacheArtifactDownloadURL) + } + /// Received HTTP response body + public var body: Operations.downloadCacheArtifact.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadCacheArtifact.Output.Ok.Headers = .init(), + body: Operations.downloadCacheArtifact.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The artifact exists and is downloadable + /// + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.downloadCacheArtifact.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadCacheArtifact.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadCacheArtifact.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadCacheArtifact.Output.Unauthorized.Headers = .init(), + body: Operations.downloadCacheArtifact.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.downloadCacheArtifact.Output.Unauthorized) + public struct PaymentRequired: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadCacheArtifact.Output.PaymentRequired.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadCacheArtifact.Output.PaymentRequired.Body + /// Creates a new `PaymentRequired`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadCacheArtifact.Output.PaymentRequired.Headers = + .init(), + body: Operations.downloadCacheArtifact.Output.PaymentRequired.Body + ) { + self.headers = headers + self.body = body + } + } + /// The account has an invalid plan + /// + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)/responses/402`. + /// + /// HTTP response code: `402 paymentRequired`. + case paymentRequired(Operations.downloadCacheArtifact.Output.PaymentRequired) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadCacheArtifact.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadCacheArtifact.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadCacheArtifact.Output.Forbidden.Headers = .init(), + body: Operations.downloadCacheArtifact.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.downloadCacheArtifact.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadCacheArtifact.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadCacheArtifact.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadCacheArtifact.Output.NotFound.Headers = .init(), + body: Operations.downloadCacheArtifact.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project or the cache artifact doesn't exist + /// + /// - Remark: Generated from `#/paths//api/cache/get(downloadCacheArtifact)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.downloadCacheArtifact.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It checks if an artifact exists in the cache. + /// + /// This endpoint checks if an artifact exists in the cache. It returns a 404 status code if the artifact does not exist. + /// + /// - Remark: HTTP `GET /api/cache/exists`. + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)`. + public enum cacheArtifactExists { + public static let id: String = "cacheArtifactExists" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.cacheArtifactExists.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var cache_category: Components.Schemas.CacheCategory? + public var project_id: Swift.String + public var hash: Swift.String + public var name: Swift.String + /// Creates a new `Query`. + /// + /// - Parameters: + /// - cache_category: + /// - project_id: + /// - hash: + /// - name: + public init( + cache_category: Components.Schemas.CacheCategory? = nil, + project_id: Swift.String, + hash: Swift.String, + name: Swift.String + ) { + self.cache_category = cache_category + self.project_id = project_id + self.hash = hash + self.name = name + } + } + public var query: Operations.cacheArtifactExists.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.cacheArtifactExists.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.cacheArtifactExists.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.cacheArtifactExists.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.cacheArtifactExists.Input.Path = .init(), + query: Operations.cacheArtifactExists.Input.Query, + headers: Operations.cacheArtifactExists.Input.Headers = .init(), + cookies: Operations.cacheArtifactExists.Input.Cookies = .init(), + body: Operations.cacheArtifactExists.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cacheArtifactExists.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// The artifact exists in the cache and can be downloaded + /// + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/data`. + public var data: OpenAPIRuntime.OpenAPIObjectContainer? + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/status`. + public var status: + Operations.cacheArtifactExists.Output.Ok.Body.jsonPayload.statusPayload? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - data: + /// - status: + public init( + data: OpenAPIRuntime.OpenAPIObjectContainer? = nil, + status: Operations.cacheArtifactExists.Output.Ok.Body.jsonPayload + .statusPayload? = nil + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + case json(Operations.cacheArtifactExists.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.cacheArtifactExists.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cacheArtifactExists.Output.Ok.Headers = .init(), + body: Operations.cacheArtifactExists.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The artifact exists + /// + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.cacheArtifactExists.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cacheArtifactExists.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cacheArtifactExists.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cacheArtifactExists.Output.Unauthorized.Headers = .init(), + body: Operations.cacheArtifactExists.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.cacheArtifactExists.Output.Unauthorized) + public struct PaymentRequired: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cacheArtifactExists.Output.PaymentRequired.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cacheArtifactExists.Output.PaymentRequired.Body + /// Creates a new `PaymentRequired`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cacheArtifactExists.Output.PaymentRequired.Headers = + .init(), + body: Operations.cacheArtifactExists.Output.PaymentRequired.Body + ) { + self.headers = headers + self.body = body + } + } + /// The account has an invalid plan + /// + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)/responses/402`. + /// + /// HTTP response code: `402 paymentRequired`. + case paymentRequired(Operations.cacheArtifactExists.Output.PaymentRequired) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cacheArtifactExists.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cacheArtifactExists.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cacheArtifactExists.Output.Forbidden.Headers = .init(), + body: Operations.cacheArtifactExists.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.cacheArtifactExists.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cacheArtifactExists.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/errorPayload`. + public struct errorPayloadPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/errorPayload/code`. + public var code: Swift.String? + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/errorPayload/message`. + public var message: Swift.String? + /// Creates a new `errorPayloadPayload`. + /// + /// - Parameters: + /// - code: + /// - message: + public init(code: Swift.String? = nil, message: Swift.String? = nil) { + self.code = code + self.message = message + } + public enum CodingKeys: String, CodingKey { + case code + case message + } + } + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/error`. + public typealias errorPayload = [Operations.cacheArtifactExists.Output + .NotFound.Body.jsonPayload.errorPayloadPayload] + /// - Remark: Generated from `#/paths/api/cache/exists/GET/json/error`. + public var error: + Operations.cacheArtifactExists.Output.NotFound.Body.jsonPayload + .errorPayload? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - error: + public init( + error: Operations.cacheArtifactExists.Output.NotFound.Body.jsonPayload + .errorPayload? = nil + ) { self.error = error } + public enum CodingKeys: String, CodingKey { case error } + } + case json(Operations.cacheArtifactExists.Output.NotFound.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.cacheArtifactExists.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cacheArtifactExists.Output.NotFound.Headers = .init(), + body: Operations.cacheArtifactExists.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The artifact doesn't exist + /// + /// - Remark: Generated from `#/paths//api/cache/exists/get(cacheArtifactExists)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.cacheArtifactExists.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. The cache will then be able to serve the artifact. + /// + /// - Remark: HTTP `POST /api/cache/multipart/complete`. + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)`. + public enum completeCacheArtifactMultipartUpload { + public static let id: String = "completeCacheArtifactMultipartUpload" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.completeCacheArtifactMultipartUpload.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var cache_category: Components.Schemas.CacheCategory? + public var project_id: Swift.String + public var hash: Swift.String + public var upload_id: Swift.String + public var name: Swift.String + /// Creates a new `Query`. + /// + /// - Parameters: + /// - cache_category: + /// - project_id: + /// - hash: + /// - upload_id: + /// - name: + public init( + cache_category: Components.Schemas.CacheCategory? = nil, + project_id: Swift.String, + hash: Swift.String, + upload_id: Swift.String, + name: Swift.String + ) { + self.cache_category = cache_category + self.project_id = project_id + self.hash = hash + self.upload_id = upload_id + self.name = name + } + } + public var query: Operations.completeCacheArtifactMultipartUpload.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.completeCacheArtifactMultipartUpload.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.completeCacheArtifactMultipartUpload.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Multi-part upload parts + /// + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/partsPayload`. + public struct partsPayloadPayload: Codable, Equatable, Hashable, Sendable { + /// The ETag of the part + /// + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/partsPayload/etag`. + public var etag: Swift.String? + /// The part number + /// + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/partsPayload/part_number`. + public var part_number: Swift.Int? + /// Creates a new `partsPayloadPayload`. + /// + /// - Parameters: + /// - etag: The ETag of the part + /// - part_number: The part number + public init(etag: Swift.String? = nil, part_number: Swift.Int? = nil) { + self.etag = etag + self.part_number = part_number + } + public enum CodingKeys: String, CodingKey { + case etag + case part_number + } + } + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/parts`. + public typealias partsPayload = [Operations.completeCacheArtifactMultipartUpload + .Input.Body.jsonPayload.partsPayloadPayload] + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/parts`. + public var parts: + Operations.completeCacheArtifactMultipartUpload.Input.Body.jsonPayload + .partsPayload? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - parts: + public init( + parts: Operations.completeCacheArtifactMultipartUpload.Input.Body + .jsonPayload.partsPayload? = nil + ) { self.parts = parts } + public enum CodingKeys: String, CodingKey { case parts } + } + case json(Operations.completeCacheArtifactMultipartUpload.Input.Body.jsonPayload) + } + public var body: Operations.completeCacheArtifactMultipartUpload.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.completeCacheArtifactMultipartUpload.Input.Path = .init(), + query: Operations.completeCacheArtifactMultipartUpload.Input.Query, + headers: Operations.completeCacheArtifactMultipartUpload.Input.Headers = .init(), + cookies: Operations.completeCacheArtifactMultipartUpload.Input.Cookies = .init(), + body: Operations.completeCacheArtifactMultipartUpload.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeCacheArtifactMultipartUpload.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// This response confirms that the upload has been completed successfully. The cache will now be able to serve the artifact. + /// + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/data`. + public var data: OpenAPIRuntime.OpenAPIObjectContainer? + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/paths/api/cache/multipart/complete/POST/json/status`. + public var status: + Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body + .jsonPayload.statusPayload? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - data: + /// - status: + public init( + data: OpenAPIRuntime.OpenAPIObjectContainer? = nil, + status: Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body + .jsonPayload.statusPayload? = nil + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + case json( + Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body.jsonPayload + ) + } + /// Received HTTP response body + public var body: Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeCacheArtifactMultipartUpload.Output.Ok.Headers = + .init(), + body: Operations.completeCacheArtifactMultipartUpload.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The upload has been completed + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.completeCacheArtifactMultipartUpload.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized + .Headers = .init(), + body: Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.completeCacheArtifactMultipartUpload.Output.Unauthorized) + public struct PaymentRequired: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired.Body + /// Creates a new `PaymentRequired`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired + .Headers = .init(), + body: Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired + .Body + ) { + self.headers = headers + self.body = body + } + } + /// The account has an invalid plan + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)/responses/402`. + /// + /// HTTP response code: `402 paymentRequired`. + case paymentRequired( + Operations.completeCacheArtifactMultipartUpload.Output.PaymentRequired + ) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeCacheArtifactMultipartUpload.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeCacheArtifactMultipartUpload.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeCacheArtifactMultipartUpload.Output.Forbidden + .Headers = .init(), + body: Operations.completeCacheArtifactMultipartUpload.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.completeCacheArtifactMultipartUpload.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeCacheArtifactMultipartUpload.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeCacheArtifactMultipartUpload.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeCacheArtifactMultipartUpload.Output.NotFound + .Headers = .init(), + body: Operations.completeCacheArtifactMultipartUpload.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/complete/post(completeCacheArtifactMultipartUpload)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.completeCacheArtifactMultipartUpload.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/cache/multipart/generate-url`. + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)`. + public enum generateCacheArtifactMultipartUploadURL { + public static let id: String = "generateCacheArtifactMultipartUploadURL" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.generateCacheArtifactMultipartUploadURL.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var cache_category: Components.Schemas.CacheCategory? + public var project_id: Swift.String + public var hash: Swift.String + public var part_number: Swift.Int + public var upload_id: Swift.String + public var name: Swift.String + /// Creates a new `Query`. + /// + /// - Parameters: + /// - cache_category: + /// - project_id: + /// - hash: + /// - part_number: + /// - upload_id: + /// - name: + public init( + cache_category: Components.Schemas.CacheCategory? = nil, + project_id: Swift.String, + hash: Swift.String, + part_number: Swift.Int, + upload_id: Swift.String, + name: Swift.String + ) { + self.cache_category = cache_category + self.project_id = project_id + self.hash = hash + self.part_number = part_number + self.upload_id = upload_id + self.name = name + } + } + public var query: Operations.generateCacheArtifactMultipartUploadURL.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.generateCacheArtifactMultipartUploadURL.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.generateCacheArtifactMultipartUploadURL.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.generateCacheArtifactMultipartUploadURL.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.generateCacheArtifactMultipartUploadURL.Input.Path = .init(), + query: Operations.generateCacheArtifactMultipartUploadURL.Input.Query, + headers: Operations.generateCacheArtifactMultipartUploadURL.Input.Headers = .init(), + cookies: Operations.generateCacheArtifactMultipartUploadURL.Input.Cookies = .init(), + body: Operations.generateCacheArtifactMultipartUploadURL.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.ArtifactMultipartUploadURL) + } + /// Received HTTP response body + public var body: Operations.generateCacheArtifactMultipartUploadURL.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateCacheArtifactMultipartUploadURL.Output.Ok.Headers = + .init(), + body: Operations.generateCacheArtifactMultipartUploadURL.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The URL has been generated + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.generateCacheArtifactMultipartUploadURL.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized + .Headers = .init(), + body: Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized + .Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized( + Operations.generateCacheArtifactMultipartUploadURL.Output.Unauthorized + ) + public struct PaymentRequired: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.PaymentRequired + .Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateCacheArtifactMultipartUploadURL.Output.PaymentRequired.Body + /// Creates a new `PaymentRequired`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateCacheArtifactMultipartUploadURL.Output + .PaymentRequired.Headers = .init(), + body: Operations.generateCacheArtifactMultipartUploadURL.Output.PaymentRequired + .Body + ) { + self.headers = headers + self.body = body + } + } + /// The account has an invalid plan + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)/responses/402`. + /// + /// HTTP response code: `402 paymentRequired`. + case paymentRequired( + Operations.generateCacheArtifactMultipartUploadURL.Output.PaymentRequired + ) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden + .Headers = .init(), + body: Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.generateCacheArtifactMultipartUploadURL.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound + .Headers = .init(), + body: Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/generate-url/post(generateCacheArtifactMultipartUploadURL)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.generateCacheArtifactMultipartUploadURL.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It initiates a multipart upload in the cache. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/cache/multipart/start`. + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)`. + public enum startCacheArtifactMultipartUpload { + public static let id: String = "startCacheArtifactMultipartUpload" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.startCacheArtifactMultipartUpload.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var cache_category: Components.Schemas.CacheCategory? + public var project_id: Swift.String + public var hash: Swift.String + public var name: Swift.String + /// Creates a new `Query`. + /// + /// - Parameters: + /// - cache_category: + /// - project_id: + /// - hash: + /// - name: + public init( + cache_category: Components.Schemas.CacheCategory? = nil, + project_id: Swift.String, + hash: Swift.String, + name: Swift.String + ) { + self.cache_category = cache_category + self.project_id = project_id + self.hash = hash + self.name = name + } + } + public var query: Operations.startCacheArtifactMultipartUpload.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.startCacheArtifactMultipartUpload.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.startCacheArtifactMultipartUpload.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.startCacheArtifactMultipartUpload.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.startCacheArtifactMultipartUpload.Input.Path = .init(), + query: Operations.startCacheArtifactMultipartUpload.Input.Query, + headers: Operations.startCacheArtifactMultipartUpload.Input.Headers = .init(), + cookies: Operations.startCacheArtifactMultipartUpload.Input.Cookies = .init(), + body: Operations.startCacheArtifactMultipartUpload.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.startCacheArtifactMultipartUpload.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.ArtifactUploadID) + } + /// Received HTTP response body + public var body: Operations.startCacheArtifactMultipartUpload.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startCacheArtifactMultipartUpload.Output.Ok.Headers = + .init(), + body: Operations.startCacheArtifactMultipartUpload.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The upload has been started + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.startCacheArtifactMultipartUpload.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startCacheArtifactMultipartUpload.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.startCacheArtifactMultipartUpload.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startCacheArtifactMultipartUpload.Output.Unauthorized + .Headers = .init(), + body: Operations.startCacheArtifactMultipartUpload.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.startCacheArtifactMultipartUpload.Output.Unauthorized) + public struct PaymentRequired: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired.Body + /// Creates a new `PaymentRequired`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired + .Headers = .init(), + body: Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired.Body + ) { + self.headers = headers + self.body = body + } + } + /// The account has an invalid plan + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)/responses/402`. + /// + /// HTTP response code: `402 paymentRequired`. + case paymentRequired( + Operations.startCacheArtifactMultipartUpload.Output.PaymentRequired + ) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startCacheArtifactMultipartUpload.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.startCacheArtifactMultipartUpload.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startCacheArtifactMultipartUpload.Output.Forbidden.Headers = + .init(), + body: Operations.startCacheArtifactMultipartUpload.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.startCacheArtifactMultipartUpload.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startCacheArtifactMultipartUpload.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.startCacheArtifactMultipartUpload.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startCacheArtifactMultipartUpload.Output.NotFound.Headers = + .init(), + body: Operations.startCacheArtifactMultipartUpload.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/cache/multipart/start/post(startCacheArtifactMultipartUpload)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.startCacheArtifactMultipartUpload.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Lists the organizations + /// + /// Returns all the organizations the authenticated subject is part of. + /// + /// - Remark: HTTP `GET /api/organizations`. + /// - Remark: Generated from `#/paths//api/organizations/get(listOrganizations)`. + public enum listOrganizations { + public static let id: String = "listOrganizations" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.listOrganizations.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.listOrganizations.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.listOrganizations.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.listOrganizations.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.listOrganizations.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.listOrganizations.Input.Path = .init(), + query: Operations.listOrganizations.Input.Query = .init(), + headers: Operations.listOrganizations.Input.Headers = .init(), + cookies: Operations.listOrganizations.Input.Cookies = .init(), + body: Operations.listOrganizations.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listOrganizations.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// The list of organizations the authenticated subject is part of. + /// + /// - Remark: Generated from `#/paths/api/organizations/GET/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/organizations/GET/json/organizations`. + public var organizations: [Components.Schemas.Organization] + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - organizations: + public init(organizations: [Components.Schemas.Organization]) { + self.organizations = organizations + } + public enum CodingKeys: String, CodingKey { case organizations } + } + case json(Operations.listOrganizations.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.listOrganizations.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listOrganizations.Output.Ok.Headers = .init(), + body: Operations.listOrganizations.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The list of organizations + /// + /// - Remark: Generated from `#/paths//api/organizations/get(listOrganizations)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.listOrganizations.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listOrganizations.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listOrganizations.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listOrganizations.Output.Unauthorized.Headers = .init(), + body: Operations.listOrganizations.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/get(listOrganizations)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.listOrganizations.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listOrganizations.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listOrganizations.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listOrganizations.Output.Forbidden.Headers = .init(), + body: Operations.listOrganizations.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/get(listOrganizations)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.listOrganizations.Output.Forbidden) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Creates an organization + /// + /// Creates an organization with the given name. + /// + /// - Remark: HTTP `POST /api/organizations`. + /// - Remark: Generated from `#/paths//api/organizations/post(createOrganization)`. + public enum createOrganization { + public static let id: String = "createOrganization" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.createOrganization.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.createOrganization.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.createOrganization.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.createOrganization.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Organization params + /// + /// - Remark: Generated from `#/paths/api/organizations/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The name of the organization that should be created. + /// + /// - Remark: Generated from `#/paths/api/organizations/POST/json/name`. + public var name: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - name: The name of the organization that should be created. + public init(name: Swift.String) { self.name = name } + public enum CodingKeys: String, CodingKey { case name } + } + case json(Operations.createOrganization.Input.Body.jsonPayload) + } + public var body: Operations.createOrganization.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.createOrganization.Input.Path = .init(), + query: Operations.createOrganization.Input.Query = .init(), + headers: Operations.createOrganization.Input.Headers = .init(), + cookies: Operations.createOrganization.Input.Cookies = .init(), + body: Operations.createOrganization.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createOrganization.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Organization) + } + /// Received HTTP response body + public var body: Operations.createOrganization.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createOrganization.Output.Ok.Headers = .init(), + body: Operations.createOrganization.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization was created + /// + /// - Remark: Generated from `#/paths//api/organizations/post(createOrganization)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.createOrganization.Output.Ok) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createOrganization.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createOrganization.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createOrganization.Output.BadRequest.Headers = .init(), + body: Operations.createOrganization.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization could not be created due to a validation error + /// + /// - Remark: Generated from `#/paths//api/organizations/post(createOrganization)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.createOrganization.Output.BadRequest) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Shows an organization + /// + /// Returns the organization with the given identifier. + /// + /// - Remark: HTTP `GET /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)`. + public enum showOrganization { + public static let id: String = "showOrganization" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.showOrganization.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.showOrganization.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.showOrganization.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.showOrganization.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.showOrganization.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.showOrganization.Input.Path, + query: Operations.showOrganization.Input.Query = .init(), + headers: Operations.showOrganization.Input.Headers = .init(), + cookies: Operations.showOrganization.Input.Cookies = .init(), + body: Operations.showOrganization.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganization.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Organization) + } + /// Received HTTP response body + public var body: Operations.showOrganization.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganization.Output.Ok.Headers = .init(), + body: Operations.showOrganization.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.showOrganization.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganization.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showOrganization.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganization.Output.Unauthorized.Headers = .init(), + body: Operations.showOrganization.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.showOrganization.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganization.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showOrganization.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganization.Output.Forbidden.Headers = .init(), + body: Operations.showOrganization.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.showOrganization.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganization.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showOrganization.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganization.Output.NotFound.Headers = .init(), + body: Operations.showOrganization.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/get(showOrganization)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.showOrganization.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Updates an organization + /// + /// Updates an organization with given parameters. + /// + /// - Remark: HTTP `PATCH /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))`. + public enum updateOrganization__2_ { + public static let id: String = "updateOrganization (2)" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.updateOrganization__2_.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.updateOrganization__2_.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.updateOrganization__2_.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.updateOrganization__2_.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Organization update params + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PATCH/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The SSO organization ID to be associated with the SSO provider + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PATCH/json/sso_organization_id`. + public var sso_organization_id: Swift.String? + /// The SSO provider to set up for the organization + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PATCH/json/sso_provider`. + @frozen + public enum sso_providerPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case google + case none + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "google": self = .google + case "none": self = .none + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .google: return "google" + case .none: return "none" + } + } + public static var allCases: [sso_providerPayload] { [.google, .none] } + } + /// The SSO provider to set up for the organization + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PATCH/json/sso_provider`. + public var sso_provider: + Operations.updateOrganization__2_.Input.Body.jsonPayload + .sso_providerPayload? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - sso_organization_id: The SSO organization ID to be associated with the SSO provider + /// - sso_provider: The SSO provider to set up for the organization + public init( + sso_organization_id: Swift.String? = nil, + sso_provider: Operations.updateOrganization__2_.Input.Body.jsonPayload + .sso_providerPayload? = nil + ) { + self.sso_organization_id = sso_organization_id + self.sso_provider = sso_provider + } + public enum CodingKeys: String, CodingKey { + case sso_organization_id + case sso_provider + } + } + case json(Operations.updateOrganization__2_.Input.Body.jsonPayload) + } + public var body: Operations.updateOrganization__2_.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.updateOrganization__2_.Input.Path, + query: Operations.updateOrganization__2_.Input.Query = .init(), + headers: Operations.updateOrganization__2_.Input.Headers = .init(), + cookies: Operations.updateOrganization__2_.Input.Cookies = .init(), + body: Operations.updateOrganization__2_.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization__2_.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Organization) + } + /// Received HTTP response body + public var body: Operations.updateOrganization__2_.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization__2_.Output.Ok.Headers = .init(), + body: Operations.updateOrganization__2_.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.updateOrganization__2_.Output.Ok) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization__2_.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization__2_.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization__2_.Output.BadRequest.Headers = .init(), + body: Operations.updateOrganization__2_.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization could not be updated due to a validation error + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.updateOrganization__2_.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization__2_.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization__2_.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization__2_.Output.Unauthorized.Headers = + .init(), + body: Operations.updateOrganization__2_.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.updateOrganization__2_.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization__2_.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization__2_.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization__2_.Output.Forbidden.Headers = .init(), + body: Operations.updateOrganization__2_.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.updateOrganization__2_.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization__2_.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization__2_.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization__2_.Output.NotFound.Headers = .init(), + body: Operations.updateOrganization__2_.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/patch(updateOrganization (2))/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.updateOrganization__2_.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Updates an organization + /// + /// Updates an organization with given parameters. + /// + /// - Remark: HTTP `PUT /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)`. + public enum updateOrganization { + public static let id: String = "updateOrganization" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.updateOrganization.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.updateOrganization.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.updateOrganization.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.updateOrganization.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Organization update params + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PUT/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The SSO organization ID to be associated with the SSO provider + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PUT/json/sso_organization_id`. + public var sso_organization_id: Swift.String? + /// The SSO provider to set up for the organization + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PUT/json/sso_provider`. + @frozen + public enum sso_providerPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case google + case none + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "google": self = .google + case "none": self = .none + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .google: return "google" + case .none: return "none" + } + } + public static var allCases: [sso_providerPayload] { [.google, .none] } + } + /// The SSO provider to set up for the organization + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/PUT/json/sso_provider`. + public var sso_provider: + Operations.updateOrganization.Input.Body.jsonPayload.sso_providerPayload? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - sso_organization_id: The SSO organization ID to be associated with the SSO provider + /// - sso_provider: The SSO provider to set up for the organization + public init( + sso_organization_id: Swift.String? = nil, + sso_provider: Operations.updateOrganization.Input.Body.jsonPayload + .sso_providerPayload? = nil + ) { + self.sso_organization_id = sso_organization_id + self.sso_provider = sso_provider + } + public enum CodingKeys: String, CodingKey { + case sso_organization_id + case sso_provider + } + } + case json(Operations.updateOrganization.Input.Body.jsonPayload) + } + public var body: Operations.updateOrganization.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.updateOrganization.Input.Path, + query: Operations.updateOrganization.Input.Query = .init(), + headers: Operations.updateOrganization.Input.Headers = .init(), + cookies: Operations.updateOrganization.Input.Cookies = .init(), + body: Operations.updateOrganization.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Organization) + } + /// Received HTTP response body + public var body: Operations.updateOrganization.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization.Output.Ok.Headers = .init(), + body: Operations.updateOrganization.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.updateOrganization.Output.Ok) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization.Output.BadRequest.Headers = .init(), + body: Operations.updateOrganization.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization could not be updated due to a validation error + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.updateOrganization.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization.Output.Unauthorized.Headers = .init(), + body: Operations.updateOrganization.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.updateOrganization.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization.Output.Forbidden.Headers = .init(), + body: Operations.updateOrganization.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.updateOrganization.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganization.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganization.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganization.Output.NotFound.Headers = .init(), + body: Operations.updateOrganization.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/put(updateOrganization)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.updateOrganization.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Deletes an organization + /// + /// Deletes the organization with the given name. + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)`. + public enum deleteOrganization { + public static let id: String = "deleteOrganization" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.deleteOrganization.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.deleteOrganization.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.deleteOrganization.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.deleteOrganization.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.deleteOrganization.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.deleteOrganization.Input.Path, + query: Operations.deleteOrganization.Input.Query = .init(), + headers: Operations.deleteOrganization.Input.Headers = .init(), + cookies: Operations.deleteOrganization.Input.Cookies = .init(), + body: Operations.deleteOrganization.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteOrganization.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.deleteOrganization.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteOrganization.Output.NoContent.Headers = .init(), + body: Operations.deleteOrganization.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// The organization was deleted + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.deleteOrganization.Output.NoContent) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteOrganization.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.deleteOrganization.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteOrganization.Output.Unauthorized.Headers = .init(), + body: Operations.deleteOrganization.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.deleteOrganization.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteOrganization.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.deleteOrganization.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteOrganization.Output.Forbidden.Headers = .init(), + body: Operations.deleteOrganization.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.deleteOrganization.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteOrganization.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.deleteOrganization.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteOrganization.Output.NotFound.Headers = .init(), + body: Operations.deleteOrganization.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/delete(deleteOrganization)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.deleteOrganization.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Creates an invitation + /// + /// Invites a user with a given email to a given organization. + /// + /// - Remark: HTTP `POST /api/organizations/{organization_name}/invitations`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)`. + public enum createInvitation { + public static let id: String = "createInvitation" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.createInvitation.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.createInvitation.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.createInvitation.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.createInvitation.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Invitation params + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/invitations/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The email of the invitee. + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/invitations/POST/json/invitee_email`. + public var invitee_email: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - invitee_email: The email of the invitee. + public init(invitee_email: Swift.String) { self.invitee_email = invitee_email } + public enum CodingKeys: String, CodingKey { case invitee_email } + } + case json(Operations.createInvitation.Input.Body.jsonPayload) + } + public var body: Operations.createInvitation.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.createInvitation.Input.Path, + query: Operations.createInvitation.Input.Query = .init(), + headers: Operations.createInvitation.Input.Headers = .init(), + cookies: Operations.createInvitation.Input.Cookies = .init(), + body: Operations.createInvitation.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createInvitation.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Invitation) + } + /// Received HTTP response body + public var body: Operations.createInvitation.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createInvitation.Output.Ok.Headers = .init(), + body: Operations.createInvitation.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The user was invited + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.createInvitation.Output.Ok) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createInvitation.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createInvitation.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createInvitation.Output.BadRequest.Headers = .init(), + body: Operations.createInvitation.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The user could not be invited due to a validation error + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.createInvitation.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createInvitation.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createInvitation.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createInvitation.Output.Unauthorized.Headers = .init(), + body: Operations.createInvitation.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.createInvitation.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createInvitation.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createInvitation.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createInvitation.Output.Forbidden.Headers = .init(), + body: Operations.createInvitation.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.createInvitation.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createInvitation.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createInvitation.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createInvitation.Output.NotFound.Headers = .init(), + body: Operations.createInvitation.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/post(createInvitation)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.createInvitation.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Cancels an invitation + /// + /// Cancels an invitation for a given invitee email and an organization. + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}/invitations`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)`. + public enum cancelInvitation { + public static let id: String = "cancelInvitation" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.cancelInvitation.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.cancelInvitation.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.cancelInvitation.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.cancelInvitation.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Invitation params + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/invitations/DELETE/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The email of the invitee. + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/invitations/DELETE/json/invitee_email`. + public var invitee_email: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - invitee_email: The email of the invitee. + public init(invitee_email: Swift.String) { self.invitee_email = invitee_email } + public enum CodingKeys: String, CodingKey { case invitee_email } + } + case json(Operations.cancelInvitation.Input.Body.jsonPayload) + } + public var body: Operations.cancelInvitation.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.cancelInvitation.Input.Path, + query: Operations.cancelInvitation.Input.Query = .init(), + headers: Operations.cancelInvitation.Input.Headers = .init(), + cookies: Operations.cancelInvitation.Input.Cookies = .init(), + body: Operations.cancelInvitation.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cancelInvitation.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(OpenAPIRuntime.OpenAPIValueContainer) + } + /// Received HTTP response body + public var body: Operations.cancelInvitation.Output.NoContent.Body + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cancelInvitation.Output.NoContent.Headers = .init(), + body: Operations.cancelInvitation.Output.NoContent.Body + ) { + self.headers = headers + self.body = body + } + } + /// The invitation was cancelled + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.cancelInvitation.Output.NoContent) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cancelInvitation.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cancelInvitation.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cancelInvitation.Output.Unauthorized.Headers = .init(), + body: Operations.cancelInvitation.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.cancelInvitation.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cancelInvitation.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cancelInvitation.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cancelInvitation.Output.Forbidden.Headers = .init(), + body: Operations.cancelInvitation.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.cancelInvitation.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cancelInvitation.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cancelInvitation.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cancelInvitation.Output.NotFound.Headers = .init(), + body: Operations.cancelInvitation.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The invitation with the given invitee email and organization name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/invitations/delete(cancelInvitation)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.cancelInvitation.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Updates a member in an organization + /// + /// Updates a member in a given organization + /// + /// - Remark: HTTP `PUT /api/organizations/{organization_name}/members/{user_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)`. + public enum updateOrganizationMember { + public static let id: String = "updateOrganizationMember" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + public var user_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + /// - user_name: + public init(organization_name: Swift.String, user_name: Swift.String) { + self.organization_name = organization_name + self.user_name = user_name + } + } + public var path: Operations.updateOrganizationMember.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.updateOrganizationMember.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.updateOrganizationMember.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.updateOrganizationMember.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Member update params + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/members/{user_name}/PUT/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The role to update the member to + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/members/{user_name}/PUT/json/role`. + @frozen + public enum rolePayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case admin + case user + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "admin": self = .admin + case "user": self = .user + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .admin: return "admin" + case .user: return "user" + } + } + public static var allCases: [rolePayload] { [.admin, .user] } + } + /// The role to update the member to + /// + /// - Remark: Generated from `#/paths/api/organizations/{organization_name}/members/{user_name}/PUT/json/role`. + public var role: + Operations.updateOrganizationMember.Input.Body.jsonPayload.rolePayload + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - role: The role to update the member to + public init( + role: Operations.updateOrganizationMember.Input.Body.jsonPayload.rolePayload + ) { self.role = role } + public enum CodingKeys: String, CodingKey { case role } + } + case json(Operations.updateOrganizationMember.Input.Body.jsonPayload) + } + public var body: Operations.updateOrganizationMember.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.updateOrganizationMember.Input.Path, + query: Operations.updateOrganizationMember.Input.Query = .init(), + headers: Operations.updateOrganizationMember.Input.Headers = .init(), + cookies: Operations.updateOrganizationMember.Input.Cookies = .init(), + body: Operations.updateOrganizationMember.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganizationMember.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.OrganizationMember) + } + /// Received HTTP response body + public var body: Operations.updateOrganizationMember.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganizationMember.Output.Ok.Headers = .init(), + body: Operations.updateOrganizationMember.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The member was updated + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.updateOrganizationMember.Output.Ok) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganizationMember.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganizationMember.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganizationMember.Output.BadRequest.Headers = + .init(), + body: Operations.updateOrganizationMember.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The member could not be updated due to a validation error + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.updateOrganizationMember.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganizationMember.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganizationMember.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganizationMember.Output.Unauthorized.Headers = + .init(), + body: Operations.updateOrganizationMember.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.updateOrganizationMember.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganizationMember.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganizationMember.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganizationMember.Output.Forbidden.Headers = .init(), + body: Operations.updateOrganizationMember.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.updateOrganizationMember.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateOrganizationMember.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateOrganizationMember.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateOrganizationMember.Output.NotFound.Headers = .init(), + body: Operations.updateOrganizationMember.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization or the user with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/put(updateOrganizationMember)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.updateOrganizationMember.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Removes a member from an organization + /// + /// Removes a member with a given username from a given organization + /// + /// - Remark: HTTP `DELETE /api/organizations/{organization_name}/members/{user_name}`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)`. + public enum removeOrganizationMember { + public static let id: String = "removeOrganizationMember" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + public var user_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + /// - user_name: + public init(organization_name: Swift.String, user_name: Swift.String) { + self.organization_name = organization_name + self.user_name = user_name + } + } + public var path: Operations.removeOrganizationMember.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.removeOrganizationMember.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.removeOrganizationMember.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.removeOrganizationMember.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.removeOrganizationMember.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.removeOrganizationMember.Input.Path, + query: Operations.removeOrganizationMember.Input.Query = .init(), + headers: Operations.removeOrganizationMember.Input.Headers = .init(), + cookies: Operations.removeOrganizationMember.Input.Cookies = .init(), + body: Operations.removeOrganizationMember.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.removeOrganizationMember.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(OpenAPIRuntime.OpenAPIValueContainer) + } + /// Received HTTP response body + public var body: Operations.removeOrganizationMember.Output.NoContent.Body + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.removeOrganizationMember.Output.NoContent.Headers = .init(), + body: Operations.removeOrganizationMember.Output.NoContent.Body + ) { + self.headers = headers + self.body = body + } + } + /// The member was removed + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.removeOrganizationMember.Output.NoContent) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.removeOrganizationMember.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.removeOrganizationMember.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.removeOrganizationMember.Output.BadRequest.Headers = + .init(), + body: Operations.removeOrganizationMember.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The member could not be removed due to a validation error + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.removeOrganizationMember.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.removeOrganizationMember.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.removeOrganizationMember.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.removeOrganizationMember.Output.Unauthorized.Headers = + .init(), + body: Operations.removeOrganizationMember.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.removeOrganizationMember.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.removeOrganizationMember.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.removeOrganizationMember.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.removeOrganizationMember.Output.Forbidden.Headers = .init(), + body: Operations.removeOrganizationMember.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.removeOrganizationMember.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.removeOrganizationMember.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.removeOrganizationMember.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.removeOrganizationMember.Output.NotFound.Headers = .init(), + body: Operations.removeOrganizationMember.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization or the user with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/members/{user_name}/delete(removeOrganizationMember)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.removeOrganizationMember.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Shows the usage of an organization + /// + /// Returns the usage of the organization with the given identifier. (e.g. number of remote cache hits) + /// + /// - Remark: HTTP `GET /api/organizations/{organization_name}/usage`. + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)`. + public enum showOrganizationUsage { + public static let id: String = "showOrganizationUsage" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var organization_name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - organization_name: + public init(organization_name: Swift.String) { + self.organization_name = organization_name + } + } + public var path: Operations.showOrganizationUsage.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.showOrganizationUsage.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.showOrganizationUsage.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.showOrganizationUsage.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.showOrganizationUsage.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.showOrganizationUsage.Input.Path, + query: Operations.showOrganizationUsage.Input.Query = .init(), + headers: Operations.showOrganizationUsage.Input.Headers = .init(), + cookies: Operations.showOrganizationUsage.Input.Cookies = .init(), + body: Operations.showOrganizationUsage.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganizationUsage.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.OrganizationUsage) + } + /// Received HTTP response body + public var body: Operations.showOrganizationUsage.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganizationUsage.Output.Ok.Headers = .init(), + body: Operations.showOrganizationUsage.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization usage + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.showOrganizationUsage.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganizationUsage.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showOrganizationUsage.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganizationUsage.Output.Unauthorized.Headers = .init(), + body: Operations.showOrganizationUsage.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.showOrganizationUsage.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganizationUsage.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showOrganizationUsage.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganizationUsage.Output.Forbidden.Headers = .init(), + body: Operations.showOrganizationUsage.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.showOrganizationUsage.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showOrganizationUsage.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showOrganizationUsage.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showOrganizationUsage.Output.NotFound.Headers = .init(), + body: Operations.showOrganizationUsage.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The organization with the given name was not found + /// + /// - Remark: Generated from `#/paths//api/organizations/{organization_name}/usage/get(showOrganizationUsage)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.showOrganizationUsage.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// List projects the authenticated user has access to. + /// + /// - Remark: HTTP `GET /api/projects`. + /// - Remark: Generated from `#/paths//api/projects/get(listProjects)`. + public enum listProjects { + public static let id: String = "listProjects" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.listProjects.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.listProjects.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.listProjects.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.listProjects.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.listProjects.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.listProjects.Input.Path = .init(), + query: Operations.listProjects.Input.Query = .init(), + headers: Operations.listProjects.Input.Headers = .init(), + cookies: Operations.listProjects.Input.Cookies = .init(), + body: Operations.listProjects.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listProjects.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// - Remark: Generated from `#/paths/api/projects/GET/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/projects/GET/json/projects`. + public var projects: [Components.Schemas.Project] + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - projects: + public init(projects: [Components.Schemas.Project]) { + self.projects = projects + } + public enum CodingKeys: String, CodingKey { case projects } + } + case json(Operations.listProjects.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.listProjects.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listProjects.Output.Ok.Headers = .init(), + body: Operations.listProjects.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// List of projects + /// + /// - Remark: Generated from `#/paths//api/projects/get(listProjects)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.listProjects.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listProjects.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listProjects.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listProjects.Output.Unauthorized.Headers = .init(), + body: Operations.listProjects.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/get(listProjects)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.listProjects.Output.Unauthorized) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Create a new project. + /// + /// - Remark: HTTP `POST /api/projects`. + /// - Remark: Generated from `#/paths//api/projects/post(createProject)`. + public enum createProject { + public static let id: String = "createProject" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.createProject.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.createProject.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.createProject.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.createProject.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Projects params + /// + /// - Remark: Generated from `#/paths/api/projects/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The full handle of the project that should be created. + /// + /// - Remark: Generated from `#/paths/api/projects/POST/json/full_handle`. + public var full_handle: Swift.String? + /// The name of the project that should be created. + /// + /// - Remark: Generated from `#/paths/api/projects/POST/json/name`. + @available(*, deprecated) public var name: Swift.String? + /// Organization to create the project with. If not specified, the project will be created with the current user's personal account. + /// + /// - Remark: Generated from `#/paths/api/projects/POST/json/organization`. + @available(*, deprecated) public var organization: Swift.String? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - full_handle: The full handle of the project that should be created. + /// - name: The name of the project that should be created. + /// - organization: Organization to create the project with. If not specified, the project will be created with the current user's personal account. + public init( + full_handle: Swift.String? = nil, + name: Swift.String? = nil, + organization: Swift.String? = nil + ) { + self.full_handle = full_handle + self.name = name + self.organization = organization + } + public enum CodingKeys: String, CodingKey { + case full_handle + case name + case organization + } + } + case json(Operations.createProject.Input.Body.jsonPayload) + } + public var body: Operations.createProject.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.createProject.Input.Path = .init(), + query: Operations.createProject.Input.Query = .init(), + headers: Operations.createProject.Input.Headers = .init(), + cookies: Operations.createProject.Input.Cookies = .init(), + body: Operations.createProject.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProject.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Project) + } + /// Received HTTP response body + public var body: Operations.createProject.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProject.Output.Ok.Headers = .init(), + body: Operations.createProject.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project was created + /// + /// - Remark: Generated from `#/paths//api/projects/post(createProject)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.createProject.Output.Ok) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProject.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createProject.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProject.Output.BadRequest.Headers = .init(), + body: Operations.createProject.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The account was not found + /// + /// - Remark: Generated from `#/paths//api/projects/post(createProject)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.createProject.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProject.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createProject.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProject.Output.Unauthorized.Headers = .init(), + body: Operations.createProject.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/post(createProject)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.createProject.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProject.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createProject.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProject.Output.Forbidden.Headers = .init(), + body: Operations.createProject.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/post(createProject)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.createProject.Output.Forbidden) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Returns a project based on the handle. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)`. + public enum showProject { + public static let id: String = "showProject" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.showProject.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.showProject.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.showProject.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.showProject.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.showProject.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.showProject.Input.Path, + query: Operations.showProject.Input.Query = .init(), + headers: Operations.showProject.Input.Headers = .init(), + cookies: Operations.showProject.Input.Cookies = .init(), + body: Operations.showProject.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showProject.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Project) + } + /// Received HTTP response body + public var body: Operations.showProject.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showProject.Output.Ok.Headers = .init(), + body: Operations.showProject.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project to show + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.showProject.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showProject.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showProject.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showProject.Output.Unauthorized.Headers = .init(), + body: Operations.showProject.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.showProject.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showProject.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showProject.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showProject.Output.Forbidden.Headers = .init(), + body: Operations.showProject.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.showProject.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.showProject.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.showProject.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.showProject.Output.NotFound.Headers = .init(), + body: Operations.showProject.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/get(showProject)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.showProject.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Updates a project + /// + /// Updates an project with given parameters. + /// + /// - Remark: HTTP `PUT /api/projects/{account_handle}/{project_handle}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)`. + public enum updateProject { + public static let id: String = "updateProject" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.updateProject.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.updateProject.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.updateProject.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.updateProject.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Project update params + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/PUT/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The default branch for the project. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/PUT/json/default_branch`. + public var default_branch: Swift.String? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - default_branch: The default branch for the project. + public init(default_branch: Swift.String? = nil) { + self.default_branch = default_branch + } + public enum CodingKeys: String, CodingKey { case default_branch } + } + case json(Operations.updateProject.Input.Body.jsonPayload) + } + public var body: Operations.updateProject.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.updateProject.Input.Path, + query: Operations.updateProject.Input.Query = .init(), + headers: Operations.updateProject.Input.Headers = .init(), + cookies: Operations.updateProject.Input.Cookies = .init(), + body: Operations.updateProject.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateProject.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Project) + } + /// Received HTTP response body + public var body: Operations.updateProject.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateProject.Output.Ok.Headers = .init(), + body: Operations.updateProject.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The updated project + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.updateProject.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateProject.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateProject.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateProject.Output.Unauthorized.Headers = .init(), + body: Operations.updateProject.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.updateProject.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateProject.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateProject.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateProject.Output.Forbidden.Headers = .init(), + body: Operations.updateProject.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.updateProject.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updateProject.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.updateProject.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updateProject.Output.NotFound.Headers = .init(), + body: Operations.updateProject.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project with the given account and project handles was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/put(updateProject)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.updateProject.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Cleans cache for a given project + /// + /// - Remark: HTTP `PUT /api/projects/{account_handle}/{project_handle}/cache/clean`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)`. + public enum cleanCache { + public static let id: String = "cleanCache" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.cleanCache.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.cleanCache.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.cleanCache.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.cleanCache.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.cleanCache.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.cleanCache.Input.Path, + query: Operations.cleanCache.Input.Query = .init(), + headers: Operations.cleanCache.Input.Headers = .init(), + cookies: Operations.cleanCache.Input.Cookies = .init(), + body: Operations.cleanCache.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cleanCache.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.cleanCache.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cleanCache.Output.NoContent.Headers = .init(), + body: Operations.cleanCache.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// The cache has been successfully cleaned + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.cleanCache.Output.NoContent) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cleanCache.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cleanCache.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cleanCache.Output.Unauthorized.Headers = .init(), + body: Operations.cleanCache.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.cleanCache.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cleanCache.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cleanCache.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cleanCache.Output.Forbidden.Headers = .init(), + body: Operations.cleanCache.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.cleanCache.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.cleanCache.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.cleanCache.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.cleanCache.Output.NotFound.Headers = .init(), + body: Operations.cleanCache.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/cache/clean/put(cleanCache)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.cleanCache.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/complete`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)`. + public enum completePreviewsMultipartUpload { + public static let id: String = "completePreviewsMultipartUpload" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.completePreviewsMultipartUpload.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.completePreviewsMultipartUpload.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.completePreviewsMultipartUpload.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.completePreviewsMultipartUpload.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// preview multipart upload completion + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/complete/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/complete/POST/json/multipart_upload_parts`. + public var multipart_upload_parts: + Components.Schemas.ArtifactMultipartUploadParts + /// The id of the preview. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/complete/POST/json/preview_id`. + public var preview_id: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - multipart_upload_parts: + /// - preview_id: The id of the preview. + public init( + multipart_upload_parts: Components.Schemas.ArtifactMultipartUploadParts, + preview_id: Swift.String + ) { + self.multipart_upload_parts = multipart_upload_parts + self.preview_id = preview_id + } + public enum CodingKeys: String, CodingKey { + case multipart_upload_parts + case preview_id + } + } + case json(Operations.completePreviewsMultipartUpload.Input.Body.jsonPayload) + } + public var body: Operations.completePreviewsMultipartUpload.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.completePreviewsMultipartUpload.Input.Path, + query: Operations.completePreviewsMultipartUpload.Input.Query = .init(), + headers: Operations.completePreviewsMultipartUpload.Input.Headers = .init(), + cookies: Operations.completePreviewsMultipartUpload.Input.Cookies = .init(), + body: Operations.completePreviewsMultipartUpload.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.completePreviewsMultipartUpload.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// The preview multipart upload has been completed + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/complete/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The URL to download the preview + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/complete/POST/json/url`. + public var url: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - url: The URL to download the preview + public init(url: Swift.String) { self.url = url } + public enum CodingKeys: String, CodingKey { case url } + } + case json(Operations.completePreviewsMultipartUpload.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.completePreviewsMultipartUpload.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completePreviewsMultipartUpload.Output.Ok.Headers = .init(), + body: Operations.completePreviewsMultipartUpload.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The upload has been completed + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.completePreviewsMultipartUpload.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completePreviewsMultipartUpload.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.completePreviewsMultipartUpload.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completePreviewsMultipartUpload.Output.Unauthorized + .Headers = .init(), + body: Operations.completePreviewsMultipartUpload.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.completePreviewsMultipartUpload.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completePreviewsMultipartUpload.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.completePreviewsMultipartUpload.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completePreviewsMultipartUpload.Output.Forbidden.Headers = + .init(), + body: Operations.completePreviewsMultipartUpload.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.completePreviewsMultipartUpload.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completePreviewsMultipartUpload.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.completePreviewsMultipartUpload.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completePreviewsMultipartUpload.Output.NotFound.Headers = + .init(), + body: Operations.completePreviewsMultipartUpload.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/complete/post(completePreviewsMultipartUpload)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.completePreviewsMultipartUpload.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/generate-url`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)`. + public enum generatePreviewsMultipartUploadURL { + public static let id: String = "generatePreviewsMultipartUploadURL" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.generatePreviewsMultipartUploadURL.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.generatePreviewsMultipartUploadURL.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.generatePreviewsMultipartUploadURL.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.generatePreviewsMultipartUploadURL.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Artifact to generate a signed URL for + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/generate-url/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/generate-url/POST/json/multipart_upload_part`. + public var multipart_upload_part: Components.Schemas.ArtifactMultipartUploadPart + /// The id of the preview. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/generate-url/POST/json/preview_id`. + public var preview_id: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - multipart_upload_part: + /// - preview_id: The id of the preview. + public init( + multipart_upload_part: Components.Schemas.ArtifactMultipartUploadPart, + preview_id: Swift.String + ) { + self.multipart_upload_part = multipart_upload_part + self.preview_id = preview_id + } + public enum CodingKeys: String, CodingKey { + case multipart_upload_part + case preview_id + } + } + case json(Operations.generatePreviewsMultipartUploadURL.Input.Body.jsonPayload) + } + public var body: Operations.generatePreviewsMultipartUploadURL.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.generatePreviewsMultipartUploadURL.Input.Path, + query: Operations.generatePreviewsMultipartUploadURL.Input.Query = .init(), + headers: Operations.generatePreviewsMultipartUploadURL.Input.Headers = .init(), + cookies: Operations.generatePreviewsMultipartUploadURL.Input.Cookies = .init(), + body: Operations.generatePreviewsMultipartUploadURL.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.generatePreviewsMultipartUploadURL.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.ArtifactMultipartUploadURL) + } + /// Received HTTP response body + public var body: Operations.generatePreviewsMultipartUploadURL.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generatePreviewsMultipartUploadURL.Output.Ok.Headers = + .init(), + body: Operations.generatePreviewsMultipartUploadURL.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The URL has been generated + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.generatePreviewsMultipartUploadURL.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized + .Headers = .init(), + body: Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.generatePreviewsMultipartUploadURL.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generatePreviewsMultipartUploadURL.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.generatePreviewsMultipartUploadURL.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generatePreviewsMultipartUploadURL.Output.Forbidden + .Headers = .init(), + body: Operations.generatePreviewsMultipartUploadURL.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.generatePreviewsMultipartUploadURL.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generatePreviewsMultipartUploadURL.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.generatePreviewsMultipartUploadURL.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generatePreviewsMultipartUploadURL.Output.NotFound.Headers = + .init(), + body: Operations.generatePreviewsMultipartUploadURL.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/generate-url/post(generatePreviewsMultipartUploadURL)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.generatePreviewsMultipartUploadURL.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It initiates a multipart upload for a preview artifact. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/previews/start`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)`. + public enum startPreviewsMultipartUpload { + public static let id: String = "startPreviewsMultipartUpload" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.startPreviewsMultipartUpload.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.startPreviewsMultipartUpload.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.startPreviewsMultipartUpload.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.startPreviewsMultipartUpload.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.startPreviewsMultipartUpload.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.startPreviewsMultipartUpload.Input.Path, + query: Operations.startPreviewsMultipartUpload.Input.Query = .init(), + headers: Operations.startPreviewsMultipartUpload.Input.Headers = .init(), + cookies: Operations.startPreviewsMultipartUpload.Input.Cookies = .init(), + body: Operations.startPreviewsMultipartUpload.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.startPreviewsMultipartUpload.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// The upload has been initiated and preview and upload unique identifier are returned to upload the various parts using multi-part uploads + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json/data`. + public struct dataPayload: Codable, Equatable, Hashable, Sendable { + /// The id of the preview. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json/data/preview_id`. + public var preview_id: Swift.String + /// The upload ID + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json/data/upload_id`. + public var upload_id: Swift.String + /// Creates a new `dataPayload`. + /// + /// - Parameters: + /// - preview_id: The id of the preview. + /// - upload_id: The upload ID + public init(preview_id: Swift.String, upload_id: Swift.String) { + self.preview_id = preview_id + self.upload_id = upload_id + } + public enum CodingKeys: String, CodingKey { + case preview_id + case upload_id + } + } + /// Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json/data`. + public var data: + Operations.startPreviewsMultipartUpload.Output.Ok.Body.jsonPayload + .dataPayload + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json/status`. + @frozen + public enum statusPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case success + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "success": self = .success + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .success: return "success" + } + } + public static var allCases: [statusPayload] { [.success] } + } + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/previews/start/POST/json/status`. + public var status: + Operations.startPreviewsMultipartUpload.Output.Ok.Body.jsonPayload + .statusPayload + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - data: Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + /// - status: + public init( + data: Operations.startPreviewsMultipartUpload.Output.Ok.Body.jsonPayload + .dataPayload, + status: Operations.startPreviewsMultipartUpload.Output.Ok.Body + .jsonPayload.statusPayload + ) { + self.data = data + self.status = status + } + public enum CodingKeys: String, CodingKey { + case data + case status + } + } + case json(Operations.startPreviewsMultipartUpload.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.startPreviewsMultipartUpload.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startPreviewsMultipartUpload.Output.Ok.Headers = .init(), + body: Operations.startPreviewsMultipartUpload.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The upload has been started + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.startPreviewsMultipartUpload.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startPreviewsMultipartUpload.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.startPreviewsMultipartUpload.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startPreviewsMultipartUpload.Output.Unauthorized.Headers = + .init(), + body: Operations.startPreviewsMultipartUpload.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.startPreviewsMultipartUpload.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.startPreviewsMultipartUpload.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.startPreviewsMultipartUpload.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startPreviewsMultipartUpload.Output.Forbidden.Headers = + .init(), + body: Operations.startPreviewsMultipartUpload.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.startPreviewsMultipartUpload.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.startPreviewsMultipartUpload.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.startPreviewsMultipartUpload.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startPreviewsMultipartUpload.Output.NotFound.Headers = + .init(), + body: Operations.startPreviewsMultipartUpload.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/start/post(startPreviewsMultipartUpload)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.startPreviewsMultipartUpload.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Downloads a preview. + /// + /// This endpoint returns a signed URL that can be used to download a preview. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}/previews/{preview_id}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)`. + public enum downloadPreview { + public static let id: String = "downloadPreview" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + public var preview_id: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + /// - preview_id: + public init( + account_handle: Swift.String, + project_handle: Swift.String, + preview_id: Swift.String + ) { + self.account_handle = account_handle + self.project_handle = project_handle + self.preview_id = preview_id + } + } + public var path: Operations.downloadPreview.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.downloadPreview.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.downloadPreview.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.downloadPreview.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.downloadPreview.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.downloadPreview.Input.Path, + query: Operations.downloadPreview.Input.Query = .init(), + headers: Operations.downloadPreview.Input.Headers = .init(), + cookies: Operations.downloadPreview.Input.Cookies = .init(), + body: Operations.downloadPreview.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadPreview.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.ArtifactDownloadURL) + } + /// Received HTTP response body + public var body: Operations.downloadPreview.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadPreview.Output.Ok.Headers = .init(), + body: Operations.downloadPreview.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The preview exists and can be downloaded + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.downloadPreview.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadPreview.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadPreview.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadPreview.Output.Unauthorized.Headers = .init(), + body: Operations.downloadPreview.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.downloadPreview.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadPreview.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadPreview.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadPreview.Output.Forbidden.Headers = .init(), + body: Operations.downloadPreview.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.downloadPreview.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.downloadPreview.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.downloadPreview.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.downloadPreview.Output.NotFound.Headers = .init(), + body: Operations.downloadPreview.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The build doesn't exist + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/previews/{preview_id}/get(downloadPreview)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.downloadPreview.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// List all project tokens. + /// + /// This endpoint returns all tokens for a given project. + /// + /// - Remark: HTTP `GET /api/projects/{account_handle}/{project_handle}/tokens`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)`. + public enum listProjectTokens { + public static let id: String = "listProjectTokens" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.listProjectTokens.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.listProjectTokens.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.listProjectTokens.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.listProjectTokens.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.listProjectTokens.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.listProjectTokens.Input.Path, + query: Operations.listProjectTokens.Input.Query = .init(), + headers: Operations.listProjectTokens.Input.Headers = .init(), + cookies: Operations.listProjectTokens.Input.Cookies = .init(), + body: Operations.listProjectTokens.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listProjectTokens.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// A list of project tokens. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/tokens/GET/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/tokens/GET/json/tokens`. + public var tokens: [Components.Schemas.ProjectToken] + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - tokens: + public init(tokens: [Components.Schemas.ProjectToken]) { + self.tokens = tokens + } + public enum CodingKeys: String, CodingKey { case tokens } + } + case json(Operations.listProjectTokens.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.listProjectTokens.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listProjectTokens.Output.Ok.Headers = .init(), + body: Operations.listProjectTokens.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// A list of project tokens. + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.listProjectTokens.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listProjectTokens.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listProjectTokens.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listProjectTokens.Output.Unauthorized.Headers = .init(), + body: Operations.listProjectTokens.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to list tokens + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.listProjectTokens.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listProjectTokens.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listProjectTokens.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listProjectTokens.Output.Forbidden.Headers = .init(), + body: Operations.listProjectTokens.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authorized to list tokens + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.listProjectTokens.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listProjectTokens.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listProjectTokens.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listProjectTokens.Output.NotFound.Headers = .init(), + body: Operations.listProjectTokens.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/get(listProjectTokens)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.listProjectTokens.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Create a new project token. + /// + /// This endpoint returns a new project token. + /// + /// - Remark: HTTP `POST /api/projects/{account_handle}/{project_handle}/tokens`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)`. + public enum createProjectToken { + public static let id: String = "createProjectToken" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + public init(account_handle: Swift.String, project_handle: Swift.String) { + self.account_handle = account_handle + self.project_handle = project_handle + } + } + public var path: Operations.createProjectToken.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.createProjectToken.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.createProjectToken.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.createProjectToken.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.createProjectToken.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.createProjectToken.Input.Path, + query: Operations.createProjectToken.Input.Query = .init(), + headers: Operations.createProjectToken.Input.Headers = .init(), + cookies: Operations.createProjectToken.Input.Cookies = .init(), + body: Operations.createProjectToken.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProjectToken.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// A new project token. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/tokens/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// The generated project token. + /// + /// - Remark: Generated from `#/paths/api/projects/{account_handle}/{project_handle}/tokens/POST/json/token`. + public var token: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - token: The generated project token. + public init(token: Swift.String) { self.token = token } + public enum CodingKeys: String, CodingKey { case token } + } + case json(Operations.createProjectToken.Output.Ok.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.createProjectToken.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProjectToken.Output.Ok.Headers = .init(), + body: Operations.createProjectToken.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// A project token was generated + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.createProjectToken.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProjectToken.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createProjectToken.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProjectToken.Output.Unauthorized.Headers = .init(), + body: Operations.createProjectToken.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to issue new tokens + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.createProjectToken.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProjectToken.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createProjectToken.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProjectToken.Output.Forbidden.Headers = .init(), + body: Operations.createProjectToken.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authorized to issue new tokens + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.createProjectToken.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.createProjectToken.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.createProjectToken.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createProjectToken.Output.NotFound.Headers = .init(), + body: Operations.createProjectToken.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/post(createProjectToken)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.createProjectToken.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Revokes a project token. + /// + /// - Remark: HTTP `DELETE /api/projects/{account_handle}/{project_handle}/tokens/{id}`. + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)`. + public enum revokeProjectToken { + public static let id: String = "revokeProjectToken" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var account_handle: Swift.String + public var project_handle: Swift.String + public var id: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - account_handle: + /// - project_handle: + /// - id: + public init( + account_handle: Swift.String, + project_handle: Swift.String, + id: Swift.String + ) { + self.account_handle = account_handle + self.project_handle = project_handle + self.id = id + } + } + public var path: Operations.revokeProjectToken.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.revokeProjectToken.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.revokeProjectToken.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.revokeProjectToken.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.revokeProjectToken.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.revokeProjectToken.Input.Path, + query: Operations.revokeProjectToken.Input.Query = .init(), + headers: Operations.revokeProjectToken.Input.Headers = .init(), + cookies: Operations.revokeProjectToken.Input.Cookies = .init(), + body: Operations.revokeProjectToken.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.revokeProjectToken.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.revokeProjectToken.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.revokeProjectToken.Output.NoContent.Headers = .init(), + body: Operations.revokeProjectToken.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// The project token was revoked + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.revokeProjectToken.Output.NoContent) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.revokeProjectToken.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.revokeProjectToken.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.revokeProjectToken.Output.BadRequest.Headers = .init(), + body: Operations.revokeProjectToken.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// The provided token ID is not valid + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.revokeProjectToken.Output.BadRequest) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.revokeProjectToken.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.revokeProjectToken.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.revokeProjectToken.Output.Unauthorized.Headers = .init(), + body: Operations.revokeProjectToken.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.revokeProjectToken.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.revokeProjectToken.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.revokeProjectToken.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.revokeProjectToken.Output.Forbidden.Headers = .init(), + body: Operations.revokeProjectToken.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.revokeProjectToken.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.revokeProjectToken.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.revokeProjectToken.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.revokeProjectToken.Output.NotFound.Headers = .init(), + body: Operations.revokeProjectToken.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project token was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{account_handle}/{project_handle}/tokens/{id}/delete(revokeProjectToken)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.revokeProjectToken.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Deletes a project with a given id. + /// + /// - Remark: HTTP `DELETE /api/projects/{id}`. + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)`. + public enum deleteProject { + public static let id: String = "deleteProject" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var id: Swift.Int + /// Creates a new `Path`. + /// + /// - Parameters: + /// - id: + public init(id: Swift.Int) { self.id = id } + } + public var path: Operations.deleteProject.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.deleteProject.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.deleteProject.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.deleteProject.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.deleteProject.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.deleteProject.Input.Path, + query: Operations.deleteProject.Input.Query = .init(), + headers: Operations.deleteProject.Input.Headers = .init(), + cookies: Operations.deleteProject.Input.Cookies = .init(), + body: Operations.deleteProject.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteProject.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.deleteProject.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteProject.Output.NoContent.Headers = .init(), + body: Operations.deleteProject.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// The project was successfully deleted. + /// + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.deleteProject.Output.NoContent) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteProject.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.deleteProject.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteProject.Output.Unauthorized.Headers = .init(), + body: Operations.deleteProject.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.deleteProject.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteProject.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.deleteProject.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteProject.Output.Forbidden.Headers = .init(), + body: Operations.deleteProject.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.deleteProject.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.deleteProject.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.deleteProject.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.deleteProject.Output.NotFound.Headers = .init(), + body: Operations.deleteProject.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project was not found + /// + /// - Remark: Generated from `#/paths//api/projects/{id}/delete(deleteProject)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.deleteProject.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It completes a multi-part upload. + /// + /// Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/complete`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)`. + public enum completeAnalyticsArtifactMultipartUpload { + public static let id: String = "completeAnalyticsArtifactMultipartUpload" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var run_id: Swift.Int + /// Creates a new `Path`. + /// + /// - Parameters: + /// - run_id: + public init(run_id: Swift.Int) { self.run_id = run_id } + } + public var path: Operations.completeAnalyticsArtifactMultipartUpload.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.completeAnalyticsArtifactMultipartUpload.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.completeAnalyticsArtifactMultipartUpload.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.completeAnalyticsArtifactMultipartUpload.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Command event artifact multipart upload completion + /// + /// - Remark: Generated from `#/paths/api/runs/{run_id}/complete/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/runs/{run_id}/complete/POST/json/command_event_artifact`. + public var command_event_artifact: Components.Schemas.CommandEventArtifact + /// - Remark: Generated from `#/paths/api/runs/{run_id}/complete/POST/json/multipart_upload_parts`. + public var multipart_upload_parts: + Components.Schemas.ArtifactMultipartUploadParts + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - command_event_artifact: + /// - multipart_upload_parts: + public init( + command_event_artifact: Components.Schemas.CommandEventArtifact, + multipart_upload_parts: Components.Schemas.ArtifactMultipartUploadParts + ) { + self.command_event_artifact = command_event_artifact + self.multipart_upload_parts = multipart_upload_parts + } + public enum CodingKeys: String, CodingKey { + case command_event_artifact + case multipart_upload_parts + } + } + case json( + Operations.completeAnalyticsArtifactMultipartUpload.Input.Body.jsonPayload + ) + } + public var body: Operations.completeAnalyticsArtifactMultipartUpload.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.completeAnalyticsArtifactMultipartUpload.Input.Path, + query: Operations.completeAnalyticsArtifactMultipartUpload.Input.Query = .init(), + headers: Operations.completeAnalyticsArtifactMultipartUpload.Input.Headers = + .init(), + cookies: Operations.completeAnalyticsArtifactMultipartUpload.Input.Cookies = + .init(), + body: Operations.completeAnalyticsArtifactMultipartUpload.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactMultipartUpload.Output.NoContent + .Headers = .init(), + body: Operations.completeAnalyticsArtifactMultipartUpload.Output.NoContent + .Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// The upload has been completed + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.completeAnalyticsArtifactMultipartUpload.Output.NoContent) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized + .Headers = .init(), + body: Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized + .Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized( + Operations.completeAnalyticsArtifactMultipartUpload.Output.Unauthorized + ) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden + .Headers = .init(), + body: Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.completeAnalyticsArtifactMultipartUpload.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound + .Headers = .init(), + body: Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete/post(completeAnalyticsArtifactMultipartUpload)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.completeAnalyticsArtifactMultipartUpload.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Completes artifacts uploads for a given command event + /// + /// Given a command event, it marks all artifact uploads as finished and does extra processing of a given command run, such as test flakiness detection. + /// + /// - Remark: HTTP `PUT /api/runs/{run_id}/complete_artifacts_uploads`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)`. + public enum completeAnalyticsArtifactsUploads { + public static let id: String = "completeAnalyticsArtifactsUploads" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var run_id: Swift.Int + /// Creates a new `Path`. + /// + /// - Parameters: + /// - run_id: + public init(run_id: Swift.Int) { self.run_id = run_id } + } + public var path: Operations.completeAnalyticsArtifactsUploads.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.completeAnalyticsArtifactsUploads.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.completeAnalyticsArtifactsUploads.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.completeAnalyticsArtifactsUploads.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Extra metadata for the post-processing of a command event. + /// + /// - Remark: Generated from `#/paths/api/runs/{run_id}/complete_artifacts_uploads/PUT/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// A list of modules with their metadata. + /// + /// - Remark: Generated from `#/paths/api/runs/{run_id}/complete_artifacts_uploads/PUT/json/modules`. + public var modules: [Components.Schemas.Module] + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - modules: A list of modules with their metadata. + public init(modules: [Components.Schemas.Module]) { self.modules = modules } + public enum CodingKeys: String, CodingKey { case modules } + } + case json(Operations.completeAnalyticsArtifactsUploads.Input.Body.jsonPayload) + } + public var body: Operations.completeAnalyticsArtifactsUploads.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.completeAnalyticsArtifactsUploads.Input.Path, + query: Operations.completeAnalyticsArtifactsUploads.Input.Query = .init(), + headers: Operations.completeAnalyticsArtifactsUploads.Input.Headers = .init(), + cookies: Operations.completeAnalyticsArtifactsUploads.Input.Cookies = .init(), + body: Operations.completeAnalyticsArtifactsUploads.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactsUploads.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.completeAnalyticsArtifactsUploads.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactsUploads.Output.NoContent.Headers = + .init(), + body: Operations.completeAnalyticsArtifactsUploads.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// The command event artifact uploads were successfully finished + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.completeAnalyticsArtifactsUploads.Output.NoContent) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized + .Headers = .init(), + body: Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.completeAnalyticsArtifactsUploads.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactsUploads.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.completeAnalyticsArtifactsUploads.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactsUploads.Output.Forbidden.Headers = + .init(), + body: Operations.completeAnalyticsArtifactsUploads.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.completeAnalyticsArtifactsUploads.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.completeAnalyticsArtifactsUploads.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.completeAnalyticsArtifactsUploads.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.completeAnalyticsArtifactsUploads.Output.NotFound.Headers = + .init(), + body: Operations.completeAnalyticsArtifactsUploads.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The command event doesn't exist + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/complete_artifacts_uploads/put(completeAnalyticsArtifactsUploads)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.completeAnalyticsArtifactsUploads.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It generates a signed URL for uploading a part. + /// + /// Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/generate-url`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)`. + public enum generateAnalyticsArtifactMultipartUploadURL { + public static let id: String = "generateAnalyticsArtifactMultipartUploadURL" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var run_id: Swift.Int + /// Creates a new `Path`. + /// + /// - Parameters: + /// - run_id: + public init(run_id: Swift.Int) { self.run_id = run_id } + } + public var path: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + /// Artifact to generate a signed URL for + /// + /// - Remark: Generated from `#/paths/api/runs/{run_id}/generate-url/POST/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/api/runs/{run_id}/generate-url/POST/json/command_event_artifact`. + public var command_event_artifact: Components.Schemas.CommandEventArtifact + /// - Remark: Generated from `#/paths/api/runs/{run_id}/generate-url/POST/json/multipart_upload_part`. + public var multipart_upload_part: Components.Schemas.ArtifactMultipartUploadPart + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - command_event_artifact: + /// - multipart_upload_part: + public init( + command_event_artifact: Components.Schemas.CommandEventArtifact, + multipart_upload_part: Components.Schemas.ArtifactMultipartUploadPart + ) { + self.command_event_artifact = command_event_artifact + self.multipart_upload_part = multipart_upload_part + } + public enum CodingKeys: String, CodingKey { + case command_event_artifact + case multipart_upload_part + } + } + case json( + Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Body.jsonPayload + ) + } + public var body: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Path, + query: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Query = .init(), + headers: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Headers = + .init(), + cookies: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Cookies = + .init(), + body: Operations.generateAnalyticsArtifactMultipartUploadURL.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.ArtifactMultipartUploadURL) + } + /// Received HTTP response body + public var body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok + .Headers = .init(), + body: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The URL has been generated + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Unauthorized + .Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateAnalyticsArtifactMultipartUploadURL.Output + .Unauthorized.Headers = .init(), + body: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Unauthorized + .Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized( + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Unauthorized + ) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden + .Headers = .init(), + body: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden + .Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.generateAnalyticsArtifactMultipartUploadURL.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound + .Headers = .init(), + body: Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound + .Body + ) { + self.headers = headers + self.body = body + } + } + /// The project doesn't exist + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/generate-url/post(generateAnalyticsArtifactMultipartUploadURL)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.generateAnalyticsArtifactMultipartUploadURL.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// It initiates a multipart upload for a command event artifact. + /// + /// The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + /// + /// - Remark: HTTP `POST /api/runs/{run_id}/start`. + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)`. + public enum startAnalyticsArtifactMultipartUpload { + public static let id: String = "startAnalyticsArtifactMultipartUpload" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var run_id: Swift.Int + /// Creates a new `Path`. + /// + /// - Parameters: + /// - run_id: + public init(run_id: Swift.Int) { self.run_id = run_id } + } + public var path: Operations.startAnalyticsArtifactMultipartUpload.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.startAnalyticsArtifactMultipartUpload.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.startAnalyticsArtifactMultipartUpload.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.startAnalyticsArtifactMultipartUpload.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.CommandEventArtifact) + } + public var body: Operations.startAnalyticsArtifactMultipartUpload.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.startAnalyticsArtifactMultipartUpload.Input.Path, + query: Operations.startAnalyticsArtifactMultipartUpload.Input.Query = .init(), + headers: Operations.startAnalyticsArtifactMultipartUpload.Input.Headers = .init(), + cookies: Operations.startAnalyticsArtifactMultipartUpload.Input.Cookies = .init(), + body: Operations.startAnalyticsArtifactMultipartUpload.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.ArtifactUploadID) + } + /// Received HTTP response body + public var body: Operations.startAnalyticsArtifactMultipartUpload.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startAnalyticsArtifactMultipartUpload.Output.Ok.Headers = + .init(), + body: Operations.startAnalyticsArtifactMultipartUpload.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// The upload has been started + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.startAnalyticsArtifactMultipartUpload.Output.Ok) + public struct Unauthorized: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized.Body + /// Creates a new `Unauthorized`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized + .Headers = .init(), + body: Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized.Body + ) { + self.headers = headers + self.body = body + } + } + /// You need to be authenticated to access this resource + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)/responses/401`. + /// + /// HTTP response code: `401 unauthorized`. + case unauthorized(Operations.startAnalyticsArtifactMultipartUpload.Output.Unauthorized) + public struct Forbidden: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden.Body + /// Creates a new `Forbidden`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden + .Headers = .init(), + body: Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden.Body + ) { + self.headers = headers + self.body = body + } + } + /// The authenticated subject is not authorized to perform this action + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)/responses/403`. + /// + /// HTTP response code: `403 forbidden`. + case forbidden(Operations.startAnalyticsArtifactMultipartUpload.Output.Forbidden) + public struct NotFound: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: + Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: + Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound.Body + /// Creates a new `NotFound`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound + .Headers = .init(), + body: Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound.Body + ) { + self.headers = headers + self.body = body + } + } + /// The command event doesn't exist + /// + /// - Remark: Generated from `#/paths//api/runs/{run_id}/start/post(startAnalyticsArtifactMultipartUpload)/responses/404`. + /// + /// HTTP response code: `404 notFound`. + case notFound(Operations.startAnalyticsArtifactMultipartUpload.Output.NotFound) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } +} diff --git a/Sources/TuistServer/OpenAPI/server.yml b/Sources/TuistServer/OpenAPI/server.yml new file mode 100644 index 00000000000..50c2868ed04 --- /dev/null +++ b/Sources/TuistServer/OpenAPI/server.yml @@ -0,0 +1,2578 @@ +--- +components: + responses: {} + schemas: + AbsentCacheArtifact: + properties: + error: + items: + properties: + code: + default: not_found + type: string + message: + type: string + type: object + type: array + title: AbsentCacheArtifact + type: object + ArtifactDownloadURL: + description: The URL to download an artifact. + properties: + expires_at: + description: The UNIX timestamp when the URL expires. + type: integer + url: + description: The URL to download the artifact. + type: string + required: + - url + - expires_at + title: ArtifactDownloadURL + type: object + x-struct: Elixir.TuistWeb.API.Schemas.ArtifactDownloadURL + ArtifactMultipartUploadPart: + description: Represents an multipart upload's part identified by the upload id and the part number + properties: + part_number: + description: The part number of the multipart upload. + type: integer + upload_id: + description: The upload ID. + type: string + required: + - part_number + - upload_id + title: ArtifactMultipartUploadPart + x-struct: Elixir.TuistWeb.API.Schemas.ArtifactMultipartUploadPart + ArtifactMultipartUploadParts: + description: It represents a part that has been uploaded using multipart uploads. The part is identified by its number and the etag + properties: + parts: + items: + properties: + etag: + description: The ETag of the part + type: string + part_number: + description: The part number + type: integer + required: + - part_number + - etag + type: object + type: array + upload_id: + description: The upload ID + type: string + required: + - upload_id + - parts + title: ArtifactMultipartUploadParts + type: object + x-struct: Elixir.TuistWeb.API.Schemas.ArtifactMultipartUploadParts + ArtifactMultipartUploadURL: + description: The URL to upload a multipart part + properties: + data: + properties: + url: + description: The URL to upload the part + type: string + required: + - url + type: object + status: + default: success + enum: + - success + type: string + required: + - status + - data + title: ArtifactMultipartUploadURL + type: object + x-struct: Elixir.TuistWeb.API.Schemas.ArtifactMultipartUploadUrl + ArtifactUploadID: + description: The upload has been initiated and a ID is returned to upload the various parts using multi-part uploads + properties: + data: + description: Data that contains ID that's associated with the multipart upload to use when uploading parts + properties: + upload_id: + description: The upload ID + type: string + required: + - upload_id + type: object + status: + default: success + enum: + - success + type: string + required: + - status + - data + title: ArtifactUploadID + type: object + x-struct: Elixir.TuistWeb.API.Schemas.ArtifactUploadId + AuthenticationTokens: + description: A pair of access token to authenticate requests and refresh token to generate new access tokens when they expire. + properties: + access_token: + description: API access token. + type: string + refresh_token: + description: A token to generate new API access tokens when they expire. + type: string + required: + - access_token + - refresh_token + title: AuthenticationTokens + type: object + x-struct: Elixir.TuistWeb.API.Schemas.AuthenticationTokens + CacheArtifactDownloadURL: + description: The URL to download the artifact from the cache. + properties: + data: + properties: + expires_at: + description: The UNIX timestamp when the URL expires. + type: integer + url: + description: The URL to download the artifact from the cache. + type: string + required: + - url + - expires_at + type: object + status: + default: success + enum: + - success + type: string + required: + - status + - data + title: CacheArtifactDownloadURL + type: object + x-struct: Elixir.TuistWeb.API.Schemas.CacheArtifactDownloadURL + CacheArtifactExistence: + description: The artifact exists in the cache and can be downloaded + properties: + data: + properties: {} + type: object + status: + default: success + enum: + - success + type: string + title: CacheArtifactExistence + type: object + CacheArtifactMultipartUploadCompletion: + description: This response confirms that the upload has been completed successfully. The cache will now be able to serve the artifact. + properties: + data: + properties: {} + type: object + status: + default: success + enum: + - success + type: string + title: CacheArtifactMultipartUploadCompletion + type: object + CacheCategory: + default: builds + description: The category of the cache. + enum: + - tests + - builds + title: CacheCategory + type: string + x-struct: Elixir.TuistWeb.API.Schemas.CacheCategory + CommandEvent: + description: The schema for the command analytics event. + properties: + id: + description: ID of the command event + type: number + name: + description: Name of the command + type: string + url: + description: URL to the command event + type: string + required: + - id + - name + - url + title: CommandEvent + type: object + x-struct: Elixir.TuistWeb.API.Schemas.CommandEvent + CommandEventArtifact: + description: It represents an artifact that's associated with a command event (e.g. result bundles) + properties: + name: + description: The name of the file. It's used only for certain types such as result_bundle_object + type: string + type: + description: | + The command event artifact type. It can be: + - result_bundle: A result bundle artifact that represents the whole `.xcresult` bundle + - invocation_record: An invocation record artifact. This is a root bundle object of the result bundle + - result_bundle_object: A result bundle object. There are many different bundle objects per result bundle. + enum: + - result_bundle + - invocation_record + - result_bundle_object + type: string + required: + - type + title: CommandEventArtifact + x-struct: Elixir.TuistWeb.API.Schemas.CommandEventArtifact + DeviceCodeAuthenticationTokens: + description: Token to authenticate the user with. + properties: + access_token: + description: A short-lived token to authenticate API requests as user. + type: string + refresh_token: + description: A token to generate new access tokens when they expire. + type: string + token: + deprecated: true + description: User authentication token + type: string + title: DeviceCodeAuthenticationTokens + type: object + Error: + properties: + message: + description: The error message + type: string + required: + - message + title: Error + type: object + x-struct: Elixir.TuistWeb.API.Schemas.Error + Invitation: + properties: + id: + description: The invitation's unique identifier + type: number + invitee_email: + description: The email of the invitee + type: string + inviter: + $ref: '#/components/schemas/User' + organization_id: + description: The id of the organization the invitee is invited to + type: number + token: + description: The token to accept the invitation + type: string + required: + - id + - invitee_email + - organization_id + - inviter + - token + title: Invitation + type: object + x-struct: Elixir.TuistWeb.API.Schemas.Invitation + Module: + properties: + hash: + description: A hash that represents the module. + type: string + name: + description: A name of the module + type: string + project_identifier: + description: Project's relative path from the root of the repository + type: string + required: + - name + - project_identifier + - hash + title: Module + type: object + x-struct: Elixir.TuistWeb.API.Schemas.Module + Organization: + description: An organization + properties: + id: + description: The organization's unique identifier + type: number + invitations: + description: A list of organization invitations + items: + $ref: '#/components/schemas/Invitation' + type: array + members: + description: A list of organization members + items: + $ref: '#/components/schemas/OrganizationMember' + type: array + name: + description: The organization's name + type: string + plan: + description: The plan associated with the organization + enum: + - air + - pro + - enterprise + - none + type: string + sso_organization_id: + description: The organization ID associated with the SSO provider + type: string + sso_provider: + description: The SSO provider set up for the organization + enum: + - google + type: string + required: + - id + - name + - plan + - members + - invitations + title: Organization + type: object + x-struct: Elixir.TuistWeb.API.Schemas.Organization + OrganizationList: + description: The list of organizations the authenticated subject is part of. + properties: + organizations: + items: + $ref: '#/components/schemas/Organization' + type: array + required: + - organizations + title: OrganizationList + type: object + OrganizationMember: + description: An organization member + properties: + email: + description: The organization member's email + type: string + id: + description: The organization member's unique identifier + type: number + name: + description: The organization member's name + type: string + role: + description: The organization member's role + enum: + - admin + - user + type: string + required: + - id + - email + - name + - role + title: OrganizationMember + type: object + x-struct: Elixir.TuistWeb.API.Schemas.OrganizationMember + OrganizationUsage: + description: The usage of an organization. + properties: + current_month_remote_cache_hits: + description: The number of remote cache hits in the current month + type: number + required: + - current_month_remote_cache_hits + title: OrganizationUsage + type: object + x-struct: Elixir.TuistWeb.API.Schemas.OrganizationUsage + PreviewArtifactUpload: + description: The upload has been initiated and preview and upload unique identifier are returned to upload the various parts using multi-part uploads + properties: + data: + description: Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + properties: + preview_id: + description: The id of the preview. + type: string + upload_id: + description: The upload ID + type: string + required: + - upload_id + - preview_id + type: object + status: + default: success + enum: + - success + type: string + required: + - status + - data + title: PreviewArtifactUpload + type: object + PreviewUploadCompletion: + description: The preview multipart upload has been completed + properties: + url: + description: The URL to download the preview + type: string + required: + - url + title: PreviewUploadCompletion + type: object + Project: + properties: + default_branch: + description: The default branch of the project. + example: main + type: string + full_name: + description: The full name of the project (e.g. tuist/tuist) + type: string + id: + description: ID of the project + type: number + token: + deprecated: true + description: The token that should be used to authenticate the project. For CI only. + type: string + required: + - id + - full_name + - token + - default_branch + title: Project + type: object + x-struct: Elixir.TuistWeb.API.Schemas.Project + ProjectFullToken: + description: A new project token. + properties: + token: + description: The generated project token. + type: string + required: + - token + title: ProjectFullToken + type: object + ProjectToken: + description: A token to authenticate API requests as a project. + properties: + id: + description: The token unique identifier. + type: string + inserted_at: + description: The timestamp of when the token was created. + format: date-time + type: string + required: + - id + - inserted_at + title: ProjectToken + type: object + x-struct: Elixir.TuistWeb.API.Schemas.ProjectToken + Tokens: + description: A list of project tokens. + properties: + tokens: + items: + $ref: '#/components/schemas/ProjectToken' + type: array + required: + - tokens + title: Tokens + type: object + User: + description: A user. + properties: + email: + description: The user's email + type: string + id: + description: The user's unique identifier + type: number + name: + description: The user's name + type: string + required: + - id + - email + - name + title: User + type: object + x-struct: Elixir.TuistWeb.API.Schemas.User + securitySchemes: + authorization: + scheme: bearer + type: http + cookie: + in: cookie + name: _tuist_cloud_key + type: apiKey +info: + title: Tuist + version: 0.1.0 + x-logo: + altText: Tuist logo + url: http://localhost:8080/ +openapi: 3.0.0 +paths: + /api/analytics: + post: + callbacks: {} + operationId: createCommandEvent + parameters: + - description: The project id. + in: query + name: project_id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + client_id: + description: The client id of the command. + type: string + command_arguments: + description: The arguments of the command. + items: + type: string + type: array + duration: + description: The duration of the command. + type: number + error_message: + description: The error message of the command. + type: string + is_ci: + description: Whether the command was run in a CI environment. + type: boolean + macos_version: + description: The version of macOS that ran the command. + type: string + name: + description: The name of the command. + type: string + params: + description: Extra parameters. + properties: + cacheable_targets: + description: A list of cacheable targets. + items: + type: string + type: array + local_cache_target_hits: + description: A list of local cache target hits. + items: + type: string + type: array + local_test_target_hits: + description: A list of local targets whose tests were skipped. + items: + type: string + type: array + remote_cache_target_hits: + description: A list of remote cache target hits. + items: + type: string + type: array + remote_test_target_hits: + description: A list of remote targets whose tests were skipped. + items: + type: string + type: array + test_targets: + description: The list of targets that were tested. + items: + type: string + type: array + type: object + status: + description: The status of the command. + enum: + - success + - failure + type: string + subcommand: + description: The subcommand of the command. + type: string + swift_version: + description: The version of Swift that ran the command. + type: string + tuist_version: + description: The version of Tuist that ran the command. + type: string + required: + - name + - duration + - tuist_version + - swift_version + - macos_version + - is_ci + - client_id + type: object + description: Command event params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/CommandEvent' + description: The command event was created + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You don't have permission to create command events for the project. + summary: Create a a new command analytics event + tags: + - Analytics + /api/auth: + post: + callbacks: {} + description: This endpoint returns API tokens for a given email and password. + operationId: authenticate + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + email: + description: The email to authenticate with. + type: string + password: + description: The password to authenticate with. + type: string + required: + - email + - password + type: object + description: Authentication params. + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticationTokens' + description: Successfully authenticated and returned new API tokens. + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: Invalid email or password. + summary: Authenticate with email and password. + tags: + - Authentication + /api/auth/device_code/{device_code}: + get: + callbacks: {} + description: This endpoint returns a token for a given device code if the device code is authenticated. + operationId: getDeviceCode + parameters: + - description: The device code to query. + in: path + name: device_code + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + description: Token to authenticate the user with. + properties: + access_token: + description: A short-lived token to authenticate API requests as user. + type: string + refresh_token: + description: A token to generate new access tokens when they expire. + type: string + token: + deprecated: true + description: User authentication token + type: string + title: DeviceCodeAuthenticationTokens + type: object + description: The device code is authenticated + 202: + content: + application/json: + schema: + type: object + description: The device code is not authenticated + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The request was not accepted, e.g., when the device code is expired + summary: Get a specific device code. + tags: + - Authentication + /api/auth/refresh_token: + post: + callbacks: {} + description: This endpoint returns new tokens for a given refresh token if the refresh token is valid. + operationId: refreshToken + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + refresh_token: + description: User refresh token + type: string + required: + - refresh_token + type: object + description: Token params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticationTokens' + description: Succcessfully generated new API tokens. + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to issue new tokens + summary: Request new tokens. + tags: + - Authentication + /api/cache: + get: + callbacks: {} + description: This endpoint returns a signed URL that can be used to download an artifact from the cache. + operationId: downloadCacheArtifact + parameters: + - description: The category of the cache. It's used to differentiate between different types of caches. + in: query + name: cache_category + required: false + schema: + $ref: '#/components/schemas/CacheCategory' + - description: The project identifier '{account_name}/{project_name}'. + in: query + name: project_id + required: true + schema: + type: string + - description: The hash that uniquely identifies the artifact in the cache. + in: query + name: hash + required: true + schema: + type: string + - description: The name of the artifact. + in: query + name: name + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/CacheArtifactDownloadURL' + description: The artifact exists and is downloadable + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 402: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The account has an invalid plan + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project or the cache artifact doesn't exist + summary: Downloads an artifact from the cache. + tags: + - Cache + /api/cache/exists: + get: + callbacks: {} + deprecated: true + description: This endpoint checks if an artifact exists in the cache. It returns a 404 status code if the artifact does not exist. + operationId: cacheArtifactExists + parameters: + - description: The category of the cache. It's used to differentiate between different types of caches. + in: query + name: cache_category + required: false + schema: + $ref: '#/components/schemas/CacheCategory' + - description: The project identifier '{account_name}/{project_name}'. + in: query + name: project_id + required: true + schema: + type: string + - description: The hash that uniquely identifies the artifact in the cache. + in: query + name: hash + required: true + schema: + type: string + - description: The name of the artifact. + in: query + name: name + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + description: The artifact exists in the cache and can be downloaded + properties: + data: + properties: {} + type: object + status: + default: success + enum: + - success + type: string + title: CacheArtifactExistence + type: object + description: The artifact exists + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 402: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The account has an invalid plan + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + properties: + error: + items: + properties: + code: + default: not_found + type: string + message: + type: string + type: object + type: array + title: AbsentCacheArtifact + type: object + description: The artifact doesn't exist + summary: It checks if an artifact exists in the cache. + tags: + - Cache + /api/cache/multipart/complete: + post: + callbacks: {} + description: Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. The cache will then be able to serve the artifact. + operationId: completeCacheArtifactMultipartUpload + parameters: + - description: The category of the cache. It's used to differentiate between different types of caches. + in: query + name: cache_category + required: false + schema: + $ref: '#/components/schemas/CacheCategory' + - description: The project identifier '{account_name}/{project_name}'. + in: query + name: project_id + required: true + schema: + type: string + - description: The hash that uniquely identifies the artifact in the cache. + in: query + name: hash + required: true + schema: + type: string + - description: The upload ID. + in: query + name: upload_id + required: true + schema: + type: string + - description: The name of the artifact. + in: query + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + parts: + items: + properties: + etag: + description: The ETag of the part + type: string + part_number: + description: The part number + type: integer + type: object + type: array + type: object + description: Multi-part upload parts + required: false + responses: + 200: + content: + application/json: + schema: + description: This response confirms that the upload has been completed successfully. The cache will now be able to serve the artifact. + properties: + data: + properties: {} + type: object + status: + default: success + enum: + - success + type: string + title: CacheArtifactMultipartUploadCompletion + type: object + description: The upload has been completed + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 402: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The account has an invalid plan + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It completes a multi-part upload. + tags: + - Cache + /api/cache/multipart/generate-url: + post: + callbacks: {} + description: Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + operationId: generateCacheArtifactMultipartUploadURL + parameters: + - description: The category of the cache. It's used to differentiate between different types of caches. + in: query + name: cache_category + required: false + schema: + $ref: '#/components/schemas/CacheCategory' + - description: The project identifier '{account_name}/{project_name}'. + in: query + name: project_id + required: true + schema: + type: string + - description: The hash that uniquely identifies the artifact in the cache. + in: query + name: hash + required: true + schema: + type: string + - description: The part number of the multipart upload. + in: query + name: part_number + required: true + schema: + type: integer + - description: The upload ID. + in: query + name: upload_id + required: true + schema: + type: string + - description: The name of the artifact. + in: query + name: name + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactMultipartUploadURL' + description: The URL has been generated + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 402: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The account has an invalid plan + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It generates a signed URL for uploading a part. + tags: + - Cache + /api/cache/multipart/start: + post: + callbacks: {} + description: The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + operationId: startCacheArtifactMultipartUpload + parameters: + - description: The category of the cache. It's used to differentiate between different types of caches. + in: query + name: cache_category + required: false + schema: + $ref: '#/components/schemas/CacheCategory' + - description: The project identifier '{account_name}/{project_name}'. + in: query + name: project_id + required: true + schema: + type: string + - description: The hash that uniquely identifies the artifact in the cache. + in: query + name: hash + required: true + schema: + type: string + - description: The name of the artifact. + in: query + name: name + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactUploadID' + description: The upload has been started + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 402: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The account has an invalid plan + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It initiates a multipart upload in the cache. + tags: + - Cache + /api/organizations: + get: + callbacks: {} + description: Returns all the organizations the authenticated subject is part of. + operationId: listOrganizations + parameters: [] + responses: + 200: + content: + application/json: + schema: + description: The list of organizations the authenticated subject is part of. + properties: + organizations: + items: + $ref: '#/components/schemas/Organization' + type: array + required: + - organizations + title: OrganizationList + type: object + description: The list of organizations + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + summary: Lists the organizations + tags: + - Organizations + post: + callbacks: {} + description: Creates an organization with the given name. + operationId: createOrganization + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + name: + description: The name of the organization that should be created. + type: string + required: + - name + type: object + description: Organization params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + description: The organization was created + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization could not be created due to a validation error + summary: Creates an organization + tags: + - Organizations + /api/organizations/{organization_name}: + delete: + callbacks: {} + description: Deletes the organization with the given name. + operationId: deleteOrganization + parameters: + - description: The name of the organization to delete. + in: path + name: organization_name + required: true + schema: + type: string + responses: + 204: + description: The organization was deleted + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization with the given name was not found + summary: Deletes an organization + tags: + - Organizations + get: + callbacks: {} + description: Returns the organization with the given identifier. + operationId: showOrganization + parameters: + - description: The name of the organization to show. + in: path + name: organization_name + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + description: The organization + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization with the given name was not found + summary: Shows an organization + tags: + - Organizations + patch: + callbacks: {} + description: Updates an organization with given parameters. + operationId: updateOrganization (2) + parameters: + - description: The name of the organization to update. + in: path + name: organization_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + sso_organization_id: + description: The SSO organization ID to be associated with the SSO provider + nullable: true + type: string + sso_provider: + description: The SSO provider to set up for the organization + enum: + - google + - none + type: string + type: object + description: Organization update params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + description: The organization + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization could not be updated due to a validation error + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization with the given name was not found + summary: Updates an organization + tags: + - Organizations + put: + callbacks: {} + description: Updates an organization with given parameters. + operationId: updateOrganization + parameters: + - description: The name of the organization to update. + in: path + name: organization_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + sso_organization_id: + description: The SSO organization ID to be associated with the SSO provider + nullable: true + type: string + sso_provider: + description: The SSO provider to set up for the organization + enum: + - google + - none + type: string + type: object + description: Organization update params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + description: The organization + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization could not be updated due to a validation error + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization with the given name was not found + summary: Updates an organization + tags: + - Organizations + /api/organizations/{organization_name}/invitations: + delete: + callbacks: {} + description: Cancels an invitation for a given invitee email and an organization. + operationId: cancelInvitation + parameters: + - description: The name of the organization. + in: path + name: organization_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + invitee_email: + description: The email of the invitee. + type: string + required: + - invitee_email + type: object + description: Invitation params + required: false + responses: + 204: + content: + application/json: {} + description: The invitation was cancelled + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The invitation with the given invitee email and organization name was not found + summary: Cancels an invitation + tags: + - Invitations + post: + callbacks: {} + description: Invites a user with a given email to a given organization. + operationId: createInvitation + parameters: + - description: The name of the organization. + in: path + name: organization_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + invitee_email: + description: The email of the invitee. + type: string + required: + - invitee_email + type: object + description: Invitation params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Invitation' + description: The user was invited + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The user could not be invited due to a validation error + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization was not found + summary: Creates an invitation + tags: + - Invitations + /api/organizations/{organization_name}/members/{user_name}: + delete: + callbacks: {} + description: Removes a member with a given username from a given organization + operationId: removeOrganizationMember + parameters: + - description: The name of the organization to remove the member from. + in: path + name: organization_name + required: true + schema: + type: string + - description: The name of the user to remove from the organization. + in: path + name: user_name + required: true + schema: + type: string + responses: + 204: + content: + application/json: {} + description: The member was removed + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The member could not be removed due to a validation error + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization or the user with the given name was not found + summary: Removes a member from an organization + tags: + - Organizations + put: + callbacks: {} + description: Updates a member in a given organization + operationId: updateOrganizationMember + parameters: + - description: The name of the organization to update the member in. + in: path + name: organization_name + required: true + schema: + type: string + - description: The name of the user to update in the organization. + in: path + name: user_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + role: + description: The role to update the member to + enum: + - admin + - user + type: string + required: + - role + type: object + description: Member update params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/OrganizationMember' + description: The member was updated + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The member could not be updated due to a validation error + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization or the user with the given name was not found + summary: Updates a member in an organization + tags: + - Organizations + /api/organizations/{organization_name}/usage: + get: + callbacks: {} + description: Returns the usage of the organization with the given identifier. (e.g. number of remote cache hits) + operationId: showOrganizationUsage + parameters: + - description: The name of the organization to show. + in: path + name: organization_name + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/OrganizationUsage' + description: The organization usage + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The organization with the given name was not found + summary: Shows the usage of an organization + tags: + - Organizations + /api/projects: + get: + callbacks: {} + operationId: listProjects + parameters: [] + responses: + 200: + content: + application/json: + schema: + properties: + projects: + items: + $ref: '#/components/schemas/Project' + type: array + required: + - projects + type: object + description: List of projects + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + summary: List projects the authenticated user has access to. + tags: + - Projects + post: + callbacks: {} + operationId: createProject + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + full_handle: + description: The full handle of the project that should be created. + example: tuist/tuist + type: string + name: + deprecated: true + description: The name of the project that should be created. + type: string + organization: + deprecated: true + description: Organization to create the project with. If not specified, the project will be created with the current user's personal account. + type: string + type: object + description: Projects params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: The project was created + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The account was not found + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + summary: Create a new project. + tags: + - Projects + /api/projects/{account_handle}/{project_handle}: + get: + callbacks: {} + operationId: showProject + parameters: + - description: The name of the account that the project belongs to. + in: path + name: account_handle + required: true + schema: + type: string + - description: The name of the project to show + in: path + name: project_handle + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: The project to show + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project was not found + summary: Returns a project based on the handle. + tags: + - Projects + put: + callbacks: {} + description: Updates an project with given parameters. + operationId: updateProject + parameters: + - description: The handle of the project's account. + in: path + name: account_handle + required: true + schema: + type: string + - description: The handle of the project to update. + in: path + name: project_handle + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + default_branch: + description: The default branch for the project. + type: string + type: object + description: Project update params + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: The updated project + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project with the given account and project handles was not found + summary: Updates a project + tags: + - Projects + /api/projects/{account_handle}/{project_handle}/cache/clean: + put: + callbacks: {} + operationId: cleanCache + parameters: + - description: The name of the account that the project belongs to. + in: path + name: account_handle + required: true + schema: + type: string + - description: The name of the project to clean cache for + in: path + name: project_handle + required: true + schema: + type: string + responses: + 204: + description: The cache has been successfully cleaned + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project was not found + summary: Cleans cache for a given project + tags: + - Cache + /api/projects/{account_handle}/{project_handle}/previews/complete: + post: + callbacks: {} + description: Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + operationId: completePreviewsMultipartUpload + parameters: + - description: The handle of the account. + in: path + name: account_handle + required: true + schema: + type: string + - description: The handle of the project. + in: path + name: project_handle + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + description: The request body to complete the multipart upload of a preview. + properties: + multipart_upload_parts: + $ref: '#/components/schemas/ArtifactMultipartUploadParts' + preview_id: + description: The id of the preview. + type: string + required: + - multipart_upload_parts + - preview_id + type: object + description: preview multipart upload completion + required: false + responses: + 200: + content: + application/json: + schema: + description: The preview multipart upload has been completed + properties: + url: + description: The URL to download the preview + type: string + required: + - url + title: PreviewUploadCompletion + type: object + description: The upload has been completed + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It completes a multi-part upload. + tags: [] + /api/projects/{account_handle}/{project_handle}/previews/generate-url: + post: + callbacks: {} + description: Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + operationId: generatePreviewsMultipartUploadURL + parameters: + - description: The handle of the account. + in: path + name: account_handle + required: true + schema: + type: string + - description: The handle of the project. + in: path + name: project_handle + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + multipart_upload_part: + $ref: '#/components/schemas/ArtifactMultipartUploadPart' + preview_id: + description: The id of the preview. + type: string + required: + - multipart_upload_part + - preview_id + type: object + description: Artifact to generate a signed URL for + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactMultipartUploadURL' + description: The URL has been generated + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It generates a signed URL for uploading a part. + tags: [] + /api/projects/{account_handle}/{project_handle}/previews/start: + post: + callbacks: {} + description: The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + operationId: startPreviewsMultipartUpload + parameters: + - description: The handle of the account. + in: path + name: account_handle + required: true + schema: + type: string + - description: The handle of the project. + in: path + name: project_handle + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + description: The upload has been initiated and preview and upload unique identifier are returned to upload the various parts using multi-part uploads + properties: + data: + description: Data that contains preview and upload unique identifier associated with the multipart upload to use when uploading parts + properties: + preview_id: + description: The id of the preview. + type: string + upload_id: + description: The upload ID + type: string + required: + - upload_id + - preview_id + type: object + status: + default: success + enum: + - success + type: string + required: + - status + - data + title: PreviewArtifactUpload + type: object + description: The upload has been started + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It initiates a multipart upload for a preview artifact. + tags: [] + /api/projects/{account_handle}/{project_handle}/previews/{preview_id}: + get: + callbacks: {} + description: This endpoint returns a signed URL that can be used to download a preview. + operationId: downloadPreview + parameters: + - description: The handle of the account. + in: path + name: account_handle + required: true + schema: + type: string + - description: The handle of the project. + in: path + name: project_handle + required: true + schema: + type: string + - description: The id of the preview. + in: path + name: preview_id + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactDownloadURL' + description: The preview exists and can be downloaded + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The build doesn't exist + summary: Downloads a preview. + tags: [] + /api/projects/{account_handle}/{project_handle}/tokens: + get: + callbacks: {} + description: This endpoint returns all tokens for a given project. + operationId: listProjectTokens + parameters: + - description: The account handle. + in: path + name: account_handle + required: true + schema: + type: string + - description: The project handle. + in: path + name: project_handle + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + description: A list of project tokens. + properties: + tokens: + items: + $ref: '#/components/schemas/ProjectToken' + type: array + required: + - tokens + title: Tokens + type: object + description: A list of project tokens. + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to list tokens + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authorized to list tokens + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project was not found + summary: List all project tokens. + tags: + - Project tokens + post: + callbacks: {} + description: This endpoint returns a new project token. + operationId: createProjectToken + parameters: + - description: The account handle. + in: path + name: account_handle + required: true + schema: + type: string + - description: The project handle. + in: path + name: project_handle + required: true + schema: + type: string + responses: + 200: + content: + application/json: + schema: + description: A new project token. + properties: + token: + description: The generated project token. + type: string + required: + - token + title: ProjectFullToken + type: object + description: A project token was generated + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to issue new tokens + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authorized to issue new tokens + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project was not found + summary: Create a new project token. + tags: + - Project tokens + /api/projects/{account_handle}/{project_handle}/tokens/{id}: + delete: + callbacks: {} + operationId: revokeProjectToken + parameters: + - description: The account handle. + in: path + name: account_handle + required: true + schema: + type: string + - description: The project handle. + in: path + name: project_handle + required: true + schema: + type: string + - description: The ID of the project token + in: path + name: id + required: true + schema: + type: string + responses: + 204: + description: The project token was revoked + 400: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The provided token ID is not valid + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project token was not found + summary: Revokes a project token. + tags: + - Project tokens + /api/projects/{id}: + delete: + callbacks: {} + operationId: deleteProject + parameters: + - description: The id of the project to delete. + in: path + name: id + required: true + schema: + type: integer + responses: + 204: + description: The project was successfully deleted. + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project was not found + summary: Deletes a project with a given id. + tags: + - Projects + /api/runs/{run_id}/complete: + post: + callbacks: {} + description: Given the upload ID and all the parts with their ETags, this endpoint completes the multipart upload. + operationId: completeAnalyticsArtifactMultipartUpload + parameters: + - description: The id of the command event. + in: path + name: run_id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + properties: + command_event_artifact: + $ref: '#/components/schemas/CommandEventArtifact' + multipart_upload_parts: + $ref: '#/components/schemas/ArtifactMultipartUploadParts' + required: + - command_event_artifact + - multipart_upload_parts + type: object + description: Command event artifact multipart upload completion + required: false + responses: + 204: + description: The upload has been completed + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It completes a multi-part upload. + tags: + - Analytics + /api/runs/{run_id}/complete_artifacts_uploads: + put: + callbacks: {} + description: Given a command event, it marks all artifact uploads as finished and does extra processing of a given command run, such as test flakiness detection. + operationId: completeAnalyticsArtifactsUploads + parameters: + - description: The id of the command event. + in: path + name: run_id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + properties: + modules: + description: A list of modules with their metadata. + items: + $ref: '#/components/schemas/Module' + type: array + required: + - modules + type: object + description: Extra metadata for the post-processing of a command event. + required: false + responses: + 204: + description: The command event artifact uploads were successfully finished + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The command event doesn't exist + summary: Completes artifacts uploads for a given command event + tags: + - Analytics + /api/runs/{run_id}/generate-url: + post: + callbacks: {} + description: Given an upload ID and a part number, this endpoint returns a signed URL that can be used to upload a part of a multipart upload. The URL is short-lived and expires in 120 seconds. + operationId: generateAnalyticsArtifactMultipartUploadURL + parameters: + - description: The id of the command event. + in: path + name: run_id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + properties: + command_event_artifact: + $ref: '#/components/schemas/CommandEventArtifact' + multipart_upload_part: + $ref: '#/components/schemas/ArtifactMultipartUploadPart' + required: + - command_event_artifact + - multipart_upload_part + type: object + description: Artifact to generate a signed URL for + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactMultipartUploadURL' + description: The URL has been generated + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The project doesn't exist + summary: It generates a signed URL for uploading a part. + tags: + - Analytics + /api/runs/{run_id}/start: + post: + callbacks: {} + description: The endpoint returns an upload ID that can be used to generate URLs for the individual parts and complete the upload. + operationId: startAnalyticsArtifactMultipartUpload + parameters: + - description: The id of the command event. + in: path + name: run_id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CommandEventArtifact' + description: Artifact to upload + required: false + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactUploadID' + description: The upload has been started + 401: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: You need to be authenticated to access this resource + 403: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The authenticated subject is not authorized to perform this action + 404: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The command event doesn't exist + summary: It initiates a multipart upload for a command event artifact. + tags: + - Analytics +security: + - authorization: [] + - cookie: [] +servers: + - url: http://localhost:8080 + variables: {} +tags: [] diff --git a/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift b/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift new file mode 100644 index 00000000000..f2da1e6826c --- /dev/null +++ b/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift @@ -0,0 +1,183 @@ +import Foundation +import Mockable +import Path +import TuistSupport +import XcodeGraph + +@Mockable +public protocol AnalyticsArtifactUploadServicing { + func uploadResultBundle( + _ resultBundle: AbsolutePath, + targetHashes: [GraphTarget: String], + graphPath: AbsolutePath, + commandEventId: Int, + serverURL: URL + ) async throws +} + +public final class AnalyticsArtifactUploadService: AnalyticsArtifactUploadServicing { + private let fileHandler: FileHandling + private let xcresultToolController: XCResultToolControlling + private let fileArchiver: FileArchivingFactorying + private let retryProvider: RetryProviding + private let multipartUploadStartAnalyticsService: MultipartUploadStartAnalyticsServicing + private let multipartUploadGenerateURLAnalyticsService: MultipartUploadGenerateURLAnalyticsServicing + private let multipartUploadArtifactService: MultipartUploadArtifactServicing + private let multipartUploadCompleteAnalyticsService: MultipartUploadCompleteAnalyticsServicing + private let completeAnalyticsArtifactsUploadsService: CompleteAnalyticsArtifactsUploadsServicing + + public convenience init() { + self.init( + fileHandler: FileHandler.shared, + xcresultToolController: XCResultToolController(), + fileArchiver: FileArchivingFactory(), + retryProvider: RetryProvider(), + multipartUploadStartAnalyticsService: MultipartUploadStartAnalyticsService(), + multipartUploadGenerateURLAnalyticsService: + MultipartUploadGenerateURLAnalyticsService(), + multipartUploadArtifactService: MultipartUploadArtifactService(), + multipartUploadCompleteAnalyticsService: + MultipartUploadCompleteAnalyticsService(), + completeAnalyticsArtifactsUploadsService: CompleteAnalyticsArtifactsUploadsService() + ) + } + + init( + fileHandler: FileHandling, + xcresultToolController: XCResultToolControlling, + fileArchiver: FileArchivingFactorying, + retryProvider: RetryProviding, + multipartUploadStartAnalyticsService: MultipartUploadStartAnalyticsServicing, + multipartUploadGenerateURLAnalyticsService: MultipartUploadGenerateURLAnalyticsServicing, + multipartUploadArtifactService: MultipartUploadArtifactServicing, + multipartUploadCompleteAnalyticsService: MultipartUploadCompleteAnalyticsServicing, + completeAnalyticsArtifactsUploadsService: CompleteAnalyticsArtifactsUploadsServicing + ) { + self.fileHandler = fileHandler + self.xcresultToolController = xcresultToolController + self.fileArchiver = fileArchiver + self.retryProvider = retryProvider + self.multipartUploadStartAnalyticsService = multipartUploadStartAnalyticsService + self.multipartUploadGenerateURLAnalyticsService = multipartUploadGenerateURLAnalyticsService + self.multipartUploadArtifactService = multipartUploadArtifactService + self.multipartUploadCompleteAnalyticsService = multipartUploadCompleteAnalyticsService + self.completeAnalyticsArtifactsUploadsService = completeAnalyticsArtifactsUploadsService + } + + public func uploadResultBundle( + _ resultBundle: AbsolutePath, + targetHashes: [GraphTarget: String], + graphPath: AbsolutePath, + commandEventId: Int, + serverURL: URL + ) async throws { + try await uploadAnalyticsArtifact( + ServerCommandEvent.Artifact( + type: .resultBundle + ), + artifactPath: resultBundle, + commandEventId: commandEventId, + serverURL: serverURL + ) + + let invocationRecordString = try xcresultToolController.resultBundleObject(resultBundle) + let invocationRecordPath = resultBundle.parentDirectory.appending(component: "invocation_record.json") + try fileHandler.write(invocationRecordString, path: invocationRecordPath, atomically: true) + + let decoder = JSONDecoder() + let invocationRecord = try decoder.decode(InvocationRecord.self, from: invocationRecordString.data(using: .utf8)!) + for testActionRecord in invocationRecord.actions._values + .filter({ $0.schemeCommandName._value == "Test" }) + { + guard let id = testActionRecord.actionResult.testsRef?.id._value else { continue } + let resultBundleObjectString = try xcresultToolController.resultBundleObject( + resultBundle, + id: id + ) + let filename = "\(id).json" + let resultBundleObjectPath = resultBundle.parentDirectory.appending(component: filename) + try fileHandler.write(resultBundleObjectString, path: resultBundleObjectPath, atomically: true) + try await uploadAnalyticsArtifact( + ServerCommandEvent.Artifact( + type: .resultBundleObject, + name: id + ), + artifactPath: resultBundleObjectPath, + commandEventId: commandEventId, + serverURL: serverURL + ) + } + + try await uploadAnalyticsArtifact( + ServerCommandEvent.Artifact( + type: .invocationRecord + ), + artifactPath: invocationRecordPath, + commandEventId: commandEventId, + serverURL: serverURL + ) + + let modules = targetHashes.map { key, value in + ServerModule( + hash: value, + projectRelativePath: + key.project.xcodeProjPath.relative(to: graphPath), + name: key.target.name + ) + } + + try await completeAnalyticsArtifactsUploadsService.completeAnalyticsArtifactsUploads( + modules: modules, + commandEventId: commandEventId, + serverURL: serverURL + ) + } + + private func uploadAnalyticsArtifact( + _ artifact: ServerCommandEvent.Artifact, + artifactPath: AbsolutePath, + name: String? = nil, + commandEventId: Int, + serverURL: URL + ) async throws { + let passedArtifactPath = artifactPath + let artifactPath: AbsolutePath + + switch artifact.type { + case .resultBundle: + artifactPath = try fileArchiver.makeFileArchiver(for: [passedArtifactPath]) + .zip(name: passedArtifactPath.basenameWithoutExt) + case .invocationRecord, .resultBundleObject: + artifactPath = passedArtifactPath + } + + try await retryProvider.runWithRetries { [self] in + let uploadId = try await multipartUploadStartAnalyticsService.uploadAnalyticsArtifact( + artifact, + commandEventId: commandEventId, + serverURL: serverURL + ) + + let parts = try await multipartUploadArtifactService.multipartUploadArtifact( + artifactPath: artifactPath, + generateUploadURL: { partNumber in + try await self.multipartUploadGenerateURLAnalyticsService.uploadAnalytics( + artifact, + commandEventId: commandEventId, + partNumber: partNumber, + uploadId: uploadId, + serverURL: serverURL + ) + } + ) + + try await multipartUploadCompleteAnalyticsService.uploadAnalyticsArtifact( + artifact, + commandEventId: commandEventId, + uploadId: uploadId, + parts: parts, + serverURL: serverURL + ) + } + } +} diff --git a/Sources/TuistServer/Services/AuthenticateService.swift b/Sources/TuistServer/Services/AuthenticateService.swift new file mode 100644 index 00000000000..f85c90b027d --- /dev/null +++ b/Sources/TuistServer/Services/AuthenticateService.swift @@ -0,0 +1,76 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol AuthenticateServicing { + func authenticate( + email: String, + password: String, + serverURL: URL + ) async throws -> ServerAuthenticationTokens +} + +enum AuthenticateServiceError: FatalError { + case unknownError(Int) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We failed to authenticate you due to an unknown Tuist response of \(statusCode)." + case let .unauthorized(message): + return message + } + } +} + +public final class AuthenticateService: AuthenticateServicing { + public init() {} + + public func authenticate( + email: String, + password: String, + serverURL: URL + ) async throws -> ServerAuthenticationTokens { + let client = Client.unauthenticated(serverURL: serverURL) + + let response = try await client.authenticate( + .init( + body: .json( + .init( + email: email, + password: password + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(authenticationTokens): + return ServerAuthenticationTokens( + accessToken: authenticationTokens.access_token, + refreshToken: authenticationTokens.refresh_token + ) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw AuthenticateServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw AuthenticateServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/CacheExistsService.swift b/Sources/TuistServer/Services/CacheExistsService.swift new file mode 100644 index 00000000000..cc8dc114264 --- /dev/null +++ b/Sources/TuistServer/Services/CacheExistsService.swift @@ -0,0 +1,87 @@ +import Foundation +import Mockable +import TuistCore +import TuistSupport + +@Mockable +public protocol CacheExistsServicing { + func cacheExists( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory + ) async throws +} + +public enum CacheExistsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case paymentRequired(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .paymentRequired, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The remote cache could not be used due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .paymentRequired(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class CacheExistsService: CacheExistsServicing { + public init() {} + + public func cacheExists( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.cacheArtifactExists( + .init(query: .init(cache_category: .init(cacheCategory), project_id: projectId, hash: hash, name: name)) + ) + + switch response { + case .ok: + // noop + break + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(body): + throw CacheExistsServiceError.notFound(body.error?.first?.message ?? "The remote cache artifact does not exist") + } + case let .paymentRequired(paymentRequiredResponse): + switch paymentRequiredResponse.body { + case let .json(error): + throw CacheExistsServiceError.paymentRequired(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CacheExistsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw CacheExistsServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/CancelOrganizationInviteService.swift b/Sources/TuistServer/Services/CancelOrganizationInviteService.swift new file mode 100644 index 00000000000..a26d73cb6b1 --- /dev/null +++ b/Sources/TuistServer/Services/CancelOrganizationInviteService.swift @@ -0,0 +1,77 @@ +import Foundation +import OpenAPIURLSession +import TuistSupport + +public protocol CancelOrganizationInviteServicing { + func cancelOrganizationInvite( + organizationName: String, + email: String, + serverURL: URL + ) async throws +} + +enum CancelOrganizationInviteServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The invitation could not be cancelled due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class CancelOrganizationInviteService: CancelOrganizationInviteServicing { + public init() {} + + public func cancelOrganizationInvite( + organizationName: String, + email: String, + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.cancelInvitation( + .init( + path: .init(organization_name: organizationName), + body: .json(.init(invitee_email: email)) + ) + ) + switch response { + case .noContent: + // noop + break + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw CancelOrganizationInviteServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw CancelOrganizationInviteServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CancelOrganizationInviteServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/CleanCacheService.swift b/Sources/TuistServer/Services/CleanCacheService.swift new file mode 100644 index 00000000000..00df862badc --- /dev/null +++ b/Sources/TuistServer/Services/CleanCacheService.swift @@ -0,0 +1,92 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol CleanCacheServicing { + func cleanCache( + serverURL: URL, + fullHandle: String + ) async throws +} + +enum CleanCacheServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The project clean failed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class CleanCacheService: CleanCacheServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func cleanCache( + serverURL: URL, + fullHandle: String + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + + let response = try await client.cleanCache( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ) + ) + ) + + switch response { + case .noContent: + // noop + break + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw CleanCacheServiceError.notFound(error.message) + } + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw CleanCacheServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CleanCacheServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/CompleteAnalyticsArtifactsUploads.swift b/Sources/TuistServer/Services/CompleteAnalyticsArtifactsUploads.swift new file mode 100644 index 00000000000..0f3c30db5c3 --- /dev/null +++ b/Sources/TuistServer/Services/CompleteAnalyticsArtifactsUploads.swift @@ -0,0 +1,79 @@ +import Foundation +import Mockable +import OpenAPIRuntime +import TuistSupport + +@Mockable +public protocol CompleteAnalyticsArtifactsUploadsServicing { + func completeAnalyticsArtifactsUploads( + modules: [ServerModule], + commandEventId: Int, + serverURL: URL + ) async throws +} + +public enum CompleteAnalyticsArtifactsUploadsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The analytics artifacts uploads could not get completed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class CompleteAnalyticsArtifactsUploadsService: CompleteAnalyticsArtifactsUploadsServicing { + public init() {} + + public func completeAnalyticsArtifactsUploads( + modules: [ServerModule], + commandEventId: Int, + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.completeAnalyticsArtifactsUploads( + .init( + path: .init(run_id: commandEventId), + body: .json( + .init(modules: .init(modules.map(Components.Schemas.Module.init))) + ) + ) + ) + switch response { + case .noContent: + return + case let .undocumented(statusCode: statusCode, _): + throw CompleteAnalyticsArtifactsUploadsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw CompleteAnalyticsArtifactsUploadsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw CompleteAnalyticsArtifactsUploadsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/CreateCommandEventService.swift b/Sources/TuistServer/Services/CreateCommandEventService.swift new file mode 100644 index 00000000000..f644b51364b --- /dev/null +++ b/Sources/TuistServer/Services/CreateCommandEventService.swift @@ -0,0 +1,110 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistCore +import TuistSupport + +@Mockable +public protocol CreateCommandEventServicing { + func createCommandEvent( + commandEvent: CommandEvent, + projectId: String, + serverURL: URL + ) async throws -> ServerCommandEvent +} + +enum CreateCommandEventServiceError: FatalError { + case unknownError(Int) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The organization could not be created due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class CreateCommandEventService: CreateCommandEventServicing { + public init() {} + + public func createCommandEvent( + commandEvent: CommandEvent, + projectId: String, + serverURL: URL + ) async throws -> ServerCommandEvent { + let client = Client.authenticated(serverURL: serverURL) + let errorMessage: String? + let status: Operations.createCommandEvent.Input.Body.jsonPayload.statusPayload? + switch commandEvent.status { + case .success: + errorMessage = nil + status = .success + case let .failure(message): + errorMessage = message + status = .failure + } + + let response = try await client.createCommandEvent( + .init( + query: .init( + project_id: projectId + ), + body: .json( + .init( + client_id: commandEvent.clientId, + command_arguments: commandEvent.commandArguments, + duration: Double(commandEvent.durationInMs), + error_message: errorMessage, + is_ci: commandEvent.isCI, + macos_version: commandEvent.macOSVersion, + name: commandEvent.name, + params: .init( + cacheable_targets: commandEvent.params["cacheable_targets"]?.value as? [String], + local_cache_target_hits: commandEvent.params["local_cache_target_hits"]?.value as? [String], + local_test_target_hits: commandEvent.params["local_test_target_hits"]?.value as? [String], + remote_cache_target_hits: commandEvent.params["remote_cache_target_hits"]?.value as? [String], + remote_test_target_hits: commandEvent.params["remote_test_target_hits"]?.value as? [String], + test_targets: commandEvent.params["test_targets"]?.value as? [String] + ), + status: status, + subcommand: commandEvent.subcommand, + swift_version: commandEvent.swiftVersion, + tuist_version: commandEvent.tuistVersion + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(commandEvent): + return ServerCommandEvent(commandEvent) + } + case let .undocumented(statusCode: statusCode, _): + throw CreateCommandEventServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw CreateCommandEventServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/CreateOrganizationInviteService.swift b/Sources/TuistServer/Services/CreateOrganizationInviteService.swift new file mode 100644 index 00000000000..6a71dfae764 --- /dev/null +++ b/Sources/TuistServer/Services/CreateOrganizationInviteService.swift @@ -0,0 +1,87 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol CreateOrganizationInviteServicing { + func createOrganizationInvite( + organizationName: String, + email: String, + serverURL: URL + ) async throws -> ServerInvitation +} + +enum CreateOrganizationInviteServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case badRequest(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .badRequest, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The user could not be invited due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .badRequest(message), let .unauthorized(message): + return message + } + } +} + +public final class CreateOrganizationInviteService: CreateOrganizationInviteServicing { + public init() {} + + public func createOrganizationInvite( + organizationName: String, + email: String, + serverURL: URL + ) async throws -> ServerInvitation { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.createInvitation( + .init( + path: .init(organization_name: organizationName), + body: .json(.init(invitee_email: email)) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(invitation): + return ServerInvitation(invitation) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw CreateOrganizationInviteServiceError.notFound(error.message) + } + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw CreateOrganizationInviteServiceError.forbidden(error.message) + } + case let .badRequest(badRequestResponse): + switch badRequestResponse.body { + case let .json(error): + throw CreateOrganizationInviteServiceError.badRequest(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CreateOrganizationInviteServiceError.unknownError(statusCode) + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/CreateOrganizationService.swift b/Sources/TuistServer/Services/CreateOrganizationService.swift new file mode 100644 index 00000000000..25fee43068d --- /dev/null +++ b/Sources/TuistServer/Services/CreateOrganizationService.swift @@ -0,0 +1,70 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol CreateOrganizationServicing { + func createOrganization( + name: String, + serverURL: URL + ) async throws -> ServerOrganization +} + +enum CreateOrganizationServiceError: FatalError { + case unknownError(Int) + case badRequest(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .badRequest: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The organization could not be created due to an unknown Tuist response of \(statusCode)." + case let .badRequest(message): + return message + } + } +} + +public final class CreateOrganizationService: CreateOrganizationServicing { + public init() {} + + public func createOrganization( + name: String, + serverURL: URL + ) async throws -> ServerOrganization { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.createOrganization( + .init( + body: .json( + .init( + name: name + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(organization): + return ServerOrganization(organization) + } + case let .badRequest(badRequestResponse): + switch badRequestResponse.body { + case let .json(error): + throw CreateOrganizationServiceError.badRequest(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CreateOrganizationServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/CreateProjectService.swift b/Sources/TuistServer/Services/CreateProjectService.swift new file mode 100644 index 00000000000..b65e0236ca9 --- /dev/null +++ b/Sources/TuistServer/Services/CreateProjectService.swift @@ -0,0 +1,82 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol CreateProjectServicing { + func createProject( + fullHandle: String, + serverURL: URL + ) async throws -> ServerProject +} + +enum CreateProjectServiceError: FatalError { + case unknownError(Int) + case forbidden(String) + case badRequest(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .badRequest, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The project could not be created due to an unknown Cloud response of \(statusCode)." + case let .forbidden(message), let .badRequest(message), let .unauthorized(message): + return message + } + } +} + +public final class CreateProjectService: CreateProjectServicing { + public init() {} + + public func createProject( + fullHandle: String, + serverURL: URL + ) async throws -> ServerProject { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.createProject( + .init( + body: .json( + .init( + full_handle: fullHandle + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(project): + return ServerProject(project) + } + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw CreateProjectServiceError.forbidden(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CreateProjectServiceError.unknownError(statusCode) + case let .badRequest(badRequestResponse): + switch badRequestResponse.body { + case let .json(error): + throw CreateProjectServiceError.badRequest(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/CreateProjectTokenService.swift b/Sources/TuistServer/Services/CreateProjectTokenService.swift new file mode 100644 index 00000000000..354ae7d84c2 --- /dev/null +++ b/Sources/TuistServer/Services/CreateProjectTokenService.swift @@ -0,0 +1,94 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol CreateProjectTokenServicing { + func createProjectToken( + fullHandle: String, + serverURL: URL + ) async throws -> String +} + +enum CreateProjectTokenServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not create a new project token due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message): + return message + } + } +} + +public final class CreateProjectTokenService: CreateProjectTokenServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func createProjectToken( + fullHandle: String, + serverURL: URL + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + + let response = try await client.createProjectToken( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(projectToken): + return projectToken.token + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw CreateProjectTokenServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw CreateProjectTokenServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw CreateProjectTokenServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw CreateProjectTokenServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/DeleteOrganizationService.swift b/Sources/TuistServer/Services/DeleteOrganizationService.swift new file mode 100644 index 00000000000..d771884dcfe --- /dev/null +++ b/Sources/TuistServer/Services/DeleteOrganizationService.swift @@ -0,0 +1,76 @@ +import Foundation +import OpenAPIURLSession +import TuistSupport + +public protocol DeleteOrganizationServicing { + func deleteOrganization( + name: String, + serverURL: URL + ) async throws +} + +enum DeleteOrganizationServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .unauthorized, .notFound: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The organization could not be deleted due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .unauthorized(message), let .notFound(message): + return message + } + } +} + +public final class DeleteOrganizationService: DeleteOrganizationServicing { + public init() {} + + public func deleteOrganization( + name: String, + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.deleteOrganization( + .init( + path: .init( + organization_name: name + ) + ) + ) + switch response { + case .noContent: + // noop + break + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw DeleteOrganizationServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw DeleteOrganizationServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw DeleteOrganizationServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/DeleteProjectService.swift b/Sources/TuistServer/Services/DeleteProjectService.swift new file mode 100644 index 00000000000..4f8cb619424 --- /dev/null +++ b/Sources/TuistServer/Services/DeleteProjectService.swift @@ -0,0 +1,76 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol DeleteProjectServicing { + func deleteProject( + projectId: Int, + serverURL: URL + ) async throws +} + +enum DeleteProjectServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The project could not be deleted due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .unauthorized(message), let .notFound(message): + return message + } + } +} + +public final class DeleteProjectService: DeleteProjectServicing { + public init() {} + + public func deleteProject( + projectId: Int, + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.deleteProject( + .init( + path: .init(id: projectId) + ) + ) + switch response { + case .noContent: + // noop + break + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw DeleteProjectServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw DeleteProjectServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw DeleteProjectServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/DownloadPreviewService.swift b/Sources/TuistServer/Services/DownloadPreviewService.swift new file mode 100644 index 00000000000..1a83b24c805 --- /dev/null +++ b/Sources/TuistServer/Services/DownloadPreviewService.swift @@ -0,0 +1,95 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol DownloadPreviewServicing { + func downloadPreview( + _ previewId: String, + fullHandle: String, + serverURL: URL + ) async throws -> String +} + +public enum DownloadPreviewServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The build could not be uploaded due to an unknown Tuist Cloud response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class DownloadPreviewService: DownloadPreviewServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func downloadPreview( + _ previewId: String, + fullHandle: String, + serverURL: URL + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + let response = try await client.downloadPreview( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle, + preview_id: previewId + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(build): + return build.url + } + case let .undocumented(statusCode: statusCode, _): + throw DownloadPreviewServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw DownloadPreviewServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw DownloadPreviewServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DownloadPreviewServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/FullHandleService.swift b/Sources/TuistServer/Services/FullHandleService.swift new file mode 100644 index 00000000000..fd99afe2f5a --- /dev/null +++ b/Sources/TuistServer/Services/FullHandleService.swift @@ -0,0 +1,38 @@ +import Foundation +import TuistSupport + +enum FullHandleServiceError: FatalError, Equatable { + case invalidHandle(String) + var type: ErrorType { + switch self { + case .invalidHandle: + return .abort + } + } + + var description: String { + switch self { + case let .invalidHandle(fullHandle): + return "The project full handle \(fullHandle) is not in the format of account-handle/project-handle." + } + } +} + +protocol FullHandleServicing { + func parse(_ fullHandle: String) throws -> (accountHandle: String, projectHandle: String) +} + +final class FullHandleService: FullHandleServicing { + func parse(_ fullHandle: String) throws -> (accountHandle: String, projectHandle: String) { + let components = fullHandle.components(separatedBy: "/") + guard components.count == 2 + else { + throw FullHandleServiceError.invalidHandle(fullHandle) + } + + let accountHandle = components[0] + let projectHandle = components[1] + + return (accountHandle: accountHandle, projectHandle: projectHandle) + } +} diff --git a/Sources/TuistServer/Services/GetAuthTokenService.swift b/Sources/TuistServer/Services/GetAuthTokenService.swift new file mode 100644 index 00000000000..e563102d187 --- /dev/null +++ b/Sources/TuistServer/Services/GetAuthTokenService.swift @@ -0,0 +1,73 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol GetAuthTokenServicing { + func getAuthToken( + serverURL: URL, + deviceCode: String + ) async throws -> ServerAuthenticationTokens? +} + +public enum GetAuthTokenServiceError: FatalError, Equatable { + case unknownError(Int) + case badRequest(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .badRequest: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The CLI authentication failed due to an unknown Tuist response of \(statusCode)." + case let .badRequest(message): + return message + } + } +} + +public final class GetAuthTokenService: GetAuthTokenServicing { + public init() {} + + public func getAuthToken( + serverURL: URL, + deviceCode: String + ) async throws -> ServerAuthenticationTokens? { + let client = Client.unauthenticated(serverURL: serverURL) + + let response = try await client.getDeviceCode( + .init(path: .init(device_code: deviceCode)) + ) + + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(token): + guard let refreshToken = token.refresh_token, + let accessToken = token.access_token + else { return nil } + return ServerAuthenticationTokens( + accessToken: accessToken, + refreshToken: refreshToken + ) + } + case .accepted: + return nil + case let .badRequest(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw GetAuthTokenServiceError.badRequest(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw GetAuthTokenServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/GetCacheService.swift b/Sources/TuistServer/Services/GetCacheService.swift new file mode 100644 index 00000000000..0eab6a40bc1 --- /dev/null +++ b/Sources/TuistServer/Services/GetCacheService.swift @@ -0,0 +1,89 @@ +import Foundation +import Mockable +import TuistCore +import TuistSupport + +@Mockable +public protocol GetCacheServicing { + func getCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory + ) async throws -> ServerCacheArtifact +} + +public enum GetCacheServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case paymentRequired(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .paymentRequired, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The remote cache could not be used due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .paymentRequired(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class GetCacheService: GetCacheServicing { + public init() {} + + public func getCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory + ) async throws -> ServerCacheArtifact { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.downloadCacheArtifact( + .init(query: .init(cache_category: .init(cacheCategory), project_id: projectId, hash: hash, name: name)) + ) + + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(cacheArtifact): + return try ServerCacheArtifact(cacheArtifact) + } + case let .paymentRequired(paymentRequiredResponse): + switch paymentRequiredResponse.body { + case let .json(error): + throw GetCacheServiceError.paymentRequired(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw GetCacheServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw GetCacheServiceError.forbidden(error.message) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw GetCacheServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/GetOrganizationService.swift b/Sources/TuistServer/Services/GetOrganizationService.swift new file mode 100644 index 00000000000..407fb674647 --- /dev/null +++ b/Sources/TuistServer/Services/GetOrganizationService.swift @@ -0,0 +1,80 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol GetOrganizationServicing { + func getOrganization( + organizationName: String, + serverURL: URL + ) async throws -> ServerOrganization +} + +enum GetOrganizationServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not get the organization due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message): + return message + } + } +} + +public final class GetOrganizationService: GetOrganizationServicing { + public init() {} + + public func getOrganization( + organizationName: String, + serverURL: URL + ) async throws -> ServerOrganization { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.showOrganization( + .init( + path: .init( + organization_name: organizationName + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(project): + return ServerOrganization(project) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw GetOrganizationServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw GetOrganizationServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw GetOrganizationServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/GetOrganizationUsage.swift b/Sources/TuistServer/Services/GetOrganizationUsage.swift new file mode 100644 index 00000000000..f4999c93688 --- /dev/null +++ b/Sources/TuistServer/Services/GetOrganizationUsage.swift @@ -0,0 +1,80 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol GetOrganizationUsageServicing { + func getOrganizationUsage( + organizationName: String, + serverURL: URL + ) async throws -> ServerOrganizationUsage +} + +enum GetOrganizationUsageServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not get the OrganizationUsage due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message): + return message + } + } +} + +public final class GetOrganizationUsageService: GetOrganizationUsageServicing { + public init() {} + + public func getOrganizationUsage( + organizationName: String, + serverURL: URL + ) async throws -> ServerOrganizationUsage { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.showOrganizationUsage( + .init( + path: .init( + organization_name: organizationName + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(organizationUsage): + return ServerOrganizationUsage(organizationUsage) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw GetOrganizationUsageServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw GetOrganizationUsageServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw GetOrganizationUsageServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/GetProjectService.swift b/Sources/TuistServer/Services/GetProjectService.swift new file mode 100644 index 00000000000..e8ed66c4419 --- /dev/null +++ b/Sources/TuistServer/Services/GetProjectService.swift @@ -0,0 +1,94 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol GetProjectServicing { + func getProject( + fullHandle: String, + serverURL: URL + ) async throws -> ServerProject +} + +enum GetProjectServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not get the project due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message): + return message + } + } +} + +public final class GetProjectService: GetProjectServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func getProject( + fullHandle: String, + serverURL: URL + ) async throws -> ServerProject { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + + let response = try await client.showProject( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(project): + return ServerProject(project) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw GetProjectServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw GetProjectServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw GetProjectServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/ListOrganizationsService.swift b/Sources/TuistServer/Services/ListOrganizationsService.swift new file mode 100644 index 00000000000..f47a9a0a12d --- /dev/null +++ b/Sources/TuistServer/Services/ListOrganizationsService.swift @@ -0,0 +1,70 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol ListOrganizationsServicing { + func listOrganizations( + serverURL: URL + ) async throws -> [String] +} + +enum ListOrganizationsServiceError: FatalError { + case unknownError(Int) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The organizations could not be listed due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class ListOrganizationsService: ListOrganizationsServicing { + public init() {} + + public func listOrganizations( + serverURL: URL + ) async throws -> [String] { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.listOrganizations( + .init( + query: .init() + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(data): + return data.organizations.map(\.name) + } + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw ListOrganizationsServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw ListOrganizationsServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/ListProjectTokensService.swift b/Sources/TuistServer/Services/ListProjectTokensService.swift new file mode 100644 index 00000000000..e7a88847b70 --- /dev/null +++ b/Sources/TuistServer/Services/ListProjectTokensService.swift @@ -0,0 +1,94 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol ListProjectTokensServicing { + func listProjectTokens( + fullHandle: String, + serverURL: URL + ) async throws -> [ServerProjectToken] +} + +enum ListProjectTokensServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not list the project tokens due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message): + return message + } + } +} + +public final class ListProjectTokensService: ListProjectTokensServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func listProjectTokens( + fullHandle: String, + serverURL: URL + ) async throws -> [ServerProjectToken] { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + + let response = try await client.listProjectTokens( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(response): + return response.tokens.map(ServerProjectToken.init) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw ListProjectTokensServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw ListProjectTokensServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw ListProjectTokensServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw ListProjectTokensServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/ListProjectsService.swift b/Sources/TuistServer/Services/ListProjectsService.swift new file mode 100644 index 00000000000..1c12d7d98a9 --- /dev/null +++ b/Sources/TuistServer/Services/ListProjectsService.swift @@ -0,0 +1,80 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol ListProjectsServicing { + func listProjects( + serverURL: URL + ) async throws -> [ServerProject] + + func listProjects( + serverURL: URL, + accountName: String?, + projectName: String? + ) async throws -> [ServerProject] +} + +enum ListProjectsServiceError: FatalError { + case unknownError(Int) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The project could not be listed due to an unknown Tuist response of \(statusCode)." + case let .unauthorized(message): + return message + } + } +} + +public final class ListProjectsService: ListProjectsServicing { + public init() {} + + public func listProjects( + serverURL: URL + ) async throws -> [ServerProject] { + try await listProjects( + serverURL: serverURL, + accountName: nil, + projectName: nil + ) + } + + public func listProjects( + serverURL: URL, + accountName _: String?, + projectName _: String? + ) async throws -> [ServerProject] { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.listProjects( + .init() + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(json): + return json.projects.map(ServerProject.init) + } + case let .undocumented(statusCode: statusCode, _): + throw ListProjectsServiceError.unknownError(statusCode) + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadArtifactService.swift b/Sources/TuistServer/Services/MultipartUploadArtifactService.swift new file mode 100644 index 00000000000..7f9551ba081 --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadArtifactService.swift @@ -0,0 +1,112 @@ +import Foundation +import Mockable +import Path +import TuistSupport + +enum MultipartUploadArtifactServiceError: FatalError { + case cannotCreateInputStream(AbsolutePath) + case noURLResponse(URL?) + case missingEtag(URL?) + case invalidMultipartUploadURL(String) + + var type: ErrorType { + switch self { + case .cannotCreateInputStream, .noURLResponse, .missingEtag, .invalidMultipartUploadURL: return .abort + } + } + + var description: String { + switch self { + case let .cannotCreateInputStream(path): + return "Couldn't create the file stream to multi-part upload file at path \(path.pathString)" + case let .noURLResponse(url): + if let url { + return "The response from request to URL \(url.absoluteString) doesnt' have the expected type HTTPURLResponse" + } else { + return "Received a response that doesn't have the expected type HTTPURLResponse" + } + case let .missingEtag(url): + if let url { + return "The response from request to URL \(url.absoluteString) lacks the etag HTTP header" + } else { + return "Received a response lacking the etag HTTP header" + } + case let .invalidMultipartUploadURL(url): + return "Received an invalid URL for a multi-part upload: \(url)" + } + } +} + +@Mockable +public protocol MultipartUploadArtifactServicing { + func multipartUploadArtifact( + artifactPath: AbsolutePath, + generateUploadURL: @escaping (Int) async throws -> String + ) async throws -> [(etag: String, partNumber: Int)] +} + +public final class MultipartUploadArtifactService: MultipartUploadArtifactServicing { + private let urlSession: URLSession + + public init(urlSession: URLSession = .tuistShared) { + self.urlSession = urlSession + } + + public func multipartUploadArtifact( + artifactPath: AbsolutePath, + generateUploadURL: (Int) async throws -> String + ) async throws -> [(etag: String, partNumber: Int)] { + let partSize = 10 * 1024 * 1024 + guard let inputStream = InputStream(url: artifactPath.url) else { + throw MultipartUploadArtifactServiceError.cannotCreateInputStream(artifactPath) + } + + inputStream.open() + + var partNumber = 1 + var buffer = [UInt8](repeating: 0, count: partSize) + var parts: [(etag: String, partNumber: Int)] = [] + + while inputStream.hasBytesAvailable { + let bytesRead = inputStream.read(&buffer, maxLength: partSize) + + if bytesRead > 0 { + let partData = Data(bytes: buffer, count: bytesRead) + let uploadURLString = try await generateUploadURL(partNumber) + guard let url = URL(string: uploadURLString) else { + throw MultipartUploadArtifactServiceError.invalidMultipartUploadURL(uploadURLString) + } + let request = uploadRequest(url: url, fileSize: UInt64(bytesRead), data: partData) + let etag = try await upload(for: request) + parts.append((etag: etag, partNumber: Int(partNumber))) + + partNumber += 1 + } + } + + inputStream.close() + + return parts + } + + private func upload(for request: URLRequest) async throws -> String { + let (_, response) = try await urlSession.data(for: request) + guard let urlResponse = response as? HTTPURLResponse else { + throw MultipartUploadArtifactServiceError.noURLResponse(request.url) + } + guard let etag = urlResponse.value(forHTTPHeaderField: "Etag") else { + throw MultipartUploadArtifactServiceError.missingEtag(request.url) + } + return etag.spm_chomp() + } + + private func uploadRequest(url: URL, fileSize: UInt64, data: Data) -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/zip", forHTTPHeaderField: "Content-Type") + request.setValue(String(fileSize), forHTTPHeaderField: "Content-Length") + request.setValue("zip", forHTTPHeaderField: "Content-Encoding") + request.httpBody = data + return request + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadCompleteAnalyticsService.swift b/Sources/TuistServer/Services/MultipartUploadCompleteAnalyticsService.swift new file mode 100644 index 00000000000..1a821dc046a --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadCompleteAnalyticsService.swift @@ -0,0 +1,90 @@ +import Foundation +import Mockable +import OpenAPIRuntime +import TuistSupport + +@Mockable +public protocol MultipartUploadCompleteAnalyticsServicing { + func uploadAnalyticsArtifact( + _ artifact: ServerCommandEvent.Artifact, + commandEventId: Int, + uploadId: String, + parts: [(etag: String, partNumber: Int)], + serverURL: URL + ) async throws +} + +public enum MultipartUploadCompleteAnalyticsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The multi-part upload could not get completed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadCompleteAnalyticsService: MultipartUploadCompleteAnalyticsServicing { + public init() {} + + public func uploadAnalyticsArtifact( + _ artifact: ServerCommandEvent.Artifact, + commandEventId: Int, + uploadId: String, + parts: [(etag: String, partNumber: Int)], + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.completeAnalyticsArtifactMultipartUpload( + .init( + path: .init(run_id: commandEventId), + body: .json( + .init( + command_event_artifact: .init(artifact), + multipart_upload_parts: .init( + parts: parts + .map { .init(etag: $0.etag, part_number: $0.partNumber) }, + upload_id: uploadId + ) + ) + ) + ) + ) + switch response { + case .noContent: + return + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadCompleteAnalyticsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadCompleteAnalyticsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadCompleteAnalyticsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadCompleteCacheService.swift b/Sources/TuistServer/Services/MultipartUploadCompleteCacheService.swift new file mode 100644 index 00000000000..d27f3938567 --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadCompleteCacheService.swift @@ -0,0 +1,96 @@ +import Foundation +import Mockable +import OpenAPIRuntime +import TuistCore +import TuistSupport + +@Mockable +public protocol MultipartUploadCompleteCacheServicing { + func uploadCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory, + uploadId: String, + parts: [(etag: String, partNumber: Int)] + ) async throws +} + +public enum MultipartUploadCompleteCacheServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case paymentRequired(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .paymentRequired, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The multi-part upload could not get completed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .paymentRequired(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadCompleteCacheService: MultipartUploadCompleteCacheServicing { + public init() {} + + public func uploadCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory, + uploadId: String, + parts: [(etag: String, partNumber: Int)] + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.completeCacheArtifactMultipartUpload(.init(query: .init( + cache_category: .init(cacheCategory), + project_id: projectId, + hash: hash, + upload_id: uploadId, + name: name + ), body: .json(.init(parts: parts.map { .init(etag: $0.etag, part_number: $0.partNumber) })))) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case .json: + return + } + case let .paymentRequired(paymentRequiredResponse): + switch paymentRequiredResponse.body { + case let .json(error): + throw MultipartUploadCompleteCacheServiceError.paymentRequired(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadCompleteCacheServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadCompleteCacheServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadCompleteCacheServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadCompletePreviewsService.swift b/Sources/TuistServer/Services/MultipartUploadCompletePreviewsService.swift new file mode 100644 index 00000000000..dc72d136657 --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadCompletePreviewsService.swift @@ -0,0 +1,117 @@ +import Foundation +import Mockable +import OpenAPIRuntime +import TuistSupport + +@Mockable +public protocol MultipartUploadCompletePreviewsServicing { + func completePreviewUpload( + _ previewId: String, + uploadId: String, + parts: [(etag: String, partNumber: Int)], + fullHandle: String, + serverURL: URL + ) async throws -> URL +} + +public enum MultipartUploadCompletePreviewsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + case invalidURL(String) + + public var type: ErrorType { + switch self { + case .unknownError, .invalidURL: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The multi-part upload could not get completed due to an unknown Tuist Cloud response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + case let .invalidURL(url): + return "The app build download URL \(url) returned from the server is invalid." + } + } +} + +public final class MultipartUploadCompletePreviewsService: MultipartUploadCompletePreviewsServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func completePreviewUpload( + _ previewId: String, + uploadId: String, + parts: [(etag: String, partNumber: Int)], + fullHandle: String, + serverURL: URL + ) async throws -> URL { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + let response = try await client.completePreviewsMultipartUpload( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ), + body: .json( + .init( + multipart_upload_parts: .init( + parts: parts + .map { .init(etag: $0.etag, part_number: $0.partNumber) }, + upload_id: uploadId + ), + preview_id: previewId + ) + ) + ) + ) + switch response { + case let .ok(previewUploadCompletionResponse): + switch previewUploadCompletionResponse.body { + case let .json(previewUploadCompletionResponse): + guard let url = URL(string: previewUploadCompletionResponse.url) + else { + throw MultipartUploadCompletePreviewsServiceError.invalidURL(previewUploadCompletionResponse.url) + } + + return url + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadCompletePreviewsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadCompletePreviewsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadCompletePreviewsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw MultipartUploadCompletePreviewsServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadGenerateURLAnalyticsService.swift b/Sources/TuistServer/Services/MultipartUploadGenerateURLAnalyticsService.swift new file mode 100644 index 00000000000..296a218e9b4 --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadGenerateURLAnalyticsService.swift @@ -0,0 +1,91 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol MultipartUploadGenerateURLAnalyticsServicing { + func uploadAnalytics( + _ artifact: ServerCommandEvent.Artifact, + commandEventId: Int, + partNumber: Int, + uploadId: String, + serverURL: URL + ) async throws -> String +} + +public enum MultipartUploadGenerateURLAnalyticsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The generation of a multi-part upload URL failed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadGenerateURLAnalyticsService: MultipartUploadGenerateURLAnalyticsServicing { + public init() {} + + public func uploadAnalytics( + _ artifact: ServerCommandEvent.Artifact, + commandEventId: Int, + partNumber: Int, + uploadId: String, + serverURL: URL + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.generateAnalyticsArtifactMultipartUploadURL( + .init( + path: .init(run_id: commandEventId), + body: .json( + .init( + command_event_artifact: .init(artifact), + multipart_upload_part: .init( + part_number: partNumber, + upload_id: uploadId + ) + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(cacheArtifact): + return cacheArtifact.data.url + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadGenerateURLAnalyticsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLAnalyticsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLAnalyticsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadGenerateURLCacheService.swift b/Sources/TuistServer/Services/MultipartUploadGenerateURLCacheService.swift new file mode 100644 index 00000000000..6c70a31f62d --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadGenerateURLCacheService.swift @@ -0,0 +1,96 @@ +import Foundation +import Mockable +import TuistCore +import TuistSupport + +@Mockable +public protocol MultipartUploadGenerateURLCacheServicing { + func uploadCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory, + uploadId: String, + partNumber: Int + ) async throws -> String +} + +public enum MultipartUploadGenerateURLCacheServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case paymentRequired(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .paymentRequired, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The generation of a multi-part upload URL failed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .paymentRequired(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadGenerateURLCacheService: MultipartUploadGenerateURLCacheServicing { + public init() {} + + public func uploadCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory, + uploadId: String, + partNumber: Int + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.generateCacheArtifactMultipartUploadURL(.init(query: .init( + cache_category: .init(cacheCategory), + project_id: projectId, + hash: hash, + part_number: partNumber, + upload_id: uploadId, + name: name + ))) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(cacheArtifact): + return cacheArtifact.data.url + } + case let .paymentRequired(paymentRequiredResponse): + switch paymentRequiredResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLCacheServiceError.paymentRequired(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadGenerateURLCacheServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLCacheServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLCacheServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadGenerateURLPreviewsService.swift b/Sources/TuistServer/Services/MultipartUploadGenerateURLPreviewsService.swift new file mode 100644 index 00000000000..c601ca636c0 --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadGenerateURLPreviewsService.swift @@ -0,0 +1,107 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol MultipartUploadGenerateURLPreviewsServicing { + func uploadPreviews( + _ previewId: String, + partNumber: Int, + uploadId: String, + fullHandle: String, + serverURL: URL + ) async throws -> String +} + +public enum MultipartUploadGenerateURLPreviewsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The generation of a multi-part upload URL failed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadGenerateURLPreviewsService: MultipartUploadGenerateURLPreviewsServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func uploadPreviews( + _ previewId: String, + partNumber: Int, + uploadId: String, + fullHandle: String, + serverURL: URL + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + let response = try await client.generatePreviewsMultipartUploadURL( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ), + body: .json( + .init( + multipart_upload_part: .init( + part_number: partNumber, + upload_id: uploadId + ), + preview_id: previewId + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(cacheArtifact): + return cacheArtifact.data.url + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadGenerateURLPreviewsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLPreviewsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadGenerateURLPreviewsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw MultipartUploadGenerateURLPreviewsServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadStartAnalyticsService.swift b/Sources/TuistServer/Services/MultipartUploadStartAnalyticsService.swift new file mode 100644 index 00000000000..e579b9d9af8 --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadStartAnalyticsService.swift @@ -0,0 +1,80 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol MultipartUploadStartAnalyticsServicing { + func uploadAnalyticsArtifact( + _ artifact: ServerCommandEvent.Artifact, + commandEventId: Int, + serverURL: URL + ) async throws -> String +} + +public enum MultipartUploadStartAnalyticsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case paymentRequired(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .paymentRequired, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The remote cache artifact could not be uploaded due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .paymentRequired(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadStartAnalyticsService: MultipartUploadStartAnalyticsServicing { + public init() {} + + public func uploadAnalyticsArtifact( + _ artifact: ServerCommandEvent.Artifact, + commandEventId: Int, + serverURL: URL + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.startAnalyticsArtifactMultipartUpload( + .init( + path: .init(run_id: commandEventId), + body: .json(.init(artifact)) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(cacheArtifact): + return cacheArtifact.data.upload_id + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadStartAnalyticsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadStartAnalyticsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadStartAnalyticsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadStartCacheService.swift b/Sources/TuistServer/Services/MultipartUploadStartCacheService.swift new file mode 100644 index 00000000000..c0b31b6a7dc --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadStartCacheService.swift @@ -0,0 +1,90 @@ +import Foundation +import Mockable +import TuistCore +import TuistSupport + +@Mockable +public protocol MultipartUploadStartCacheServicing { + func uploadCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory + ) async throws -> String +} + +public enum MultipartUploadStartCacheServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case paymentRequired(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .paymentRequired, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The remote cache artifact could not be uploaded due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .paymentRequired(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadStartCacheService: MultipartUploadStartCacheServicing { + public init() {} + + public func uploadCache( + serverURL: URL, + projectId: String, + hash: String, + name: String, + cacheCategory: RemoteCacheCategory + ) async throws -> String { + let client = Client.authenticated(serverURL: serverURL) + let response = try await client.startCacheArtifactMultipartUpload(.init(query: .init( + cache_category: .init(cacheCategory), + project_id: projectId, + hash: hash, + name: name + ))) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(cacheArtifact): + return cacheArtifact.data.upload_id + } + case let .paymentRequired(paymentRequiredResponse): + switch paymentRequiredResponse.body { + case let .json(error): + throw MultipartUploadStartCacheServiceError.paymentRequired(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadStartCacheServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadStartCacheServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadStartCacheServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/MultipartUploadStartPreviewsService.swift b/Sources/TuistServer/Services/MultipartUploadStartPreviewsService.swift new file mode 100644 index 00000000000..2c12c9d55af --- /dev/null +++ b/Sources/TuistServer/Services/MultipartUploadStartPreviewsService.swift @@ -0,0 +1,95 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol MultipartUploadStartPreviewsServicing { + func startPreviewsMultipartUpload( + fullHandle: String, + serverURL: URL + ) async throws -> PreviewUpload +} + +public enum MultipartUploadStartPreviewsServiceError: FatalError, Equatable { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The app build could not be uploaded due to an unknown Tuist Cloud response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message): + return message + } + } +} + +public final class MultipartUploadStartPreviewsService: MultipartUploadStartPreviewsServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func startPreviewsMultipartUpload( + fullHandle: String, + serverURL: URL + ) async throws -> PreviewUpload { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + let response = try await client.startPreviewsMultipartUpload( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(preview): + return PreviewUpload( + previewId: preview.data.preview_id, + uploadId: preview.data.upload_id + ) + } + case let .undocumented(statusCode: statusCode, _): + throw MultipartUploadStartPreviewsServiceError.unknownError(statusCode) + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw MultipartUploadStartPreviewsServiceError.forbidden(error.message) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw MultipartUploadStartPreviewsServiceError.notFound(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw MultipartUploadStartPreviewsServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/PreviewsUploadService.swift b/Sources/TuistServer/Services/PreviewsUploadService.swift new file mode 100644 index 00000000000..c2c3173cdde --- /dev/null +++ b/Sources/TuistServer/Services/PreviewsUploadService.swift @@ -0,0 +1,92 @@ +import Foundation +import Mockable +import Path +import TuistSupport +import XcodeGraph + +@Mockable +public protocol PreviewsUploadServicing { + func uploadPreviews( + _ previewPaths: [AbsolutePath], + fullHandle: String, + serverURL: URL + ) async throws -> URL +} + +public final class PreviewsUploadService: PreviewsUploadServicing { + private let fileHandler: FileHandling + private let fileArchiver: FileArchivingFactorying + private let retryProvider: RetryProviding + private let multipartUploadStartPreviewsService: MultipartUploadStartPreviewsServicing + private let multipartUploadGenerateURLPreviewsService: MultipartUploadGenerateURLPreviewsServicing + private let multipartUploadArtifactService: MultipartUploadArtifactServicing + private let multipartUploadCompletePreviewsService: MultipartUploadCompletePreviewsServicing + + public convenience init() { + self.init( + fileHandler: FileHandler.shared, + fileArchiver: FileArchivingFactory(), + retryProvider: RetryProvider(), + multipartUploadStartPreviewsService: MultipartUploadStartPreviewsService(), + multipartUploadGenerateURLPreviewsService: + MultipartUploadGenerateURLPreviewsService(), + multipartUploadArtifactService: MultipartUploadArtifactService(), + multipartUploadCompletePreviewsService: + MultipartUploadCompletePreviewsService() + ) + } + + init( + fileHandler: FileHandling, + fileArchiver: FileArchivingFactorying, + retryProvider: RetryProviding, + multipartUploadStartPreviewsService: MultipartUploadStartPreviewsServicing, + multipartUploadGenerateURLPreviewsService: MultipartUploadGenerateURLPreviewsServicing, + multipartUploadArtifactService: MultipartUploadArtifactServicing, + multipartUploadCompletePreviewsService: MultipartUploadCompletePreviewsServicing + ) { + self.fileHandler = fileHandler + self.fileArchiver = fileArchiver + self.retryProvider = retryProvider + self.multipartUploadStartPreviewsService = multipartUploadStartPreviewsService + self.multipartUploadGenerateURLPreviewsService = multipartUploadGenerateURLPreviewsService + self.multipartUploadArtifactService = multipartUploadArtifactService + self.multipartUploadCompletePreviewsService = multipartUploadCompletePreviewsService + } + + public func uploadPreviews( + _ previewPaths: [AbsolutePath], + fullHandle: String, + serverURL: URL + ) async throws -> URL { + let buildPath = try fileArchiver.makeFileArchiver(for: previewPaths).zip(name: "previews.zip") + + return try await retryProvider.runWithRetries { [self] in + let previewUpload = try await multipartUploadStartPreviewsService.startPreviewsMultipartUpload( + fullHandle: fullHandle, + serverURL: serverURL + ) + + let parts = try await multipartUploadArtifactService.multipartUploadArtifact( + artifactPath: buildPath, + generateUploadURL: { partNumber in + try await self.multipartUploadGenerateURLPreviewsService.uploadPreviews( + previewUpload.previewId, + partNumber: partNumber, + uploadId: previewUpload.uploadId, + fullHandle: fullHandle, + serverURL: serverURL + ) + } + ) + + return try await multipartUploadCompletePreviewsService.completePreviewUpload( + previewUpload.previewId, + uploadId: previewUpload.uploadId, + parts: parts, + fullHandle: fullHandle, + serverURL: serverURL + ) + } + } +} diff --git a/Sources/TuistServer/Services/RefreshAuthTokenService.swift b/Sources/TuistServer/Services/RefreshAuthTokenService.swift new file mode 100644 index 00000000000..e78b17c274b --- /dev/null +++ b/Sources/TuistServer/Services/RefreshAuthTokenService.swift @@ -0,0 +1,65 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol RefreshAuthTokenServicing: Sendable { + func refreshTokens( + serverURL: URL, + refreshToken: String + ) async throws -> ServerAuthenticationTokens +} + +public enum RefreshAuthTokenServiceError: FatalError, Equatable { + case unknownError(Int) + case unauthorized(String) + + public var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .unauthorized: + return .abort + } + } + + public var description: String { + switch self { + case let .unknownError(statusCode): + return "The CLI authentication failed due to an unknown Tuist response of \(statusCode)." + case let .unauthorized(message): + return message + } + } +} + +public final class RefreshAuthTokenService: RefreshAuthTokenServicing { + public init() {} + + public func refreshTokens( + serverURL: URL, + refreshToken: String + ) async throws -> ServerAuthenticationTokens { + let client = Client.unauthenticated(serverURL: serverURL) + + let response = try await client.refreshToken( + .init(body: .json(.init(refresh_token: refreshToken))) + ) + + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(tokens): + return ServerAuthenticationTokens(accessToken: tokens.access_token, refreshToken: tokens.refresh_token) + } + case let .unauthorized(unauthorizedResponse): + switch unauthorizedResponse.body { + case let .json(error): + throw RefreshAuthTokenServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw RefreshAuthTokenServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/RemoveOrganizationMemberService.swift b/Sources/TuistServer/Services/RemoveOrganizationMemberService.swift new file mode 100644 index 00000000000..74964f39d6a --- /dev/null +++ b/Sources/TuistServer/Services/RemoveOrganizationMemberService.swift @@ -0,0 +1,85 @@ +import Foundation +import OpenAPIURLSession +import TuistSupport + +public protocol RemoveOrganizationMemberServicing { + func removeOrganizationMember( + organizationName: String, + username: String, + serverURL: URL + ) async throws +} + +enum RemoveOrganizationMemberServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case badRequest(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .badRequest, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The member could not be removed due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .badRequest(message), let .unauthorized(message): + return message + } + } +} + +public final class RemoveOrganizationMemberService: RemoveOrganizationMemberServicing { + public init() {} + + public func removeOrganizationMember( + organizationName: String, + username: String, + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.removeOrganizationMember( + .init( + path: .init( + organization_name: organizationName, + user_name: username + ) + ) + ) + switch response { + case .noContent: + // noop + break + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw RemoveOrganizationMemberServiceError.notFound(error.message) + } + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw RemoveOrganizationMemberServiceError.forbidden(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw RemoveOrganizationMemberServiceError.unknownError(statusCode) + case let .badRequest(badRequestResponse): + switch badRequestResponse.body { + case let .json(error): + throw RemoveOrganizationMemberServiceError.badRequest(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/RevokeProjectTokenService.swift b/Sources/TuistServer/Services/RevokeProjectTokenService.swift new file mode 100644 index 00000000000..a24bda6da4d --- /dev/null +++ b/Sources/TuistServer/Services/RevokeProjectTokenService.swift @@ -0,0 +1,101 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol RevokeProjectTokenServicing { + func revokeProjectToken( + projectTokenId: String, + fullHandle: String, + serverURL: URL + ) async throws +} + +enum RevokeProjectTokenServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + case badRequest(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized, .badRequest: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not revoke the project token due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message), let .badRequest(message): + return message + } + } +} + +public final class RevokeProjectTokenService: RevokeProjectTokenServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func revokeProjectToken( + projectTokenId: String, + fullHandle: String, + serverURL: URL + ) async throws { + let client = Client.authenticated(serverURL: serverURL) + let handles = try fullHandleService.parse(fullHandle) + + let response = try await client.revokeProjectToken( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle, + id: projectTokenId + ) + ) + ) + switch response { + case .noContent: + // noop + break + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw RevokeProjectTokenServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw RevokeProjectTokenServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw RevokeProjectTokenServiceError.unauthorized(error.message) + } + case let .badRequest(badRequest): + switch badRequest.body { + case let .json(error): + throw RevokeProjectTokenServiceError.badRequest(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw RevokeProjectTokenServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/ServerURLService.swift b/Sources/TuistServer/Services/ServerURLService.swift new file mode 100644 index 00000000000..b8f2b13aa69 --- /dev/null +++ b/Sources/TuistServer/Services/ServerURLService.swift @@ -0,0 +1,42 @@ +import Foundation +import Mockable +import TuistSupport + +enum ServerURLServiceError: FatalError { + case invalidServerURL + + /// Error description. + var description: String { + switch self { + case .invalidServerURL: + return "The server URL is invalid." + } + } + + /// Error type. + var type: ErrorType { + switch self { + case .invalidServerURL: + return .bug + } + } +} + +@Mockable +public protocol ServerURLServicing { + func url(configServerURL: URL) throws -> URL +} + +public final class ServerURLService: ServerURLServicing { + public init() {} + + public func url(configServerURL: URL) throws -> URL { + guard let serverURL = ProcessInfo.processInfo.environment["TUIST_URL"] + .map(URL.init(string:)) ?? configServerURL + else { + throw ServerURLServiceError.invalidServerURL + } + + return serverURL + } +} diff --git a/Sources/TuistServer/Services/UpdateOrganizationMemberService.swift b/Sources/TuistServer/Services/UpdateOrganizationMemberService.swift new file mode 100644 index 00000000000..594c3683630 --- /dev/null +++ b/Sources/TuistServer/Services/UpdateOrganizationMemberService.swift @@ -0,0 +1,90 @@ +import Foundation +import OpenAPIURLSession +import TuistSupport + +public protocol UpdateOrganizationMemberServicing { + func updateOrganizationMember( + organizationName: String, + username: String, + role: ServerOrganization.Member.Role, + serverURL: URL + ) async throws -> ServerOrganization.Member +} + +enum UpdateOrganizationMemberServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case badRequest(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .notFound, .forbidden, .unauthorized, .badRequest: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "The member could not be updated due to an unknown Tuist response of \(statusCode)." + case let .notFound(message), let .forbidden(message), let .unauthorized(message), let .badRequest(message): + return message + } + } +} + +public final class UpdateOrganizationMemberService: UpdateOrganizationMemberServicing { + public init() {} + + public func updateOrganizationMember( + organizationName: String, + username: String, + role: ServerOrganization.Member.Role, + serverURL: URL + ) async throws -> ServerOrganization.Member { + let client = Client.authenticated(serverURL: serverURL) + + let response = try await client.updateOrganizationMember( + .init( + path: .init( + organization_name: organizationName, + user_name: username + ), + body: .json(.init(role: .init(rawValue: role.rawValue)!)) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(organizationMember): + return ServerOrganization.Member(organizationMember) + } + case let .notFound(notFoundResponse): + switch notFoundResponse.body { + case let .json(error): + throw UpdateOrganizationMemberServiceError.notFound(error.message) + } + case let .forbidden(forbiddenResponse): + switch forbiddenResponse.body { + case let .json(error): + throw UpdateOrganizationMemberServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw UpdateOrganizationMemberServiceError.unknownError(statusCode) + case let .badRequest(badRequestResponse): + switch badRequestResponse.body { + case let .json(error): + throw UpdateOrganizationMemberServiceError.badRequest(error.message) + } + } + } +} diff --git a/Sources/TuistServer/Services/UpdateOrganizationService.swift b/Sources/TuistServer/Services/UpdateOrganizationService.swift new file mode 100644 index 00000000000..59e9cc69641 --- /dev/null +++ b/Sources/TuistServer/Services/UpdateOrganizationService.swift @@ -0,0 +1,108 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol UpdateOrganizationServicing { + func updateOrganization( + organizationName: String, + serverURL: URL, + ssoOrganization: SSOOrganization? + ) async throws -> ServerOrganization +} + +enum UpdateOrganizationServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case badRequest(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .badRequest, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not update the organization due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .badRequest(message), let .unauthorized(message): + return message + } + } +} + +public final class UpdateOrganizationService: UpdateOrganizationServicing { + public init() {} + + public func updateOrganization( + organizationName: String, + serverURL: URL, + ssoOrganization: SSOOrganization? + ) async throws -> ServerOrganization { + let client = Client.authenticated(serverURL: serverURL) + let ssoProvider: Operations.updateOrganization + .Input.Body.jsonPayload.sso_providerPayload + let ssoOrganizationId: String? + + if let ssoOrganization { + switch ssoOrganization { + case let .google(organizationId): + ssoProvider = .google + ssoOrganizationId = organizationId + } + } else { + ssoOrganizationId = nil + ssoProvider = .none + } + + let response = try await client.updateOrganization( + .init( + path: .init( + organization_name: organizationName + ), + body: .json( + .init( + sso_organization_id: ssoOrganizationId, + sso_provider: ssoProvider + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(project): + return ServerOrganization(project) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw UpdateOrganizationServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw UpdateOrganizationServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw DeleteOrganizationServiceError.unauthorized(error.message) + } + case let .badRequest(badRequest): + switch badRequest.body { + case let .json(error): + throw UpdateOrganizationServiceError.forbidden(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw UpdateOrganizationServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Services/UpdateProjectService.swift b/Sources/TuistServer/Services/UpdateProjectService.swift new file mode 100644 index 00000000000..ec67d8f42d1 --- /dev/null +++ b/Sources/TuistServer/Services/UpdateProjectService.swift @@ -0,0 +1,102 @@ +import Foundation +import Mockable +import OpenAPIURLSession +import TuistSupport + +@Mockable +public protocol UpdateProjectServicing { + func updateProject( + fullHandle: String, + serverURL: URL, + defaultBranch: String? + ) async throws -> ServerProject +} + +enum UpdateProjectServiceError: FatalError { + case unknownError(Int) + case notFound(String) + case forbidden(String) + case unauthorized(String) + + var type: ErrorType { + switch self { + case .unknownError: + return .bug + case .forbidden, .notFound, .unauthorized: + return .abort + } + } + + var description: String { + switch self { + case let .unknownError(statusCode): + return "We could not update the project due to an unknown Tuist response of \(statusCode)." + case let .forbidden(message), let .notFound(message), let .unauthorized(message): + return message + } + } +} + +public final class UpdateProjectService: UpdateProjectServicing { + private let fullHandleService: FullHandleServicing + + public convenience init() { + self.init( + fullHandleService: FullHandleService() + ) + } + + init( + fullHandleService: FullHandleServicing + ) { + self.fullHandleService = fullHandleService + } + + public func updateProject( + fullHandle: String, + serverURL: URL, + defaultBranch: String? + ) async throws -> ServerProject { + let client = Client.authenticated(serverURL: serverURL) + + let handles = try fullHandleService.parse(fullHandle) + + let response = try await client.updateProject( + .init( + path: .init( + account_handle: handles.accountHandle, + project_handle: handles.projectHandle + ), + body: .json( + .init( + default_branch: defaultBranch + ) + ) + ) + ) + switch response { + case let .ok(okResponse): + switch okResponse.body { + case let .json(project): + return ServerProject(project) + } + case let .notFound(notFound): + switch notFound.body { + case let .json(error): + throw UpdateProjectServiceError.notFound(error.message) + } + case let .forbidden(forbidden): + switch forbidden.body { + case let .json(error): + throw UpdateProjectServiceError.forbidden(error.message) + } + case let .unauthorized(unauthorized): + switch unauthorized.body { + case let .json(error): + throw UpdateProjectServiceError.unauthorized(error.message) + } + case let .undocumented(statusCode: statusCode, _): + throw UpdateProjectServiceError.unknownError(statusCode) + } + } +} diff --git a/Sources/TuistServer/Session/ServerSessionController.swift b/Sources/TuistServer/Session/ServerSessionController.swift new file mode 100644 index 00000000000..ee151e9d134 --- /dev/null +++ b/Sources/TuistServer/Session/ServerSessionController.swift @@ -0,0 +1,128 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol ServerSessionControlling: AnyObject { + /// It authenticates the user for the server with the given URL. + /// - Parameter serverURL: Server URL. + func authenticate(serverURL: URL) async throws + + /// Prints the session for the server with the given URL. + /// - Parameter serverURL: Server URL. + func printSession(serverURL: URL) throws + + /// Removes the session for the server with the given URL. + /// - Parameter serverURL: Server URL. + func logout(serverURL: URL) async throws +} + +public final class ServerSessionController: ServerSessionControlling { + static let port: UInt16 = 4545 + + private let credentialsStore: ServerCredentialsStoring + private let ciChecker: CIChecking + private let opener: Opening + private let getAuthTokenService: GetAuthTokenServicing + private let uniqueIDGenerator: UniqueIDGenerating + private let serverAuthenticationController: ServerAuthenticationControlling + + public convenience init() { + let credentialsStore = ServerCredentialsStore() + self.init( + credentialsStore: credentialsStore, + ciChecker: CIChecker(), + opener: Opener(), + getAuthTokenService: GetAuthTokenService(), + uniqueIDGenerator: UniqueIDGenerator(), + serverAuthenticationController: ServerAuthenticationController(credentialsStore: credentialsStore) + ) + } + + init( + credentialsStore: ServerCredentialsStoring, + ciChecker: CIChecking, + opener: Opening, + getAuthTokenService: GetAuthTokenServicing, + uniqueIDGenerator: UniqueIDGenerating, + serverAuthenticationController: ServerAuthenticationControlling + ) { + self.credentialsStore = credentialsStore + self.ciChecker = ciChecker + self.opener = opener + self.getAuthTokenService = getAuthTokenService + self.uniqueIDGenerator = uniqueIDGenerator + self.serverAuthenticationController = serverAuthenticationController + } + + // MARK: - ServerSessionControlling + + public func authenticate(serverURL: URL) async throws { + var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)! + let deviceCode = uniqueIDGenerator.uniqueID() + components.path = "/auth/cli/\(deviceCode)" + components.queryItems = nil + let authURL = components.url! + + logger.notice("Opening \(authURL.absoluteString) to start the authentication flow") + try opener.open(url: authURL) + + if Environment.shared.shouldOutputBeColoured { + logger.notice("Press \("CTRL + C".cyan()) once to cancel the process.", metadata: .pretty) + } else { + logger.notice("Press CTRL + C once to cancel the process.") + } + + let tokens = try await getAuthTokens( + serverURL: serverURL, + deviceCode: deviceCode + ) + let credentials = ServerCredentials( + token: nil, + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken + ) + try credentialsStore.store(credentials: credentials, serverURL: serverURL) + logger.notice("Credentials stored successfully", metadata: .success) + } + + public func printSession(serverURL: URL) throws { + if let token = try serverAuthenticationController.authenticationToken(serverURL: serverURL) { + switch token { + case let .user(legacyToken: legacyToken, accessToken: accessToken, refreshToken: _): + logger.notice(""" + Requests against \(serverURL.absoluteString) will be authenticated as a user using the following token: + \(accessToken?.token ?? legacyToken!) + """) + case let .project(projectToken): + logger.notice(""" + Requests against \(serverURL.absoluteString) will be authenticated as a project using the following token: + \(projectToken) + """) + } + } else { + logger.notice("There are no sessions for the server with URL \(serverURL.absoluteString)") + } + } + + public func logout(serverURL: URL) async throws { + logger.notice("Removing session for server with URL \(serverURL.absoluteString)") + try await credentialsStore.delete(serverURL: serverURL) + logger.notice("Session deleted successfully", metadata: .success) + } + + private func getAuthTokens( + serverURL: URL, + deviceCode: String + ) async throws -> ServerAuthenticationTokens { + if let token = try await getAuthTokenService.getAuthToken( + serverURL: serverURL, + deviceCode: deviceCode + ) { + return token + } else { + try await Task.sleep(nanoseconds: 1_000_000_000) + return try await getAuthTokens(serverURL: serverURL, deviceCode: deviceCode) + } + } +} diff --git a/Sources/TuistServer/Utilities/DelayProvider.swift b/Sources/TuistServer/Utilities/DelayProvider.swift new file mode 100644 index 00000000000..2dd3bd8942d --- /dev/null +++ b/Sources/TuistServer/Utilities/DelayProvider.swift @@ -0,0 +1,18 @@ +import Foundation +import Mockable + +@Mockable +public protocol DelayProviding { + func delay(for retry: Int) -> UInt64 +} + +public struct DelayProvider: DelayProviding { + public init() {} + + public func delay(for retry: Int) -> UInt64 { + /// 0.1 seconds + let baseInterval = TimeInterval(1_000_000) + let randomInterval = Double.random(in: -1_000_000 ... 1_000_000) + return UInt64(baseInterval * pow(2, Double(retry)) + randomInterval) + } +} diff --git a/Sources/TuistServer/Utilities/JWT.swift b/Sources/TuistServer/Utilities/JWT.swift new file mode 100644 index 00000000000..bac8c0df84b --- /dev/null +++ b/Sources/TuistServer/Utilities/JWT.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct JWT: Equatable { + public let token: String + public let expiryDate: Date +} + +#if DEBUG + extension JWT { + public static func test( + token: String = "token", + expiryDate: Date = Date() + ) -> JWT { + .init( + token: token, + expiryDate: expiryDate + ) + } + } +#endif diff --git a/Sources/TuistServer/Utilities/RemoteArtifactDownloader.swift b/Sources/TuistServer/Utilities/RemoteArtifactDownloader.swift new file mode 100644 index 00000000000..46c01423416 --- /dev/null +++ b/Sources/TuistServer/Utilities/RemoteArtifactDownloader.swift @@ -0,0 +1,78 @@ +import Foundation +import Mockable +import Path +import TuistSupport + +enum RemoteArtifactDownloaderError: FatalError, Equatable { + case urlSessionError(url: URL, httpMethod: String, description: String) + case noURLResponse(URL?) + + var type: ErrorType { + switch self { + case .urlSessionError: return .abort + case .noURLResponse: return .abort + } + } + + var description: String { + switch self { + case let .urlSessionError(url, httpMethod, error): + return "Received a session error when sending \(httpMethod) request to \(url.absoluteString): \(error)" + case let .noURLResponse(url): + if let url { + return "The response from request to URL \(url.absoluteString) doesnt' have the expected type HTTPURLResponse" + } else { + return "Received a response that doesn't have the expected type HTTPURLResponse" + } + } + } +} + +@Mockable +public protocol RemoteArtifactDownloading { + func download(url: URL) async throws -> AbsolutePath? +} + +public struct RemoteArtifactDownloader: RemoteArtifactDownloading { + private let urlSession: URLSession + + public init() { + self.init(urlSession: URLSession.shared) + } + + init(urlSession: URLSession) { + self.urlSession = urlSession + } + + public func download(url: URL) async throws -> AbsolutePath? { + let request = URLRequest(url: url) + do { + let (localUrl, response) = try await urlSession.download(for: request) + guard let urlResponse = response as? HTTPURLResponse else { + throw RemoteArtifactDownloaderError.noURLResponse(request.url) + } + if (200 ..< 300).contains(urlResponse.statusCode) { + return try AbsolutePath(validating: localUrl.path) + } else if urlResponse.statusCode == 404 { + return nil + } else { + throw RemoteArtifactDownloaderError.urlSessionError( + url: request.url!, + httpMethod: request.httpMethod!, + description: response.description + ) + } + } catch { + if error is RemoteArtifactDownloaderError { + throw error + } else { + throw RemoteArtifactDownloaderError.urlSessionError( + url: request.url!, + httpMethod: + request.httpMethod!, + description: error.localizedDescription + ) + } + } + } +} diff --git a/Sources/TuistServer/Utilities/RetryProvider.swift b/Sources/TuistServer/Utilities/RetryProvider.swift new file mode 100644 index 00000000000..6a88382d1bf --- /dev/null +++ b/Sources/TuistServer/Utilities/RetryProvider.swift @@ -0,0 +1,42 @@ +import Foundation + +public protocol RetryProviding { + func runWithRetries( + operation: @Sendable @escaping () async throws -> T + ) async throws -> T +} + +public struct RetryProvider: RetryProviding { + private let delayProvider: DelayProviding + + public init( + delayProvider: DelayProviding = DelayProvider() + ) { + self.delayProvider = delayProvider + } + + public func runWithRetries( + operation: @Sendable @escaping () async throws -> T + ) async throws -> T { + try await Task { + let maxRetryCount = 3 + for retry in 0 ..< maxRetryCount { + do { + return try await operation() + } catch { + logger.debug(""" + The following error happened for retry \(retry): \(error.localizedDescription). + Retrying... + """) + try await Task.sleep(nanoseconds: delayProvider.delay(for: retry)) + + continue + } + } + + try Task.checkCancellation() + return try await operation() + } + .value + } +} diff --git a/Sources/TuistServer/Utilities/ServerAuthenticationController.swift b/Sources/TuistServer/Utilities/ServerAuthenticationController.swift new file mode 100644 index 00000000000..08cbaa22dd0 --- /dev/null +++ b/Sources/TuistServer/Utilities/ServerAuthenticationController.swift @@ -0,0 +1,145 @@ +import Foundation +import Mockable +import TuistSupport + +@Mockable +public protocol ServerAuthenticationControlling: Sendable { + func authenticationToken(serverURL: URL) throws -> AuthenticationToken? +} + +public enum AuthenticationToken: CustomStringConvertible, Equatable { + /// The token represents a user session. User sessions are typically used in + /// local environments where the user can be guided through an interactive + /// authentication workflow + case user(legacyToken: String?, accessToken: JWT?, refreshToken: JWT?) + + /// The token represents a project session. Project sessions are typically used + /// in CI environments where limited scopes are desired for security reasons. + case project(String) + + /// It returns the value of the token + public var value: String { + switch self { + case let .user(legacyToken: legacyToken, accessToken: accessToken, refreshToken: _): + if let accessToken { + return accessToken.token + } else { + return legacyToken! + } + case let .project(token): + return token + } + } + + public var description: String { + switch self { + case .user: + return "tuist user token: \(value)" + case let .project(token): + return "tuist project token: \(token)" + } + } +} + +enum ServerAuthenticationControllerError: FatalError { + case invalidJWT(String) + + var description: String { + switch self { + case let .invalidJWT(token): + return "The access token \(token) is invalid. Try to reauthenticate by running `tuist auth`." + } + } + + var type: ErrorType { + switch self { + case .invalidJWT: + return .bug + } + } +} + +public final class ServerAuthenticationController: ServerAuthenticationControlling { + private let credentialsStore: ServerCredentialsStoring + private let ciChecker: CIChecking + private let environment: Environmenting + + public init( + credentialsStore: ServerCredentialsStoring = ServerCredentialsStore(), + ciChecker: CIChecking = CIChecker(), + environment: Environmenting = Environment.shared + ) { + self.credentialsStore = credentialsStore + self.ciChecker = ciChecker + self.environment = environment + } + + public func authenticationToken(serverURL: URL) throws -> AuthenticationToken? { + if ciChecker.isCI() { + if let configToken = environment.tuistVariables[Constants.EnvironmentVariables.token] { + return .project(configToken) + } else if let deprecatedToken = environment.tuistVariables[Constants.EnvironmentVariables.deprecatedToken] { + logger + .warning( + "Use `TUIST_CONFIG_TOKEN` environment variable instead of `TUIST_CONFIG_CLOUD_TOKEN` to authenticate on the CI" + ) + return .project(deprecatedToken) + } else { + return nil + } + } else { + let credentials = try credentialsStore.read(serverURL: serverURL) + return try credentials.map { + if let refreshToken = $0.refreshToken { + return .user( + legacyToken: nil, + accessToken: try $0.accessToken.map(parseJWT), + refreshToken: try parseJWT(refreshToken) + ) + } else { + logger.warning("You are using a deprecated user token. Please, reauthenticate by running `tuist auth`.") + return .user( + legacyToken: $0.token, + accessToken: nil, + refreshToken: nil + ) + } + } + } + } + + private func parseJWT(_ jwt: String) throws -> JWT { + let components = jwt.components(separatedBy: ".") + guard components.count == 3 + else { + throw ServerAuthenticationControllerError.invalidJWT(jwt) + } + let jwtEncodedPayload = components[1] + let remainder = jwtEncodedPayload.count % 4 + let paddedJWTEncodedPayload: String + if remainder > 0 { + paddedJWTEncodedPayload = jwtEncodedPayload.padding( + toLength: jwtEncodedPayload.count + 4 - remainder, + withPad: "=", + startingAt: 0 + ) + } else { + paddedJWTEncodedPayload = jwtEncodedPayload + } + guard let data = Data(base64Encoded: paddedJWTEncodedPayload) + else { + throw ServerAuthenticationControllerError.invalidJWT(jwtEncodedPayload) + } + let jsonDecoder = JSONDecoder() + let payload = try jsonDecoder.decode(JWTPayload.self, from: data) + + return JWT( + token: jwt, + expiryDate: Date(timeIntervalSince1970: TimeInterval(payload.exp)) + ) + } + + private struct JWTPayload: Codable { + let exp: Int + } +} diff --git a/Sources/TuistServer/Utilities/ServerCredentialsStore.swift b/Sources/TuistServer/Utilities/ServerCredentialsStore.swift new file mode 100644 index 00000000000..82a3c51f944 --- /dev/null +++ b/Sources/TuistServer/Utilities/ServerCredentialsStore.swift @@ -0,0 +1,169 @@ +import FileSystem +import Foundation +import Mockable +import Path +import TuistSupport + +public struct ServerCredentials: Codable, Equatable { + /// Deprecated authentication token. + public let token: String? + + /// JWT access token + public let accessToken: String? + + /// JWT refresh token + public let refreshToken: String? + + /// Initializes the credentials with its attributes. + /// - Parameters: + /// - token: Authentication token. + /// - account: Account identifier. + public init( + token: String?, + accessToken: String?, + refreshToken: String? + ) { + self.token = token + self.accessToken = accessToken + self.refreshToken = refreshToken + } +} + +@Mockable +public protocol ServerCredentialsStoring: Sendable { + /// It stores the credentials for the server with the given URL. + /// - Parameters: + /// - credentials: Credentials to be stored. + /// - serverURL: Server URL (without path). + func store(credentials: ServerCredentials, serverURL: URL) throws + + /// Gets the credentials to authenticate the user against the server with the given URL. Throws an error if credentials are + /// not found. + /// - Parameter serverURL: Server URL (without path). + func get(serverURL: URL) throws -> ServerCredentials + + /// Reads the credentials to authenticate the user against the server with the given URL. + /// - Parameter serverURL: Server URL (without path). + func read(serverURL: URL) throws -> ServerCredentials? + + /// Deletes the credentials for the server with the given URL. + /// - Parameter serverURL: Server URL (without path). + func delete(serverURL: URL) async throws +} + +enum ServerCredentialsStoreError: FatalError { + case credentialsNotFound + case xcdgHomePathNotAbsolute(String) + case invalidServerURL(String) + + var description: String { + switch self { + case .credentialsNotFound: + return "You are not authenticated. Authenticate by running `tuist auth`." + case let .xcdgHomePathNotAbsolute(path): + return "We expected the value of the XDG_CONFIG_HOME environment variable, \(path), to be an absolute path but it's not." + case let .invalidServerURL(url): + return "We couldn't obtain the host from the following URL because it seems invalid \(url)" + } + } + + var type: ErrorType { + switch self { + case .credentialsNotFound: + return .abort + case .xcdgHomePathNotAbsolute: + return .abort + case .invalidServerURL: + return .abort + } + } +} + +public final class ServerCredentialsStore: ServerCredentialsStoring { + private let fileHandler: FileHandling + private let fileSystem: FileSystem + private let configDirectory: AbsolutePath? + + /// Default initializer. + public convenience init() { + self.init(fileHandler: FileHandler.shared) + } + + init(fileHandler: FileHandling, fileSystem: FileSystem = FileSystem(), configDirectory: AbsolutePath? = nil) { + self.fileHandler = fileHandler + self.configDirectory = configDirectory + self.fileSystem = fileSystem + } + + // MARK: - CredentialsStoring + + public func store(credentials: ServerCredentials, serverURL: URL) throws { + let path = try credentialsFilePath(serverURL: serverURL) + let data = try JSONEncoder().encode(credentials) + if !fileHandler.exists(path.parentDirectory) { + try fileHandler.createFolder(path.parentDirectory) + } + try data.write(to: path.url, options: .atomic) + } + + public func read(serverURL: URL) throws -> ServerCredentials? { + let path = try credentialsFilePath(serverURL: serverURL) + guard fileHandler.exists(path) else { return nil } + let data = try fileHandler.readFile(path) + + /** + This might fail if we've migrated the schema, which is very unlikely, or if someone modifies the content in it + and the new schema doesn't align with the one that we expect. We could add logic to handle those gracefully, + but since the user can recover from it by signing in again, I think it's ok not to add more complexity here. + */ + return try? JSONDecoder().decode(ServerCredentials.self, from: data) + } + + public func get(serverURL: URL) throws -> ServerCredentials { + guard let credentials = try read(serverURL: serverURL) + else { + throw ServerCredentialsStoreError.credentialsNotFound + } + + return credentials + } + + public func delete(serverURL: URL) async throws { + let path = try credentialsFilePath(serverURL: serverURL) + if fileHandler.exists(path) { + try await fileSystem.remove(path) + } + } + + // MARK: - Fileprivate + + fileprivate static func configDirectory() throws -> AbsolutePath { + var directory: AbsolutePath! + + if let xdgConfigHomeString = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"] { + do { + directory = try AbsolutePath(validating: xdgConfigHomeString) + } catch { + throw ServerCredentialsStoreError.xcdgHomePathNotAbsolute(xdgConfigHomeString) + } + } + + if directory == nil { + directory = FileHandler.shared.homeDirectory.appending(component: ".config") + } + return directory.appending(component: "tuist") + } + + fileprivate func credentialsFilePath(serverURL: URL) throws -> AbsolutePath { + guard let components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false), let host = components.host else { + throw ServerCredentialsStoreError.invalidServerURL(serverURL.absoluteString) + } + let directory = if let configDirectory { + configDirectory + } else { + try ServerCredentialsStore.configDirectory() + } + // swiftlint:disable:next force_try + return directory.appending(try! RelativePath(validating: "credentials/\(host).json")) + } +} diff --git a/Sources/TuistServer/Utilities/UniqueIDGenerator.swift b/Sources/TuistServer/Utilities/UniqueIDGenerator.swift new file mode 100644 index 00000000000..fb1103af5eb --- /dev/null +++ b/Sources/TuistServer/Utilities/UniqueIDGenerator.swift @@ -0,0 +1,13 @@ +import Foundation +import Mockable + +@Mockable +protocol UniqueIDGenerating { + func uniqueID() -> String +} + +final class UniqueIDGenerator: UniqueIDGenerating { + func uniqueID() -> String { + UUID().uuidString + } +} diff --git a/Sources/TuistServer/Utilities/XCResultToolController.swift b/Sources/TuistServer/Utilities/XCResultToolController.swift new file mode 100644 index 00000000000..121946c78e7 --- /dev/null +++ b/Sources/TuistServer/Utilities/XCResultToolController.swift @@ -0,0 +1,31 @@ +import Mockable +import Path +import TuistSupport + +@Mockable +protocol XCResultToolControlling { + func resultBundleObject(_ path: AbsolutePath) throws -> String + func resultBundleObject(_ path: AbsolutePath, id: String) throws -> String +} + +final class XCResultToolController: XCResultToolControlling { + private let system: Systeming + + init( + system: Systeming = System.shared + ) { + self.system = system + } + + func resultBundleObject(_ path: AbsolutePath) throws -> String { + try system.capture( + ["/usr/bin/xcrun", "xcresulttool", "get", "--path", path.pathString, "--format", "json"] + ) + } + + func resultBundleObject(_ path: AbsolutePath, id: String) throws -> String { + try system.capture( + ["/usr/bin/xcrun", "xcresulttool", "get", "--path", path.pathString, "--id", id, "--format", "json"] + ) + } +} diff --git a/Sources/TuistSigning/Certificate/Certificate.swift b/Sources/TuistSigning/Certificate/Certificate.swift deleted file mode 100644 index 4ea195d4eff..00000000000 --- a/Sources/TuistSigning/Certificate/Certificate.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import TSCBasic - -struct Certificate: Equatable { - let publicKey: AbsolutePath - let privateKey: AbsolutePath - /// Content of the fingerprint property of the public key - let fingerprint: String - let developmentTeam: String - let name: String - let isRevoked: Bool -} diff --git a/Sources/TuistSigning/Certificate/CertificateParser.swift b/Sources/TuistSigning/Certificate/CertificateParser.swift deleted file mode 100644 index 0307bb02e5b..00000000000 --- a/Sources/TuistSigning/Certificate/CertificateParser.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -enum CertificateParserError: FatalError, Equatable { - case nameParsingFailed(AbsolutePath, String) - case developmentTeamParsingFailed(AbsolutePath, String) - case fileParsingFailed(AbsolutePath) - - var type: ErrorType { - switch self { - case .nameParsingFailed, .developmentTeamParsingFailed, .fileParsingFailed: - return .abort - } - } - - var description: String { - switch self { - case let .nameParsingFailed(path, input): - return "We couldn't parse the name while parsing the following output from the file \(path.pathString): \(input)" - case let .developmentTeamParsingFailed(path, input): - return "We couldn't parse the development team while parsing the following output from the file \(path.pathString): \(input)" - case let .fileParsingFailed(path): - return "We couldn't parse the file \(path.pathString)" - } - } -} - -/// Used to parse and extract info from a certificate -protocol CertificateParsing { - /// Parse public-private key pair - /// - Returns: Parse `Certificate` - func parse(publicKey: AbsolutePath, privateKey: AbsolutePath) throws -> Certificate - - /// Retrieve fingerprint of a public key - func parseFingerPrint(developerCertificate: Data) throws -> String -} - -/// Subject attributes that are returnen with `openssl x509 -subject` -private enum SubjectAttribute: String { - case commonName = "CN" - case country = "C" - case description - case emailAddress - case locality = "L" - case organization = "O" - case organizationalUnit = "OU" - case state = "ST" - case uid = "UID" -} - -final class CertificateParser: CertificateParsing { - func parse(publicKey: AbsolutePath, privateKey: AbsolutePath) throws -> Certificate { - let subject = try subject(at: publicKey) - let fingerprint = try fingerprint(at: publicKey) - let isRevoked = subject.contains("REVOKED") - - let nameRegex = try NSRegularExpression( - pattern: SubjectAttribute.commonName.rawValue + " *= *(?:\"([^/,]+)\"|([^/,]+))", - options: [] - ) - guard let nameResult = nameRegex.firstMatch(in: subject, range: NSRange(location: 0, length: subject.count)), - nameResult.range(at: 1).length > 0 || nameResult.range(at: 2).length > 0 - else { throw CertificateParserError.nameParsingFailed(publicKey, subject) } - let nameResultRange = nameResult.range(at: nameResult.range(at: 1).length > 0 ? 1 : 2) - let name = NSString(string: subject).substring(with: nameResultRange).spm_chomp() - - let developmentTeamRegex = try NSRegularExpression( - pattern: SubjectAttribute.organizationalUnit.rawValue + " *= *(?:\"([^/,]+)\"|([^/,]+))", - options: [] - ) - guard let developmentTeamResult = developmentTeamRegex.firstMatch( - in: subject, - range: NSRange(location: 0, length: subject.count) - ), nameResult.range(at: 1).length > 0 || nameResult.range(at: 2).length > 0 - else { throw CertificateParserError.developmentTeamParsingFailed(publicKey, subject) } - let developmentTeamResultRange = developmentTeamResult.range(at: developmentTeamResult.range(at: 1).length > 0 ? 1 : 2) - let developmentTeam = NSString(string: subject).substring(with: developmentTeamResultRange).spm_chomp() - - return Certificate( - publicKey: publicKey, - privateKey: privateKey, - fingerprint: fingerprint, - developmentTeam: developmentTeam, - name: name.sanitizeEncoding(), - isRevoked: isRevoked - ) - } - - func parseFingerPrint(developerCertificate: Data) throws -> String { - let temporaryFile = try FileHandler.shared.temporaryDirectory().appending(component: "developerCertificate.cer") - try developerCertificate.write(to: temporaryFile.asURL) - - return try fingerprint(at: temporaryFile) - } - - // MARK: - Helpers - - private func subject(at path: AbsolutePath) throws -> String { - do { - return try System.shared - .capture(["/usr/bin/openssl", "x509", "-inform", "der", "-in", path.pathString, "-noout", "-subject"]) - } catch let TuistSupport.SystemError.terminated(_, _, standardError) { - if let string = String(data: standardError, encoding: .utf8) { - logger.warning("Parsing subject of \(path) failed with: \(string)") - } - throw CertificateParserError.fileParsingFailed(path) - } catch { - throw CertificateParserError.fileParsingFailed(path) - } - } - - private func fingerprint(at path: AbsolutePath) throws -> String { - do { - return try System.shared - .capture(["/usr/bin/openssl", "x509", "-inform", "der", "-in", path.pathString, "-noout", "-fingerprint"]) - .spm_chomp() - } catch let TuistSupport.SystemError.terminated(_, _, standardError) { - if let string = String(data: standardError, encoding: .utf8) { - logger.warning("Parsing fingerprint of \(path) failed with: \(string)") - } - throw CertificateParserError.fileParsingFailed(path) - } catch { - throw CertificateParserError.fileParsingFailed(path) - } - } -} - -extension String { - func sanitizeEncoding() -> String { - // Had some real life certificates where encoding in the name was broken - e.g. \\xC3\\xA4 instead of ä - guard let regex = try? NSRegularExpression(pattern: "(\\\\x([A-Za-z0-9]{2}))(\\\\x([A-Za-z0-9]{2}))", options: []) - else { return self } - let matches = regex.matches(in: self, options: [], range: NSRange(startIndex..., in: self)).reversed() - - var modifiableString = self - 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 { - continue - } - let resultRange = Range(result.range, in: modifiableString)! - modifiableString.replaceSubrange( - resultRange, - with: String(decoding: [firstInt, secondInt] as [UTF8.CodeUnit], as: UTF8.self) - ) - } - - return modifiableString - } -} diff --git a/Sources/TuistSigning/GraphMappers/Dictionary+Fingerprint.swift b/Sources/TuistSigning/GraphMappers/Dictionary+Fingerprint.swift deleted file mode 100644 index b74a659ec85..00000000000 --- a/Sources/TuistSigning/GraphMappers/Dictionary+Fingerprint.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -extension [Fingerprint: Certificate] { - func first(for provisioningProfile: ProvisioningProfile) -> Certificate? { - provisioningProfile.developerCertificateFingerprints.compactMap { self[$0] }.first - } -} diff --git a/Sources/TuistSigning/GraphMappers/SigningMapper.swift b/Sources/TuistSigning/GraphMappers/SigningMapper.swift deleted file mode 100644 index 58175afc3a3..00000000000 --- a/Sources/TuistSigning/GraphMappers/SigningMapper.swift +++ /dev/null @@ -1,102 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -public class SigningMapper: ProjectMapping { - private let signingFilesLocator: SigningFilesLocating - private let signingMatcher: SigningMatching - private let signingCipher: SigningCiphering - - public convenience init() { - self.init( - signingFilesLocator: SigningFilesLocator(), - signingMatcher: SigningMatcher(), - signingCipher: SigningCipher() - ) - } - - init( - signingFilesLocator: SigningFilesLocating, - signingMatcher: SigningMatching, - signingCipher: SigningCiphering - ) { - self.signingFilesLocator = signingFilesLocator - self.signingMatcher = signingMatcher - self.signingCipher = signingCipher - } - - // MARK: - GraphMapping - - public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { - var project = project - let path = project.path - guard try signingFilesLocator.locateSigningDirectory(from: path) != nil - else { - logger.debug("No signing artifacts found") - return (project, []) - } - - try signingCipher.decryptSigning(at: path, keepFiles: true) - defer { try? signingCipher.encryptSigning(at: path, keepFiles: false) } - - let derivedDirectory = project.path.appending(component: Constants.DerivedDirectory.name) - let keychainPath = derivedDirectory.appending(component: Constants.DerivedDirectory.signingKeychain) - - let (certificates, provisioningProfiles) = try signingMatcher.match(from: project.path) - - project.targets = try project.targets.map { - try map( - target: $0, - project: project, - keychainPath: keychainPath, - certificates: certificates, - provisioningProfiles: provisioningProfiles - ) - } - - return (project, []) - } - - // MARK: - Helpers - - private func map( - target: Target, - project: Project, - keychainPath: AbsolutePath, - certificates: [Fingerprint: Certificate], - provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]] - ) throws -> Target { - var target = target - let targetConfigurations = target.settings?.configurations ?? [:] - let configurations: [BuildConfiguration: Configuration?] = targetConfigurations - .merging( - project.settings.configurations, - uniquingKeysWith: { config, _ in config } - ) - .reduce(into: [:]) { dict, configurationPair in - guard let provisioningProfile = provisioningProfiles[target.name]?[configurationPair.key.name], - let certificate = certificates.first(for: provisioningProfile) - else { - dict[configurationPair.key] = configurationPair.value - return - } - let configuration = configurationPair.value ?? Configuration() - var settings = configuration.settings - settings["CODE_SIGN_STYLE"] = "Manual" - settings["CODE_SIGN_IDENTITY"] = SettingValue(stringLiteral: certificate.name) - settings["OTHER_CODE_SIGN_FLAGS"] = SettingValue(stringLiteral: "--keychain \(keychainPath.pathString)") - settings["DEVELOPMENT_TEAM"] = SettingValue(stringLiteral: provisioningProfile.teamId) - settings["PROVISIONING_PROFILE_SPECIFIER"] = SettingValue(stringLiteral: provisioningProfile.uuid) - dict[configurationPair.key] = configuration.with(settings: settings) - } - - target.settings = Settings( - base: target.settings?.base ?? [:], - configurations: configurations, - defaultSettings: target.settings?.defaultSettings ?? .recommended - ) - return target - } -} diff --git a/Sources/TuistSigning/Linter/SigningLinter.swift b/Sources/TuistSigning/Linter/SigningLinter.swift deleted file mode 100644 index 15b552da6dc..00000000000 --- a/Sources/TuistSigning/Linter/SigningLinter.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation -import TuistCore -import TuistGraph - -protocol SigningLinting { - func lint(certificate: Certificate, provisioningProfile: ProvisioningProfile) -> [LintingIssue] - func lint(certificate: Certificate) -> [LintingIssue] - func lint(provisioningProfile: ProvisioningProfile, target: Target) -> [LintingIssue] -} - -final class SigningLinter: SigningLinting { - func lint(certificate: Certificate, provisioningProfile: ProvisioningProfile) -> [LintingIssue] { - var issues: [LintingIssue] = [] - if certificate.developmentTeam != provisioningProfile.teamId { - let reason = """ - Certificate \(certificate.name)'s development team \( - certificate - .developmentTeam - ) does not correspond to \(provisioningProfile.teamId). - Make sure they are the same. - """ - issues.append(LintingIssue(reason: reason, severity: .error)) - } - - return issues - } - - func lint(certificate: Certificate) -> [LintingIssue] { - var issues: [LintingIssue] = [] - if certificate.isRevoked { - issues.append(LintingIssue( - reason: "Certificate \(certificate.name) is revoked. Create a new one and replace it to resolve the issue.", - severity: .warning - )) - } - return issues - } - - func lint(provisioningProfile: ProvisioningProfile, target: Target) -> [LintingIssue] { - let appIdPrefix = provisioningProfile.applicationIdPrefix.first ?? provisioningProfile.teamId - let appId = appIdPrefix + "." + target.bundleId - let invalidProvisioningProfileIssue = LintingIssue( - reason: """ - App id \(provisioningProfile.appId) does not correspond to \(appIdPrefix).\( - target - .bundleId - ). Make sure the provisioning profile has been added to the right target. - """, - severity: .error - ) - let buildSettingRegex = "\\$[\\({](.*)[\\)}]" - - var issues: [LintingIssue] = [] - - if target.bundleId.matches(pattern: buildSettingRegex) { - return issues - } else if provisioningProfile.appId.last == "*" { - if !appId.hasPrefix(provisioningProfile.appId.dropLast()) { - issues.append(invalidProvisioningProfileIssue) - } - } else { - if provisioningProfile.appId != appId { - issues.append(invalidProvisioningProfileIssue) - } - } - - return issues - } -} diff --git a/Sources/TuistSigning/Log/Logger.swift b/Sources/TuistSigning/Log/Logger.swift deleted file mode 100644 index 03bcaf3a40b..00000000000 --- a/Sources/TuistSigning/Log/Logger.swift +++ /dev/null @@ -1,3 +0,0 @@ -import TuistSupport - -let logger = Logger(label: "io.tuist.signing") diff --git a/Sources/TuistSigning/ProvisioningProfile/ProvisioningProfile.swift b/Sources/TuistSigning/ProvisioningProfile/ProvisioningProfile.swift deleted file mode 100644 index c37a0c3b312..00000000000 --- a/Sources/TuistSigning/ProvisioningProfile/ProvisioningProfile.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -private protocol Entitlements: Decodable { - var appId: String { get } -} - -/// Model of a provisioning profile -struct ProvisioningProfile: Equatable { - /// Path to the provisioning profile - var path: AbsolutePath - let name: String - let targetName: String - let configurationName: String - let uuid: String - let teamId: String - let appId: String - let appIdName: String - let applicationIdPrefix: [String] - let platforms: [String] - let expirationDate: Date - let developerCertificateFingerprints: [String] - - struct Content { - let name: String - let uuid: String - let teamId: String - let appId: String - let appIdName: String - let applicationIdPrefix: [String] - let platforms: [String] - let expirationDate: Date - let developerCertificates: [Data] - } -} - -extension ProvisioningProfile.Content: Decodable { - private enum CodingKeys: String, CodingKey { - case name = "Name" - case uuid = "UUID" - case teamIds = "TeamIdentifier" - case appIdName = "AppIDName" - case applicationIdPrefix = "ApplicationIdentifierPrefix" - case platforms = "Platform" - case entitlements = "Entitlements" - case expirationDate = "ExpirationDate" - case developerCertificates = "DeveloperCertificates" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - name = try container.decode(String.self, forKey: .name) - uuid = try container.decode(String.self, forKey: .uuid) - teamId = try container.decode(DecodingFirst.self, forKey: .teamIds).wrappedValue - appIdName = try container.decode(String.self, forKey: .appIdName) - applicationIdPrefix = try container.decode([String].self, forKey: .applicationIdPrefix) - platforms = try container.decode([String].self, forKey: .platforms) - let entitlements = try Self.platformEntitlements(container, for: platforms) - appId = entitlements.appId - expirationDate = try container.decode(Date.self, forKey: .expirationDate) - developerCertificates = try container.decode([Data].self, forKey: .developerCertificates) - } - - private static func platformEntitlements( - _ container: KeyedDecodingContainer, - for platforms: [String] - ) throws -> Entitlements { - // OSX profiles are special because they use a different key to define the application identifier - if platforms.contains("OSX") { - try container.decode(DesktopEntitlements.self, forKey: .entitlements) - } else { - try container.decode(MobileEntitlements.self, forKey: .entitlements) - } - } -} - -extension ProvisioningProfile.Content { - private struct MobileEntitlements: Entitlements { - private(set) var appId: String - - enum CodingKeys: String, CodingKey { - case appId = "application-identifier" - } - } - - private struct DesktopEntitlements: Entitlements { - private(set) var appId: String - - enum CodingKeys: String, CodingKey { - case appId = "com.apple.application-identifier" - } - } -} diff --git a/Sources/TuistSigning/ProvisioningProfile/ProvisioningProfileParser.swift b/Sources/TuistSigning/ProvisioningProfile/ProvisioningProfileParser.swift deleted file mode 100644 index 575cb3597b5..00000000000 --- a/Sources/TuistSigning/ProvisioningProfile/ProvisioningProfileParser.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -enum ProvisioningProfileParserError: FatalError { - var type: ErrorType { - switch self { - case .valueNotFound, .invalidFormat: - return .abort - } - } - - var description: String { - switch self { - case let .valueNotFound(value, path): - return "Could not find \(value). Check if the provided xml at \(path.pathString) is valid." - case let .invalidFormat(provisioningProfile): - return "Provisioning Profile \(provisioningProfile) is in invalid format. Please name your certificates in the following way: Target.Configuration.mobileprovision" - } - } - - case valueNotFound(String, AbsolutePath) - case invalidFormat(String) -} - -protocol ProvisioningProfileParsing { - func parse(at path: AbsolutePath) throws -> ProvisioningProfile -} - -final class ProvisioningProfileParser: ProvisioningProfileParsing { - private let securityController: SecurityControlling - private let certificateParser: CertificateParsing - - init( - securityController: SecurityControlling = SecurityController(), - certificateParser: CertificateParsing = CertificateParser() - ) { - self.securityController = securityController - self.certificateParser = certificateParser - } - - func parse(at path: AbsolutePath) throws -> ProvisioningProfile { - let provisioningProfileComponents = path.basenameWithoutExt.components(separatedBy: ".") - guard provisioningProfileComponents.count == 2 - else { throw ProvisioningProfileParserError.invalidFormat(path.pathString) } - let targetName = provisioningProfileComponents[0] - let configurationName = provisioningProfileComponents[1] - - let unencryptedProvisioningProfile = try securityController.decodeFile(at: path) - let plistData = Data(unencryptedProvisioningProfile.utf8) - let provisioningProfileContent = try PropertyListDecoder().decode(ProvisioningProfile.Content.self, from: plistData) - - let developerCertificateFingerprints = try provisioningProfileContent.developerCertificates.map { - try certificateParser.parseFingerPrint(developerCertificate: $0) - } - - return ProvisioningProfile( - path: path, - name: provisioningProfileContent.name, - targetName: targetName, - configurationName: configurationName, - uuid: provisioningProfileContent.uuid, - teamId: provisioningProfileContent.teamId, - appId: provisioningProfileContent.appId, - appIdName: provisioningProfileContent.appIdName, - applicationIdPrefix: provisioningProfileContent.applicationIdPrefix, - platforms: provisioningProfileContent.platforms, - expirationDate: provisioningProfileContent.expirationDate, - developerCertificateFingerprints: developerCertificateFingerprints - ) - } -} diff --git a/Sources/TuistSigning/SecurityController.swift b/Sources/TuistSigning/SecurityController.swift deleted file mode 100644 index d76efb3f117..00000000000 --- a/Sources/TuistSigning/SecurityController.swift +++ /dev/null @@ -1,75 +0,0 @@ -import TSCBasic -import TuistSupport - -/// Controller for command line utility `security` -protocol SecurityControlling { - func decodeFile(at path: AbsolutePath) throws -> String - func importCertificate(_ certificate: Certificate, keychainPath: AbsolutePath) throws - func createKeychain(at path: AbsolutePath, password: String) throws - func unlockKeychain(at path: AbsolutePath, password: String) throws - func lockKeychain(at path: AbsolutePath, password: String) throws -} - -final class SecurityController: SecurityControlling { - func decodeFile(at path: AbsolutePath) throws -> String { - try System.shared.capture(["/usr/bin/security", "cms", "-D", "-i", path.pathString]) - } - - func importCertificate(_ certificate: Certificate, keychainPath: AbsolutePath) throws { - if try !certificateExists(certificate, keychainPath: keychainPath) { - try importToKeychain(at: certificate.publicKey, keychainPath: keychainPath) - logger.debug("Imported certificate at \(certificate.publicKey.pathString)") - - // found no way to check for the presence of a private key in the keychain, but fortunately keychain takes care of - // duplicate private keys on its own - try importToKeychain(at: certificate.privateKey, keychainPath: keychainPath) - logger.debug("Imported certificate private key at \(certificate.privateKey.pathString)") - } else { - logger.debug("Skipping importing certificate at \(certificate.publicKey.pathString) because it is already present") - } - } - - func createKeychain(at path: AbsolutePath, password: String) throws { - try System.shared.run(["/usr/bin/security", "create-keychain", "-p", password, path.pathString]) - logger.debug("Created keychain at \(path.pathString)") - } - - func unlockKeychain(at path: AbsolutePath, password: String) throws { - try System.shared.run(["/usr/bin/security", "unlock-keychain", "-p", password, path.pathString]) - logger.debug("Unlocked keychain at \(path.pathString)") - } - - func lockKeychain(at path: AbsolutePath, password: String) throws { - try System.shared.run(["/usr/bin/security", "lock-keychain", "-p", password, path.pathString]) - logger.debug("Locked keychain at \(path.pathString)") - } - - // MARK: - Helpers - - private func certificateExists(_ certificate: Certificate, keychainPath: AbsolutePath) throws -> Bool { - do { - let existingCertificates = try System.shared.capture([ - "/usr/bin/security", - "find-certificate", - "-c", - certificate.name, - "-a", - keychainPath.pathString, - ]) - return !existingCertificates.isEmpty - } catch { - return false - } - } - - private func importToKeychain(at path: AbsolutePath, keychainPath: AbsolutePath) throws { - try System.shared.run([ - "/usr/bin/security", - "import", path.pathString, - "-P", "", - "-T", "/usr/bin/codesign", - "-T", "/usr/bin/security", - "-k", keychainPath.pathString, - ]) - } -} diff --git a/Sources/TuistSigning/SigningCipher.swift b/Sources/TuistSigning/SigningCipher.swift deleted file mode 100644 index c6752bfa7f4..00000000000 --- a/Sources/TuistSigning/SigningCipher.swift +++ /dev/null @@ -1,238 +0,0 @@ -import CryptoSwift -import Foundation -import TSCBasic -import TuistCore -import TuistSupport - -enum SigningCipherError: FatalError, Equatable { - case failedToEncrypt - case failedToDecrypt(String) - case ivGenerationFailed(String) - case masterKeyNotFound(AbsolutePath) - case signingDirectoryNotFound(AbsolutePath) - - var type: ErrorType { - switch self { - case .failedToEncrypt, .failedToDecrypt, .ivGenerationFailed, - .masterKeyNotFound, .signingDirectoryNotFound: - return .abort - } - } - - var description: String { - switch self { - case .failedToEncrypt: - return "Unable to encrypt data" - case let .failedToDecrypt(reason): - return "Could not decrypt data: \(reason)" - case let .ivGenerationFailed(reason): - return "Generation of IV failed with error: \(reason)" - case let .masterKeyNotFound(masterKeyPath): - return "Could not find master.key at \(masterKeyPath.pathString)" - case let .signingDirectoryNotFound(fromPath): - return "Could not find signing directory from \(fromPath.pathString)" - } - } -} - -/// SigningCiphering handles all encryption/decryption of files needed for signing (certificates, profiles, etc.) -public protocol SigningCiphering { - /// Encrypts all signing files at `Tuist/Signing` - /// - Parameters: - /// - keepFiles: Keep unencrypted files - func encryptSigning(at path: AbsolutePath, keepFiles: Bool) throws - /// Decrypts all signing files at `Tuist/Signing - /// - Parameters: - /// - keepFiles: Keep encrypted files - func decryptSigning(at path: AbsolutePath, keepFiles: Bool) throws - func readMasterKey(at path: AbsolutePath) throws -> String -} - -public final class SigningCipher: SigningCiphering { - private let rootDirectoryLocator: RootDirectoryLocating - private let signingFilesLocator: SigningFilesLocating - - /// Public initializer - public convenience init() { - self.init( - rootDirectoryLocator: RootDirectoryLocator(), - signingFilesLocator: SigningFilesLocator() - ) - } - - init( - rootDirectoryLocator: RootDirectoryLocating, - signingFilesLocator: SigningFilesLocating - ) { - self.rootDirectoryLocator = rootDirectoryLocator - self.signingFilesLocator = signingFilesLocator - } - - public func encryptSigning(at path: AbsolutePath, keepFiles: Bool) throws { - let masterKey = try masterKey(at: path) - let signingKeyFiles = try locateUnencryptedSigningFiles(at: path) - guard !signingKeyFiles.isEmpty else { return } - - let correctlyEncryptedSigningFiles = try correctlyEncryptedSigningFiles(at: path, masterKey: masterKey) - - try locateEncryptedSigningFiles(at: path) - .filter { !correctlyEncryptedSigningFiles.map(\.encrypted).contains($0) } - .forEach(FileHandler.shared.delete) - - let cipheredKeys = try signingKeyFiles - .filter { !correctlyEncryptedSigningFiles.map(\.unencrypted).contains($0) } - .map(FileHandler.shared.readFile) - .map { try encryptData($0, masterKey: masterKey) } - - 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) - } - } - - public func decryptSigning(at path: AbsolutePath, keepFiles: Bool) throws { - let masterKey = try masterKey(at: path) - let signingKeyFiles = try locateEncryptedSigningFiles(at: path) - guard !signingKeyFiles.isEmpty else { return } - let decipheredKeys = try signingKeyFiles - .map(FileHandler.shared.readFile) - .map { - try decryptData($0, masterKey: masterKey) - } - - try locateUnencryptedSigningFiles(at: path) - .forEach(FileHandler.shared.delete) - - for (key, keyFile) in zip(decipheredKeys, signingKeyFiles) { - logger.debug("Decrypting \(keyFile.pathString)") - let decryptedPath = try AbsolutePath( - validating: keyFile.parentDirectory.pathString + "/" + keyFile - .basenameWithoutExt - ) - try key.write(to: decryptedPath.url) - } - - if !keepFiles { - try signingKeyFiles.forEach(FileHandler.shared.delete) - } - } - - /// - Returns: Master key data - public func readMasterKey(at path: AbsolutePath) throws -> String { - guard let rootDirectory = rootDirectoryLocator.locate(from: path) - else { throw SigningCipherError.signingDirectoryNotFound(path) } - let masterKeyFile = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.masterKey) - guard FileHandler.shared.exists(masterKeyFile) else { throw SigningCipherError.masterKeyNotFound(masterKeyFile) } - let plainMasterKey = try FileHandler.shared.readTextFile(masterKeyFile).trimmingCharacters(in: .newlines) - return plainMasterKey - } - - // MARK: - Helpers - - private func locateUnencryptedSigningFiles(at path: AbsolutePath) throws -> [AbsolutePath] { - try signingFilesLocator.locateUnencryptedCertificates(from: path) + signingFilesLocator - .locateUnencryptedPrivateKeys(from: path) - } - - private func locateEncryptedSigningFiles(at path: AbsolutePath) throws -> [AbsolutePath] { - try signingFilesLocator.locateEncryptedCertificates(from: path) + signingFilesLocator - .locateEncryptedPrivateKeys(from: path) - } - - /// - Returns: Files that are already correctly encrypted - private func correctlyEncryptedSigningFiles( - at path: AbsolutePath, - masterKey: Data - ) throws -> [(unencrypted: AbsolutePath, encrypted: AbsolutePath)] { - try locateUnencryptedSigningFiles(at: path).compactMap { unencryptedFile in - let encryptedFile = try AbsolutePath(validating: unencryptedFile.pathString + "." + Constants.encryptedExtension) - guard FileHandler.shared.exists(encryptedFile) else { return nil } - let isEncryptionNeeded: Bool = try self.isEncryptionNeeded( - encryptedFile: encryptedFile, - unencryptedFile: unencryptedFile, - masterKey: masterKey - ) - return isEncryptionNeeded ? nil : (unencrypted: unencryptedFile, encrypted: encryptedFile) - } - } - - /// Determines if encryption is needed - private func isEncryptionNeeded(encryptedFile: AbsolutePath, unencryptedFile: AbsolutePath, masterKey: Data) throws -> Bool { - guard let encodedString = String(data: try FileHandler.shared.readFile(encryptedFile), encoding: .utf8), - let dividerIndex = encodedString.firstIndex(of: "-"), - let iv = Data(base64Encoded: String(encodedString.prefix(upTo: dividerIndex))) - else { throw SigningCipherError.failedToDecrypt("corrupted data") } - - let aesCipher = try AES(key: masterKey.bytes, blockMode: CTR(iv: iv.bytes), padding: .noPadding) - let unencryptedData = try FileHandler.shared.readFile(unencryptedFile) - let encryptedBase64String = try aesCipher.encrypt(unencryptedData.bytes).toBase64() - guard let data = (iv.base64EncodedString() + "-" + encryptedBase64String).data(using: .utf8) else { - throw SigningCipherError.failedToEncrypt - } - - return try FileHandler.shared.readFile(encryptedFile) != data - } - - /// Encrypts `data` - /// - Parameters: - /// - data: Data to encrypt - /// - masterKey: Master key data - /// - Returns: Encrypted data - private func encryptData(_ data: Data, masterKey: Data) throws -> Data { - let iv = try generateIv() - let aesCipher = try AES(key: masterKey.bytes, blockMode: CTR(iv: iv.bytes), padding: .noPadding) - let encryptedBase64String = try aesCipher.encrypt(data.bytes).toBase64() - guard let data = (iv.base64EncodedString() + "-" + encryptedBase64String).data(using: .utf8) else { - throw SigningCipherError.failedToEncrypt - } - return data - } - - /// Decrypts `data` - /// - Parameters: - /// - data: Data to decrypt - /// - masterKey: Master key data - /// - Returns: Decrypted data - private func decryptData(_ data: Data, masterKey: Data) throws -> Data { - guard let encodedString = String(data: data, encoding: .utf8), - let dividerIndex = encodedString.firstIndex(of: "-"), - let iv = Data(base64Encoded: String(encodedString.prefix(upTo: dividerIndex))) - else { throw SigningCipherError.failedToDecrypt("corrupted data") } - - let dataToDecrypt = Data(base64Encoded: String(encodedString.suffix(from: dividerIndex).dropFirst())) - let aesCipher = try AES(key: masterKey.bytes, blockMode: CTR(iv: iv.bytes), padding: .noPadding) - guard let decryptedData = try dataToDecrypt?.decrypt(cipher: aesCipher) - else { throw SigningCipherError.failedToDecrypt("data is in wrong format") } - return decryptedData - } - - /// - Returns: Master key data - private func masterKey(at path: AbsolutePath) throws -> Data { - try readMasterKey(at: path).data(using: .utf8)!.sha256() - } - - /// - Returns: Data of generated initialization vector - private func generateIv() throws -> Data { - let blockSize = 16 - var iv = Data(repeating: 0, count: blockSize) - let result = try iv.withUnsafeMutableBytes { bytes -> Int32 in - guard let baseAddress = bytes.baseAddress - else { throw SigningCipherError.ivGenerationFailed("Base address not found") } - return SecRandomCopyBytes(kSecRandomDefault, blockSize, baseAddress) - } - if result == errSecSuccess { - return iv - } else { - if let errorMessage = SecCopyErrorMessageString(result, nil) { - throw SigningCipherError.ivGenerationFailed(String(errorMessage)) - } else { - throw SigningCipherError.ivGenerationFailed("code: \(result)") - } - } - } -} diff --git a/Sources/TuistSigning/SigningFilesLocator.swift b/Sources/TuistSigning/SigningFilesLocator.swift deleted file mode 100644 index 129a0e41f17..00000000000 --- a/Sources/TuistSigning/SigningFilesLocator.swift +++ /dev/null @@ -1,61 +0,0 @@ -import TSCBasic -import TuistCore -import TuistSupport - -protocol SigningFilesLocating { - func locateSigningDirectory(from path: AbsolutePath) throws -> AbsolutePath? - func locateProvisioningProfiles(from path: AbsolutePath) throws -> [AbsolutePath] - func locateUnencryptedCertificates(from path: AbsolutePath) throws -> [AbsolutePath] - func locateEncryptedCertificates(from path: AbsolutePath) throws -> [AbsolutePath] - func locateUnencryptedPrivateKeys(from path: AbsolutePath) throws -> [AbsolutePath] - func locateEncryptedPrivateKeys(from path: AbsolutePath) throws -> [AbsolutePath] -} - -final class SigningFilesLocator: SigningFilesLocating { - private let rootDirectoryLocator: RootDirectoryLocating - - init(rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator()) { - self.rootDirectoryLocator = rootDirectoryLocator - } - - func locateSigningDirectory(from path: AbsolutePath) throws -> AbsolutePath? { - guard let rootDirectory = rootDirectoryLocator.locate(from: path) - else { return nil } - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return FileHandler.shared.exists(signingDirectory) ? signingDirectory : nil - } - - func locateProvisioningProfiles(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateSigningFiles(at: path) - .filter { $0.extension == "mobileprovision" || $0.extension == "provisionprofile" } - } - - func locateUnencryptedCertificates(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateSigningFiles(at: path) - .filter { $0.extension == "cer" } - } - - func locateEncryptedCertificates(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateSigningFiles(at: path) - .filter { $0.pathString.hasSuffix("cer.encrypted") } - } - - func locateUnencryptedPrivateKeys(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateSigningFiles(at: path) - .filter { $0.extension == "p12" } - } - - func locateEncryptedPrivateKeys(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateSigningFiles(at: path) - .filter { $0.pathString.hasSuffix("p12.encrypted") } - } - - // MARK: - Helpers - - private func locateSigningFiles(at path: AbsolutePath) throws -> [AbsolutePath] { - guard let rootDirectory = rootDirectoryLocator.locate(from: path) - else { return [] } - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return FileHandler.shared.glob(signingDirectory, glob: "*") - } -} diff --git a/Sources/TuistSigning/SigningInstaller.swift b/Sources/TuistSigning/SigningInstaller.swift deleted file mode 100644 index 9794d939b64..00000000000 --- a/Sources/TuistSigning/SigningInstaller.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistSupport - -extension LintingIssue { - static func noFileExtension(_ path: AbsolutePath) -> Self { - Self(reason: "Unable to parse extension from file at \(path.pathString)", severity: .error) - } - - static func expiredProvisioningProfile(_ profile: ProvisioningProfile) -> Self { - Self( - reason: "The provisioning profile \(profile.name) has expired. Bear in mind that attempting to export or run the app on a device might lead to an error.", - severity: .warning - ) - } -} - -/// Handles installing for signing (provisioning profiles, certificates ...) -protocol SigningInstalling { - func installProvisioningProfile(_ provisioningProfile: ProvisioningProfile) throws -> [LintingIssue] - /// Installs certificate to a given keychain - /// - Parameters: - /// - certificate: Certificate to be installed - /// - keychainPath: Path to keychain where the certificate should be installed to - func installCertificate(_ certificate: Certificate, keychainPath: AbsolutePath) throws -} - -final class SigningInstaller: SigningInstalling { - private let securityController: SecurityControlling - - init(securityController: SecurityControlling = SecurityController()) { - self.securityController = securityController - } - - func installProvisioningProfile(_ provisioningProfile: ProvisioningProfile) throws -> [LintingIssue] { - var issues = [LintingIssue]() - - if provisioningProfile.expirationDate < Date() { - issues.append(.expiredProvisioningProfile(provisioningProfile)) - } - - let provisioningProfilesPath = FileHandler.shared.homeDirectory - // swiftlint:disable:next force_try - .appending(try! RelativePath(validating: "Library/MobileDevice/Provisioning Profiles")) - if !FileHandler.shared.exists(provisioningProfilesPath) { - try FileHandler.shared.createFolder(provisioningProfilesPath) - } - let provisioningProfileSourcePath = provisioningProfile.path - guard let profileExtension = provisioningProfileSourcePath.extension - else { - issues.append(.noFileExtension(provisioningProfileSourcePath)) - return issues - } - - let provisioningProfilePath = provisioningProfilesPath - .appending(component: provisioningProfile.uuid + "." + profileExtension) - if FileHandler.shared.exists(provisioningProfilePath) { - try FileHandler.shared.delete(provisioningProfilePath) - } - try FileHandler.shared.copy( - from: provisioningProfileSourcePath, - to: provisioningProfilePath - ) - - logger - .debug( - "Installed provisioning profile \(provisioningProfileSourcePath.pathString) to \(provisioningProfilePath.pathString)" - ) - - return issues - } - - func installCertificate(_ certificate: Certificate, keychainPath: AbsolutePath) throws { - try securityController.importCertificate(certificate, keychainPath: keychainPath) - logger - .debug( - "Installed certificate with public key at \(certificate.publicKey.pathString) and private key at \(certificate.privateKey.pathString) to keychain at \(keychainPath.pathString)" - ) - } -} diff --git a/Sources/TuistSigning/SigningInteractor.swift b/Sources/TuistSigning/SigningInteractor.swift deleted file mode 100644 index 4b48251951e..00000000000 --- a/Sources/TuistSigning/SigningInteractor.swift +++ /dev/null @@ -1,138 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport - -/// Interacts with signing -public protocol SigningInteracting { - /// Install signing for a given graph - func install(graphTraverser: GraphTraversing) throws -> [LintingIssue] -} - -public final class SigningInteractor: SigningInteracting { - private let signingFilesLocator: SigningFilesLocating - private let rootDirectoryLocator: RootDirectoryLocating - private let signingMatcher: SigningMatching - private let signingInstaller: SigningInstalling - private let signingLinter: SigningLinting - private let securityController: SecurityControlling - private let signingCipher: SigningCiphering - - public convenience init() { - self.init( - signingFilesLocator: SigningFilesLocator(), - rootDirectoryLocator: RootDirectoryLocator(), - signingMatcher: SigningMatcher(), - signingInstaller: SigningInstaller(), - signingLinter: SigningLinter(), - securityController: SecurityController(), - signingCipher: SigningCipher() - ) - } - - init( - signingFilesLocator: SigningFilesLocating, - rootDirectoryLocator: RootDirectoryLocating, - signingMatcher: SigningMatching, - signingInstaller: SigningInstalling, - signingLinter: SigningLinting, - securityController: SecurityControlling, - signingCipher: SigningCiphering - ) { - self.signingFilesLocator = signingFilesLocator - self.rootDirectoryLocator = rootDirectoryLocator - self.signingMatcher = signingMatcher - self.signingInstaller = signingInstaller - self.signingLinter = signingLinter - self.securityController = securityController - self.signingCipher = signingCipher - } - - public func install(graphTraverser: GraphTraversing) throws -> [LintingIssue] { - let entryPath = graphTraverser.path - guard let signingDirectory = try signingFilesLocator.locateSigningDirectory(from: entryPath), - let derivedDirectory = rootDirectoryLocator.locate(from: entryPath)? - .appending(component: Constants.DerivedDirectory.name) - else { return [] } - - let keychainPath = derivedDirectory.appending(component: Constants.DerivedDirectory.signingKeychain) - - let masterKey = try signingCipher.readMasterKey(at: signingDirectory) - try FileHandler.shared.createFolder(derivedDirectory) - if !FileHandler.shared.exists(keychainPath) { - try securityController.createKeychain(at: keychainPath, password: masterKey) - } - try securityController.unlockKeychain(at: keychainPath, password: masterKey) - defer { try? securityController.lockKeychain(at: keychainPath, password: masterKey) } - - try signingCipher.decryptSigning(at: entryPath, keepFiles: true) - defer { try? signingCipher.encryptSigning(at: entryPath, keepFiles: false) } - - let (certificates, provisioningProfiles) = try signingMatcher.match(from: graphTraverser.path) - - return try graphTraverser.allTargets().sorted().flatMap { target in - try install( - target: target, - keychainPath: keychainPath, - certificates: certificates, - provisioningProfiles: provisioningProfiles - ) - } - } - - // MARK: - Helpers - - private func install( - target: GraphTarget, - keychainPath: AbsolutePath, - certificates: [Fingerprint: Certificate], - provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]] - ) throws -> [LintingIssue] { - let targetConfigurations = target.target.settings?.configurations ?? [:] - /// Filtering certificate-provisioning profile pairs, so they are installed only when necessary (they correspond to some - /// configuration and target in the project) - let signingPairs = Set( - targetConfigurations - .merging( - target.project.settings.configurations, - uniquingKeysWith: { config, _ in config } - ) - .keys - ) - .compactMap { configuration -> (certificate: Certificate, provisioningProfile: ProvisioningProfile)? in - guard let provisioningProfile = provisioningProfiles[target.target.name]?[configuration.name], - let certificate = certificates.first(for: provisioningProfile) - else { - return nil - } - return (certificate: certificate, provisioningProfile: provisioningProfile) - } - - for signingPair in signingPairs.map(\.certificate) { - try signingInstaller.installCertificate(signingPair, keychainPath: keychainPath) - } - - let provisioningProfileInstallLintIssues = try signingPairs.map(\.provisioningProfile) - .flatMap(signingInstaller.installProvisioningProfile) - try provisioningProfileInstallLintIssues.printAndThrowErrorsIfNeeded() - - let provisioningProfileLintIssues = signingPairs.map(\.provisioningProfile).flatMap { - signingLinter.lint(provisioningProfile: $0, target: target.target) - } - try provisioningProfileLintIssues.printAndThrowErrorsIfNeeded() - - let signingPairLintIssues = signingPairs.flatMap(signingLinter.lint) - try signingPairLintIssues.printAndThrowErrorsIfNeeded() - - let certificateLintIssues = signingPairs.map(\.certificate).flatMap(signingLinter.lint) - try certificateLintIssues.printAndThrowErrorsIfNeeded() - - return [ - provisioningProfileInstallLintIssues, - provisioningProfileLintIssues, - signingPairLintIssues, - certificateLintIssues, - ].flatMap { $0 } - } -} diff --git a/Sources/TuistSigning/SigningMatcher.swift b/Sources/TuistSigning/SigningMatcher.swift deleted file mode 100644 index 2fc938e8b83..00000000000 --- a/Sources/TuistSigning/SigningMatcher.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore - -typealias Fingerprint = String -typealias TargetName = String -typealias ConfigurationName = String - -/// Matching signing artifacts -protocol SigningMatching { - /// - Returns: Certificates and provisioning profiles matched with their configuration and target - /// - Warning: Expects certificates and provisioning profiles already decrypted - func match(from path: AbsolutePath) throws -> ( - certificates: [Fingerprint: Certificate], - provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]] - ) -} - -final class SigningMatcher: SigningMatching { - private let signingFilesLocator: SigningFilesLocating - private let provisioningProfileParser: ProvisioningProfileParsing - private let certificateParser: CertificateParsing - - init( - signingFilesLocator: SigningFilesLocating = SigningFilesLocator(), - provisioningProfileParser: ProvisioningProfileParsing = ProvisioningProfileParser(), - certificateParser: CertificateParsing = CertificateParser() - ) { - self.signingFilesLocator = signingFilesLocator - self.provisioningProfileParser = provisioningProfileParser - self.certificateParser = certificateParser - } - - func match(from path: AbsolutePath) throws -> ( - certificates: [Fingerprint: Certificate], - provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]] - ) { - let certificateFiles = try signingFilesLocator.locateUnencryptedCertificates(from: path) - .sorted() - let privateKeyFiles = try signingFilesLocator.locateUnencryptedPrivateKeys(from: path) - .sorted() - let certificates: [Fingerprint: Certificate] = try zip(certificateFiles, privateKeyFiles) - .map(certificateParser.parse) - .reduce(into: [:]) { dict, certificate in - dict[certificate.fingerprint] = certificate - } - - let provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]] = try signingFilesLocator - .locateProvisioningProfiles(from: path) - .map(provisioningProfileParser.parse) - .reduce(into: [:]) { dict, profile in - var currentTargetDict = dict[profile.targetName] ?? [:] - currentTargetDict[profile.configurationName] = profile - dict[profile.targetName] = currentTargetDict - } - - return (certificates: certificates, provisioningProfiles: provisioningProfiles) - } -} diff --git a/Sources/TuistSigningTesting/Extensions/Certificate+TestData.swift b/Sources/TuistSigningTesting/Extensions/Certificate+TestData.swift deleted file mode 100644 index 8be3643efec..00000000000 --- a/Sources/TuistSigningTesting/Extensions/Certificate+TestData.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistSigning - -extension Certificate { - static func test( - publicKey: AbsolutePath = try! AbsolutePath(validating: "/"), // swiftlint:disable:this force_try - privateKey: AbsolutePath = try! AbsolutePath(validating: "/"), // swiftlint:disable:this force_try - fingerprint: String = "", - developmentTeam: String = "", - name: String = "", - isRevoked: Bool = false - ) -> Certificate { - Certificate( - publicKey: publicKey, - privateKey: privateKey, - fingerprint: fingerprint, - developmentTeam: developmentTeam, - name: name, - isRevoked: isRevoked - ) - } -} diff --git a/Sources/TuistSigningTesting/Extensions/ProvisioningProfile+TestData.swift b/Sources/TuistSigningTesting/Extensions/ProvisioningProfile+TestData.swift deleted file mode 100644 index 091e916c114..00000000000 --- a/Sources/TuistSigningTesting/Extensions/ProvisioningProfile+TestData.swift +++ /dev/null @@ -1,36 +0,0 @@ -import TSCBasic -import XCTest -@testable import TuistSigning - -extension ProvisioningProfile { - public static func test( - // swiftlint:disable:next force_try - path: AbsolutePath = try! AbsolutePath(validating: "/targetName.configurationName.mobileprovision"), - name: String = "name", - targetName: String = "targetName", - configurationName: String = "configurationName", - uuid: String = "uuid", - teamId: String = "teamId", - appId: String = "appId", - appIdName: String = "appIdName", - applicationIdPrefix: [String] = [], - platforms: [String] = ["iOS"], - expirationDate: Date = Date().addingTimeInterval(100), - developerCertificateFingerprints: [String] = ["developerCertificateFingerprint"] - ) -> ProvisioningProfile { - ProvisioningProfile( - path: path, - name: name, - targetName: targetName, - configurationName: configurationName, - uuid: uuid, - teamId: teamId, - appId: appId, - appIdName: appIdName, - applicationIdPrefix: applicationIdPrefix, - platforms: platforms, - expirationDate: expirationDate, - developerCertificateFingerprints: developerCertificateFingerprints - ) - } -} diff --git a/Sources/TuistSigningTesting/Mocks/MockSigningCipher.swift b/Sources/TuistSigningTesting/Mocks/MockSigningCipher.swift deleted file mode 100644 index 17e193925d8..00000000000 --- a/Sources/TuistSigningTesting/Mocks/MockSigningCipher.swift +++ /dev/null @@ -1,21 +0,0 @@ -import TSCBasic -@testable import TuistSigning - -public final class MockSigningCipher: SigningCiphering { - public init() {} - - public var readMasterKeyStub: ((AbsolutePath) throws -> String)? - public func readMasterKey(at path: AbsolutePath) throws -> String { - try readMasterKeyStub?(path) ?? "" - } - - public var decryptSigningStub: ((AbsolutePath, Bool) throws -> Void)? - public func decryptSigning(at path: AbsolutePath, keepFiles: Bool) throws { - try decryptSigningStub?(path, keepFiles) - } - - public var encryptSigningStub: ((AbsolutePath, Bool) throws -> Void)? - public func encryptSigning(at path: AbsolutePath, keepFiles: Bool) throws { - try encryptSigningStub?(path, keepFiles) - } -} diff --git a/Sources/TuistSigningTesting/Mocks/MockSigningInstaller.swift b/Sources/TuistSigningTesting/Mocks/MockSigningInstaller.swift deleted file mode 100644 index 6d4f68ab2a1..00000000000 --- a/Sources/TuistSigningTesting/Mocks/MockSigningInstaller.swift +++ /dev/null @@ -1,17 +0,0 @@ -import TSCBasic -import TuistCore -@testable import TuistSigning - -public final class MockSigningInstaller: SigningInstalling { - public init() {} - - var installProvisioningProfileStub: ((ProvisioningProfile) throws -> [LintingIssue])? - public func installProvisioningProfile(_ provisioningProfile: ProvisioningProfile) throws -> [LintingIssue] { - try installProvisioningProfileStub?(provisioningProfile) ?? [] - } - - var installCertificateStub: ((Certificate, AbsolutePath) throws -> Void)? - public func installCertificate(_ certificate: Certificate, keychainPath: AbsolutePath) throws { - try installCertificateStub?(certificate, keychainPath) - } -} diff --git a/Sources/TuistSupport/Atomic.swift b/Sources/TuistSupport/Atomic.swift deleted file mode 100644 index df369b84317..00000000000 --- a/Sources/TuistSupport/Atomic.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -/// Ensures that writing and reading from property annotated with this property wrapper is thread safe -/// Taken from https://www.onswiftwings.com/posts/atomic-property-wrapper/ -@propertyWrapper -public class Atomic { - private var value: Value - private let lock = NSLock() - - public init(wrappedValue value: Value) { - self.value = value - } - - public var wrappedValue: Value { - get { load() } - set { store(newValue: newValue) } - } - - public func modify(_ accessBlock: (inout Value) -> Void) { - lock.lock() - defer { lock.unlock() } - accessBlock(&value) - } - - private func load() -> Value { - lock.lock() - defer { lock.unlock() } - return value - } - - private func store(newValue: Value) { - modify { - $0 = newValue - } - } -} diff --git a/Sources/TuistSupport/Constants.swift b/Sources/TuistSupport/Constants.swift index fa14b81692f..d4960daf7a8 100644 --- a/Sources/TuistSupport/Constants.swift +++ b/Sources/TuistSupport/Constants.swift @@ -1,6 +1,10 @@ import Foundation public enum Constants { + // NOTE: We expect tuist-cloud to set the value before running the CLI + // public static var version: String! = "x.y.z" + public static var version: String! = "4.24.0.1-runtastic" + public static let versionFileName = ".tuist-version" public static let binFolderName = ".tuist-bin" public static let binName = "tuist" @@ -8,11 +12,10 @@ 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.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"] public static let tuistDirectoryName: String = "Tuist" + public static let resultBundleName = "result-bundle" public static let helpersDirectoryName: String = "ProjectDescriptionHelpers" public static let signingDirectoryName: String = "Signing" @@ -28,31 +31,25 @@ public enum Constants { public static let tuistGeneratedFileName = ".tuist-generated" /// The cache version. - /// This should change only when it changes the logic to map a `TuistGraph.Target` to a cached build artifact. + /// This should change only when it changes the logic to map a `XcodeGraph.Target` to a cached build artifact. /// Changing this results in changing the target hash and hence forcing a rebuild of its artifact. public static let cacheVersion = "1.0.0" - public enum DependenciesDirectory { - public static let dependenciesFileName = "Dependencies.swift" - public static let name = "Dependencies" - public static let graphName = "graph.json" - public static let lockfilesDirectoryName = "Lockfiles" - public static let cartfileName = "Cartfile" - public static let cartfileResolvedName = "Cartfile.resolved" + public enum SwiftPackageManager { public static let packageSwiftName = "Package.swift" public static let packageResolvedName = "Package.resolved" public static let packageBuildDirectoryName = ".build" - public static let carthageDirectoryName = "Carthage" - public static let swiftPackageManagerDirectoryName = "SwiftPackageManager" } public enum DerivedDirectory { public static let name = "Derived" public static let infoPlists = "InfoPlists" public static let entitlements = "Entitlements" + public static let privacyManifest = "PrivacyManifests" public static let moduleMaps = "ModuleMaps" public static let sources = "Sources" public static let signingKeychain = "signing.keychain" + public static let dependenciesDerivedDirectory = "tuist-derived" } public enum AsyncQueue { @@ -64,9 +61,7 @@ public enum Constants { /// But only eg. for acceptance tests and other cases needed internally public enum EnvironmentVariables { public static let verbose = "TUIST_CONFIG_VERBOSE" - public static let colouredOutput = "TUIST_CONFIG_COLOURED_OUTPUT" public static let versionsDirectory = "TUIST_CONFIG_VERSIONS_DIRECTORY" - 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 cacheManifests = "TUIST_CONFIG_CACHE_MANIFESTS" @@ -76,5 +71,12 @@ 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" + public static let token = "TUIST_CONFIG_TOKEN" + @available(*, deprecated, message: "Use `token` instead") + public static let deprecatedToken = "TUIST_CONFIG_CLOUD_TOKEN" + } + + public enum URLs { + public static let production = URL(string: "https://cloud.tuist.io")! } } diff --git a/Sources/TuistSupport/CoreDataVersionExtractor.swift b/Sources/TuistSupport/CoreDataVersionExtractor.swift index a0450676021..0076edc04e8 100644 --- a/Sources/TuistSupport/CoreDataVersionExtractor.swift +++ b/Sources/TuistSupport/CoreDataVersionExtractor.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path /// Extract version from .xccurrentversion file. public enum CoreDataVersionExtractor { diff --git a/Sources/TuistSupport/Credentials/CredentialsStore.swift b/Sources/TuistSupport/Credentials/CredentialsStore.swift index 0887e4b168d..6a44dd4fe4c 100644 --- a/Sources/TuistSupport/Credentials/CredentialsStore.swift +++ b/Sources/TuistSupport/Credentials/CredentialsStore.swift @@ -1,6 +1,8 @@ import Foundation import KeychainAccess +import Mockable +@Mockable public protocol CredentialsStoring { /// It stores the credentials for the server with the given URL. /// - Parameters: @@ -28,7 +30,7 @@ enum CredentialsStoreError: FatalError { var description: String { switch self { case .credentialsNotFound: - return "You are not authenticated. Authenticate by running `tuist cloud auth`." + return "You are not authenticated. Authenticate by running `tuist auth`." } } diff --git a/Sources/TuistSupport/Errors/FatalError.swift b/Sources/TuistSupport/Errors/FatalError.swift index 248642f807f..606ab3213d3 100644 --- a/Sources/TuistSupport/Errors/FatalError.swift +++ b/Sources/TuistSupport/Errors/FatalError.swift @@ -6,7 +6,7 @@ import Foundation /// - bug: error thrown when a bug is found and the execution cannot continue. /// - abortSilent: like abort but without printing anything to the user. /// - bugSilent: like bug but without printing anything to the user. -public enum ErrorType { +public enum ErrorType: Sendable { case abort case bug case abortSilent diff --git a/Sources/TuistSupport/Extensions/AbsolutePath+Extras.swift b/Sources/TuistSupport/Extensions/AbsolutePath+Extras.swift index 7283fde0973..0727600b185 100644 --- a/Sources/TuistSupport/Extensions/AbsolutePath+Extras.swift +++ b/Sources/TuistSupport/Extensions/AbsolutePath+Extras.swift @@ -1,6 +1,6 @@ import Darwin import Foundation -import TSCBasic +import Path import UniformTypeIdentifiers let systemGlob = Darwin.glob diff --git a/Sources/TuistSupport/Extensions/AnyPublisher+Extras.swift b/Sources/TuistSupport/Extensions/AnyPublisher+Extras.swift deleted file mode 100644 index c0654fa8170..00000000000 --- a/Sources/TuistSupport/Extensions/AnyPublisher+Extras.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Combine -import CombineExt - -extension AnyPublisher { - public init(value: Output) { - self = AnyPublisher.create { subscriber in - subscriber.send(value) - subscriber.send(completion: .finished) - return AnyCancellable {} - } - } - - public init(error: Failure) { - self = AnyPublisher.create { subscriber in - subscriber.send(completion: .failure(error)) - return AnyCancellable {} - } - } - - public init(result: Result) { - self = AnyPublisher.create { subscriber in - switch result { - case let .success(value): - subscriber.send(value) - subscriber.send(completion: .finished) - case let .failure(error): - subscriber.send(completion: .failure(error)) - } - return AnyCancellable {} - } - } -} diff --git a/Sources/TuistSupport/Extensions/Array+ExecutionContext.swift b/Sources/TuistSupport/Extensions/Array+ExecutionContext.swift index dfc5a8bcffe..e58dbc93803 100644 --- a/Sources/TuistSupport/Extensions/Array+ExecutionContext.swift +++ b/Sources/TuistSupport/Extensions/Array+ExecutionContext.swift @@ -1,6 +1,6 @@ import Foundation -extension Array { +extension Array where Element: Sendable { /// Map (with execution context) /// /// - Parameters: @@ -19,7 +19,7 @@ extension Array { /// /// - Parameters: /// - transform: The transformation closure to apply to the array - public func concurrentMap(_ transform: @escaping (Element) async throws -> B) async throws -> [B] { + public func concurrentMap(_ transform: @Sendable @escaping (Element) async throws -> B) async throws -> [B] { let tasks = map { element in Task { try await transform(element) @@ -50,7 +50,9 @@ extension Array { /// /// - Parameters: /// - transform: The transformation closure to apply to the array - public func concurrentCompactMap(_ transform: @escaping (Element) async throws -> B?) async throws -> [B] { + public func concurrentCompactMap(_ transform: @Sendable @escaping (Element) async throws -> B?) async throws + -> [B] + { let tasks = map { element in Task { try await transform(element) @@ -78,6 +80,22 @@ extension Array { return try concurrentForEach(perform) } } + + /// For Each (with execution context) + /// + /// - Parameters: + /// - context: The execution context to perform the `perform` operation with + /// - perform: The perform closure to call on each element in the array + public func forEach(context: ExecutionContext, _ perform: @escaping (Element) async throws -> Void) async rethrows { + switch context.executionType { + case .serial: + for item in self { + try await perform(item) + } + case .concurrent: + return try await concurrentForEach(perform) + } + } } // MARK: - Private @@ -87,35 +105,17 @@ extension Array { // based on https://talk.objc.io/episodes/S01E90-concurrent-map // extension Array { - private final class ThreadSafe { - private var _value: A - private let queue = DispatchQueue(label: "ThreadSafe") - init(_ value: A) { - _value = value - } - - var value: A { - queue.sync { _value } - } - - func atomically(_ transform: @escaping (inout A) -> Void) { - queue.async { - transform(&self._value) - } - } - } - private func concurrentMap(_ transform: (Element) throws -> B) rethrows -> [B] { let result = ThreadSafe([Result?](repeating: nil, count: count)) DispatchQueue.concurrentPerform(iterations: count) { idx in let element = self[idx] do { let transformed = try transform(element) - result.atomically { + result.mutate { $0[idx] = .success(transformed) } } catch { - result.atomically { + result.mutate { $0[idx] = .failure(error) } } @@ -129,11 +129,11 @@ extension Array { let element = self[idx] do { guard let transformed = try transform(element) else { return } - result.atomically { + result.mutate { $0[idx] = .success(transformed) } } catch { - result.atomically { + result.mutate { $0[idx] = .failure(error) } } @@ -148,7 +148,7 @@ extension Array { do { try perform(element) } catch { - result.atomically { + result.mutate { $0[idx] = error } } @@ -157,4 +157,27 @@ extension Array { throw $0 } } + + private func concurrentForEach(_ perform: @escaping (Element) async throws -> Void) async rethrows { + let result = ThreadSafe([Error?](repeating: nil, count: count)) + + await withTaskGroup(of: Void.self) { group in + for idx in 0 ..< count { + group.addTask { + let element = self[idx] + do { + try await perform(element) + } catch { + result.mutate { + $0[idx] = error + } + } + } + } + } + + try result.value.compactMap { $0 }.forEach { + throw $0 + } + } } diff --git a/Sources/TuistSupport/Extensions/Dictionary+ExecutionContext.swift b/Sources/TuistSupport/Extensions/Dictionary+ExecutionContext.swift index 3596f5e7af0..2d2190ebb86 100644 --- a/Sources/TuistSupport/Extensions/Dictionary+ExecutionContext.swift +++ b/Sources/TuistSupport/Extensions/Dictionary+ExecutionContext.swift @@ -1,4 +1,4 @@ -extension Dictionary { +extension Dictionary where Key: Sendable, Value: Sendable { /// Map (with execution context) /// /// - Parameters: @@ -13,7 +13,7 @@ extension Dictionary { /// /// - Parameters: /// - transform: The transformation closure to apply to the dictionary - public func concurrentMap(_ transform: @escaping (Key, Value) async throws -> B) async throws -> [B] { + public func concurrentMap(_ transform: @Sendable @escaping (Key, Value) async throws -> B) async throws -> [B] { try await map { ($0.key, $0.value) } .concurrentMap(transform) } diff --git a/Sources/TuistSupport/Extensions/FileManager+Extras.swift b/Sources/TuistSupport/Extensions/FileManager+Extras.swift index 0fb4f4a6517..75e1bdf6e6f 100644 --- a/Sources/TuistSupport/Extensions/FileManager+Extras.swift +++ b/Sources/TuistSupport/Extensions/FileManager+Extras.swift @@ -5,9 +5,23 @@ extension FileManager { subdirectoriesResolvingSymbolicLinks(atNestedPath: nil, basePath: path) } - private func subdirectoriesResolvingSymbolicLinks(atNestedPath nestedPath: String?, basePath: String) -> [String] { + private func subdirectoriesResolvingSymbolicLinks( + atNestedPath nestedPath: String?, + basePath: String, + visitedPaths: inout Set + ) -> [String] { let currentLevelPath = nestedPath.map { NSString(string: basePath).appendingPathComponent($0) } ?? basePath let resolvedCurrentLevelPath = resolvingSymbolicLinks(path: currentLevelPath) + let standardizedPath = NSString(string: resolvedCurrentLevelPath).standardizingPath + + // Check if we've already visited this standardized path + guard !visitedPaths.contains(standardizedPath) else { + return [] + } + + // Mark this standardized path as visited + visitedPaths.insert(standardizedPath) + guard let resolvedSubpathsFromCurrentRoot = try? subpathsOfDirectory(atPath: resolvedCurrentLevelPath) else { return [] @@ -21,7 +35,11 @@ extension FileManager { if isSymbolicLinkToDirectory(path: completeSubpath) { resolvedSubpaths.append(relativeSubpath) resolvedSubpaths.append( - contentsOf: subdirectoriesResolvingSymbolicLinks(atNestedPath: relativeSubpath, basePath: basePath) + contentsOf: subdirectoriesResolvingSymbolicLinks( + atNestedPath: relativeSubpath, + basePath: basePath, + visitedPaths: &visitedPaths + ) ) } else if isDirectory(path: completeSubpath) { resolvedSubpaths.append(relativeSubpath) @@ -31,6 +49,11 @@ extension FileManager { return resolvedSubpaths } + private func subdirectoriesResolvingSymbolicLinks(atNestedPath nestedPath: String?, basePath: String) -> [String] { + var visitedPaths: Set = .init() + return subdirectoriesResolvingSymbolicLinks(atNestedPath: nestedPath, basePath: basePath, visitedPaths: &visitedPaths) + } + private func isSymbolicLinkToDirectory(path: String) -> Bool { let pathResolvingSymbolicLinks = resolvingSymbolicLinks(path: path) return pathResolvingSymbolicLinks != path && isDirectory(path: pathResolvingSymbolicLinks) diff --git a/Sources/TuistSupport/Extensions/Publisher+Blocking.swift b/Sources/TuistSupport/Extensions/Publisher+Blocking.swift deleted file mode 100644 index 20140dbea5f..00000000000 --- a/Sources/TuistSupport/Extensions/Publisher+Blocking.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Combine -import Foundation - -extension Publisher { - /// It blocks the current thread until the publisher finishes. - /// - Parameter timeout: Timeout - /// - Throws: Error thrown by the publisher or as a result of a time out caused by the publishing not finishing in time. - /// - Returns: List of events sent through the publisher. - public func toBlocking(timeout: DispatchTime = .now() + .seconds(10)) throws -> [Output] { - let semaphore = DispatchSemaphore(value: 0) - var values: [Output] = [] - var error: Error? - var cancellables: Set = Set() - let synchronizationQueue = DispatchQueue(label: "io.tuist.support.blocking-publisher") - - sink { completion in - switch completion { - // swiftlint:disable:next identifier_name - case let .failure(_error): - synchronizationQueue.async { - error = _error - semaphore.signal() - } - default: - synchronizationQueue.async { - semaphore.signal() - } - } - } receiveValue: { value in - synchronizationQueue.async { - values.append(value) - } - } - .store(in: &cancellables) - - _ = semaphore.wait(timeout: timeout) - - // By calling cancellables at this point we - // prevent ARC from releasing the set from memory - // and causing the publisher to be cancelled - cancellables.removeAll() - - return try synchronizationQueue.sync { () throws -> [Output] in - if let error { throw error } - return values - } - } -} diff --git a/Sources/TuistSupport/Extensions/String+Extras.swift b/Sources/TuistSupport/Extensions/String+Extras.swift index c94979bc386..bbaa4fe0ff6 100644 --- a/Sources/TuistSupport/Extensions/String+Extras.swift +++ b/Sources/TuistSupport/Extensions/String+Extras.swift @@ -141,6 +141,11 @@ extension String { return sanitized } + /// Makes the string to be available in the `Bundle Identifier`. + public func toValidInBundleIdentifier() -> String { + return replacingOccurrences(of: "[^a-zA-Z0-9.-]", with: "-", options: .regularExpression) + } + public func camelCaseToKebabCase() -> String { convertCamelCase(separator: "-") } @@ -224,6 +229,21 @@ extension String { return true } } + + /// Encloses the current string inside quotes if it contains spaces + public var quotedIfContainsSpaces: String { + if contains(" ") { + return "\"\(self)\"" + } else { + return self + } + } + + /// Formats the current string to a Uniform Type Identifier + public var sanitizedModuleName: String { + replacingOccurrences(of: "-", with: "_") + .replacingOccurrences(of: "/", with: "_") + } } extension Array where Element: CustomStringConvertible { diff --git a/Sources/TuistSupport/Extensions/TSCBasic+Extras.swift b/Sources/TuistSupport/Extensions/TSCBasic+Extras.swift index eb3a6a734c1..b1cbf15914c 100644 --- a/Sources/TuistSupport/Extensions/TSCBasic+Extras.swift +++ b/Sources/TuistSupport/Extensions/TSCBasic+Extras.swift @@ -1,9 +1,10 @@ +import Path import TSCBasic -public func withTemporaryDirectories(body: (AbsolutePath, AbsolutePath) throws -> Result) throws -> Result { +public func withTemporaryDirectories(body: (Path.AbsolutePath, Path.AbsolutePath) throws -> Result) throws -> Result { try withTemporaryDirectory { tempDirOne in try withTemporaryDirectory { tempDirTwo in - try body(tempDirOne, tempDirTwo) + try body(.init(validating: tempDirOne.pathString), .init(validating: tempDirTwo.pathString)) } } } diff --git a/Sources/TuistSupport/Extensions/Task+Retry.swift b/Sources/TuistSupport/Extensions/Task+Retry.swift new file mode 100644 index 00000000000..bf7a631dea5 --- /dev/null +++ b/Sources/TuistSupport/Extensions/Task+Retry.swift @@ -0,0 +1,25 @@ +import Foundation + +// Inspired by https://www.swiftbysundell.com/articles/retrying-an-async-swift-task/ + +extension Task where Failure == Error { + @discardableResult + public static func retrying( + priority: TaskPriority? = nil, + maxRetryCount: Int = 3, + operation: @Sendable @escaping () async throws -> Success + ) -> Task { + Task(priority: priority) { + for _ in 0 ..< maxRetryCount { + do { + return try await operation() + } catch { + continue + } + } + + try Task.checkCancellation() + return try await operation() + } + } +} diff --git a/Sources/TuistSupport/HTTP/FileClient.swift b/Sources/TuistSupport/HTTP/FileClient.swift index 8ff9f14b81f..99d0e55d1b3 100644 --- a/Sources/TuistSupport/HTTP/FileClient.swift +++ b/Sources/TuistSupport/HTTP/FileClient.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path enum FileClientError: LocalizedError, FatalError { case urlSessionError(URLRequest, Error, AbsolutePath?) diff --git a/Sources/TuistSupport/HTTP/HTTPRequestDispatcher.swift b/Sources/TuistSupport/HTTP/HTTPRequestDispatcher.swift deleted file mode 100644 index 9e9a25dad88..00000000000 --- a/Sources/TuistSupport/HTTP/HTTPRequestDispatcher.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Combine -import Foundation - -public enum HTTPRequestDispatcherError: LocalizedError, FatalError { - case urlSessionError(URLRequest, Error) - case parseError(URLRequest, Error) - case invalidResponse(URLRequest) - case serverSideError(URLRequest, Error, HTTPURLResponse) - - // MARK: - LocalizedError - - public var errorDescription: String? { description } - - // MARK: - FatalError - - public var description: String { - switch self { - case let .urlSessionError(request, error): - return "Received a session error when performing \(request.descriptionForError): \(error.localizedDescription)" - case let .parseError(request, error): - return "Error parsing the network response of \(request.descriptionForError): \(error.localizedDescription)" - case let .invalidResponse(request): - return "Received unexpected response from the network when performing \(request.descriptionForError)" - case let .serverSideError(request, error, response): - return """ - Error returned by the server when performing \(request.descriptionForError): - - URL: \(response.url!.absoluteString) - - Code: \(response.statusCode) - - Description: \(error.localizedDescription) - """ - } - } - - public var type: ErrorType { - switch self { - case .urlSessionError: return .bug - case .parseError: return .abort - case .invalidResponse: return .bug - case .serverSideError: return .bug - } - } -} - -public protocol HTTPRequestDispatching { - func dispatch(resource: HTTPResource) async throws -> (object: T, response: HTTPURLResponse) -} - -public final class HTTPRequestDispatcher: HTTPRequestDispatching { - let session: URLSession - - public init(session: URLSession = URLSession.shared) { - self.session = session - } - - public func dispatch(resource: HTTPResource) async throws -> (object: T, response: HTTPURLResponse) { - let request = resource.request() - do { - let (data, response) = try await session.data(for: request) - guard let response = response as? HTTPURLResponse else { - throw HTTPRequestDispatcherError.invalidResponse(request) - } - switch response.statusCode { - case 200 ..< 300: - do { - let object = try resource.parse(data, response) - return (object: object, response: response) - } catch { - throw HTTPRequestDispatcherError.parseError(request, error) - } - default: // Error - let thrownError: Error - do { - let parsedError = try resource.parseError(data, response) - thrownError = HTTPRequestDispatcherError.serverSideError(request, parsedError, response) - } catch { - thrownError = HTTPRequestDispatcherError.parseError(request, error) - } - throw thrownError - } - } catch { - if error is HTTPRequestDispatcherError { - throw error - } else { - throw HTTPRequestDispatcherError.urlSessionError(request, error) - } - } - } -} diff --git a/Sources/TuistSupport/HTTP/HTTPResource.swift b/Sources/TuistSupport/HTTP/HTTPResource.swift deleted file mode 100644 index c7059d6945e..00000000000 --- a/Sources/TuistSupport/HTTP/HTTPResource.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation - -public struct HTTPResource: Equatable, Hashable, CustomStringConvertible { - public let request: () -> URLRequest - public let parse: (Data, HTTPURLResponse) throws -> T - public let parseError: (Data, HTTPURLResponse) throws -> E - - public init( - request: @escaping () -> URLRequest, - parse: @escaping (Data, HTTPURLResponse) throws -> T, - parseError: @escaping (Data, HTTPURLResponse) throws -> E - ) { - self.request = request - self.parse = parse - self.parseError = parseError - } - - public func withURL(_ url: URL) -> HTTPResource { - HTTPResource(request: { - URLRequest(url: url) - }, parse: parse, parseError: parseError) - } - - public func mappingRequest(_ requestMapper: @escaping (URLRequest) throws -> URLRequest) throws -> HTTPResource { - let request = try requestMapper(request()) - return HTTPResource( - request: { request }, - parse: parse, - parseError: parseError - ) - } - - public func eraseToAnyResource() -> HTTPResource { - HTTPResource { - request() - } parse: { data, response in - try parse(data, response) as Any - } parseError: { data, response in - try parseError(data, response) - } - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(request()) - } - - // MARK: - Equatable - - public static func == (lhs: HTTPResource, rhs: HTTPResource) -> Bool { - lhs.request() == rhs.request() - } - - // MARK: - CustomStringConvertible - - public var description: String { - let request = request() - - return "[\(request.httpMethod ?? "GET")] - \(request.url?.path ?? "")" - } -} - -extension HTTPResource where T: Decodable, E: Decodable { - public static func jsonResource(request: @escaping () -> URLRequest) -> HTTPResource { - let jsonDecoder = JSONDecoder() - jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase - return HTTPResource( - request: request, - parse: { data, _ in - try jsonDecoder.decode(T.self, from: data) - }, - parseError: { data, _ in try jsonDecoder.decode(E.self, from: data) } - ) - } - - public static func jsonResource(for url: URL, httpMethod: String) -> HTTPResource { - var request = URLRequest(url: url) - request.httpMethod = httpMethod - return .jsonResource { request } - } -} diff --git a/Sources/TuistSupport/Logging/FileLogger.swift b/Sources/TuistSupport/Logging/FileLogger.swift index 03c83106ff4..02e469dd10c 100644 --- a/Sources/TuistSupport/Logging/FileLogger.swift +++ b/Sources/TuistSupport/Logging/FileLogger.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path public struct FileLogger: TextOutputStream { enum FileHandlerOutputStream: Error { diff --git a/Sources/TuistSupport/Logging/JSONLogHandler.swift b/Sources/TuistSupport/Logging/JSONLogHandler.swift new file mode 100644 index 00000000000..a0e110eb8ec --- /dev/null +++ b/Sources/TuistSupport/Logging/JSONLogHandler.swift @@ -0,0 +1,61 @@ +import Foundation +import Logging + +public struct JSONLogHandler: LogHandler { + public var logLevel: Logger.Level + + public let label: String + + public init(label: String) { + self.init(label: label, logLevel: .info) + } + + public init(label: String, logLevel: Logger.Level) { + self.label = label + self.logLevel = logLevel + } + + public func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + file _: String, function _: String, line _: UInt + ) { + let string: String + + switch metadata?[Logger.Metadata.tuist] { + case Logger.Metadata.jsonKey?: + string = message.description + default: + switch level { + case .critical: + if Environment.shared.shouldOutputBeColoured { + string = message.description.bold() + } else { + string = message.description + } + case .error: + if Environment.shared.shouldOutputBeColoured { + string = message.description.red() + } else { + string = message.description + } + default: + return + } + } + + output(for: level).print(string) + } + + func output(for level: Logger.Level) -> FileHandle { + level < .error ? .standardOutput : .standardError + } + + public var metadata = Logger.Metadata() + + public subscript(metadataKey key: String) -> Logger.Metadata.Value? { + get { metadata[key] } + set { metadata[key] = newValue } + } +} diff --git a/Sources/TuistSupport/Logging/Logger.Metadata+Tuist.swift b/Sources/TuistSupport/Logging/Logger.Metadata+Tuist.swift index fe9782faa1a..ba2d4447459 100644 --- a/Sources/TuistSupport/Logging/Logger.Metadata+Tuist.swift +++ b/Sources/TuistSupport/Logging/Logger.Metadata+Tuist.swift @@ -20,4 +20,9 @@ extension Logger.Metadata { public static var pretty: Logger.Metadata { [tuist: .string(prettyKey)] } + + public static let jsonKey: String = "json" + public static var json: Logger.Metadata { + [tuist: .string(jsonKey)] + } } diff --git a/Sources/TuistSupport/Logging/Logger.swift b/Sources/TuistSupport/Logging/Logger.swift index 538237f7b88..cb1a5540edc 100644 --- a/Sources/TuistSupport/Logging/Logger.swift +++ b/Sources/TuistSupport/Logging/Logger.swift @@ -4,10 +4,16 @@ import class Foundation.ProcessInfo let logger = Logger(label: "io.tuist.support") public struct LoggingConfig { + public init(loggerType: LoggerType, verbose: Bool) { + self.loggerType = loggerType + self.verbose = verbose + } + public enum LoggerType { case console case detailed case osLog + case json } public var loggerType: LoggerType @@ -45,6 +51,8 @@ public enum LogOutput { handler = DetailedLogHandler.self case .console: handler = StandardLogHandler.self + case .json: + handler = JSONLogHandler.self } if config.verbose { @@ -78,3 +86,9 @@ extension OSLogHandler: VerboseLogHandler { OSLogHandler(label: label, logLevel: .debug) } } + +extension JSONLogHandler: VerboseLogHandler { + public static func verbose(label: String) -> LogHandler { + StandardLogHandler(label: label, logLevel: .debug) + } +} diff --git a/Sources/TuistSupport/Logging/OSLogHandler.swift b/Sources/TuistSupport/Logging/OSLogHandler.swift index 3cfe2606fe2..52201721881 100644 --- a/Sources/TuistSupport/Logging/OSLogHandler.swift +++ b/Sources/TuistSupport/Logging/OSLogHandler.swift @@ -1,6 +1,6 @@ import Foundation import struct Logging.Logger -import os +@preconcurrency import os public struct OSLogHandler: LogHandler { public var logLevel: Logger.Level diff --git a/Sources/TuistSupport/MachineEnvironment.swift b/Sources/TuistSupport/MachineEnvironment.swift index 12f9cda494b..0c32a3c0606 100644 --- a/Sources/TuistSupport/MachineEnvironment.swift +++ b/Sources/TuistSupport/MachineEnvironment.swift @@ -1,7 +1,9 @@ import CryptoKit import Foundation +import Mockable -public protocol MachineEnvironmentRetrieving { +@Mockable +public protocol MachineEnvironmentRetrieving: Sendable { var clientId: String { get } var macOSVersion: String { get } var swiftVersion: String { get } @@ -10,12 +12,12 @@ public protocol MachineEnvironmentRetrieving { } /// `MachineEnvironment` is a data structure that contains information about the machine executing Tuist -public class MachineEnvironment: MachineEnvironmentRetrieving { +public final class MachineEnvironment: MachineEnvironmentRetrieving { public static let shared = MachineEnvironment() private init() {} /// `clientId` is a unique anonymous hash that identifies the machine running Tuist - public lazy var clientId: String = { + public let clientId: String = { let matchingDict = IOServiceMatching("IOPlatformExpertDevice") let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, matchingDict) defer { IOObjectRelease(platformExpert) } @@ -33,20 +35,20 @@ public class MachineEnvironment: MachineEnvironmentRetrieving { }() /// The `macOSVersion` of the machine running Tuist, in the format major.minor.path, e.g: "10.15.7" - public lazy var macOSVersion = """ + public let macOSVersion = """ \(ProcessInfo.processInfo.operatingSystemVersion.majorVersion).\ \(ProcessInfo.processInfo.operatingSystemVersion.minorVersion).\ \(ProcessInfo.processInfo.operatingSystemVersion.patchVersion) """ /// The `swiftVersion` of the machine running Tuist - public lazy var swiftVersion = try! System.shared // swiftlint:disable:this force_try + public let swiftVersion = try! System.shared // swiftlint:disable:this force_try .capture(["/usr/bin/xcrun", "swift", "-version"]) .components(separatedBy: "Swift version ").last! .components(separatedBy: " ").first! /// `hardwareName` is the name of the architecture of the machine running Tuist, e.g: "arm64" or "x86_64" - public lazy var hardwareName = ProcessInfo.processInfo.machineHardwareName + public let hardwareName = ProcessInfo.processInfo.machineHardwareName /// Indicates whether Tuist is running in Continuous Integration (CI) environment public var isCI: Bool { diff --git a/Sources/TuistSupport/Models/InvalidGlob.swift b/Sources/TuistSupport/Models/InvalidGlob.swift index 51e8e795dec..216c723d122 100644 --- a/Sources/TuistSupport/Models/InvalidGlob.swift +++ b/Sources/TuistSupport/Models/InvalidGlob.swift @@ -1,7 +1,7 @@ -import TSCBasic +import Path /// A structs that represents an invalid glob pattern. -public struct InvalidGlob: Equatable, CustomStringConvertible { +public struct InvalidGlob: Equatable, CustomStringConvertible, Sendable { /// Glob patterns. public let pattern: String diff --git a/Sources/TuistSupport/SignalHandler.swift b/Sources/TuistSupport/SignalHandler.swift deleted file mode 100644 index a1e8806699d..00000000000 --- a/Sources/TuistSupport/SignalHandler.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -public typealias SigActionHandler = @convention(c) (Int32) -> Void - -/// Signal handling -public protocol SignalHandling { - /// Traps SIGINT or SIGABRT signals, and invokes the closure when received - func trap(_ action: @escaping SigActionHandler) -} - -/// Signal handler -public struct SignalHandler: SignalHandling { - public init() {} - - public func trap(_ action: @escaping SigActionHandler) { - trap(signal: SIGINT, action: action) - trap(signal: SIGABRT, action: action) - } - - private func trap(signal: Int32, action: @escaping SigActionHandler) { - var signalAction = sigaction(__sigaction_u: unsafeBitCast(action, to: __sigaction_u.self), sa_mask: 0, sa_flags: 0) - _ = withUnsafePointer(to: &signalAction) { actionPointer in - sigaction(signal, actionPointer, nil) - } - } -} diff --git a/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift b/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift index 2f1314910d6..190f6e22ed0 100644 --- a/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift +++ b/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift @@ -1,3 +1,4 @@ +import Path import ProjectDescription import TSCUtility @@ -7,6 +8,9 @@ import TSCUtility /// It decodes data encoded from Manifest.swift: https://github.com/apple/swift-package-manager/blob/06f9b30f4593940272f57f6284e5614d817d2f22/Sources/PackageModel/Manifest.swift#L372-L409 /// Fields not needed by tuist are commented out and not decoded at all. public struct PackageInfo: Hashable { + /// The name of the package. + public let name: String + /// The products declared in the manifest. public let products: [Product] @@ -27,9 +31,6 @@ public struct PackageInfo: Hashable { // Ignored fields - // /// The name of the package. - // let name: String - // /// The tools version declared in the manifest. // let toolsVersion: ToolsVersion @@ -46,6 +47,7 @@ public struct PackageInfo: Hashable { // let packageKind: PackageReference.Kind public init( + name: String, products: [Product], targets: [Target], platforms: [Platform], @@ -53,6 +55,7 @@ public struct PackageInfo: Hashable { cxxLanguageStandard: String?, swiftLanguageVersions: [TSCUtility.Version]? ) { + self.name = name self.products = products self.targets = targets self.platforms = platforms @@ -65,7 +68,7 @@ public struct PackageInfo: Hashable { // MARK: Platform extension PackageInfo { - public struct Platform: Decodable, Hashable { + public struct Platform: Codable, Hashable { public let platformName: String public let version: String public let options: [String] @@ -89,7 +92,7 @@ extension PackageInfo { // MARK: PackageConditionDescription extension PackageInfo { - public struct PackageConditionDescription: Decodable, Hashable { + public struct PackageConditionDescription: Codable, Hashable { public let platformNames: [String] public let config: String? @@ -106,7 +109,7 @@ extension PackageInfo { // MARK: - Product extension PackageInfo { - public struct Product: Decodable, Hashable { + public struct Product: Codable, Hashable { /// The name of the product. public let name: String @@ -162,7 +165,7 @@ extension PackageInfo.Product { // MARK: - Target extension PackageInfo { - public struct Target: Decodable, Hashable { + public struct Target: Codable, Hashable { private enum CodingKeys: String, CodingKey { case name, path, url, sources, packageAccess, resources, exclude, dependencies, publicHeadersPath, type, settings, checksum @@ -274,8 +277,8 @@ extension PackageInfo.Target { // MARK: Target.Resource extension PackageInfo.Target { - public struct Resource: Decodable, Hashable { - public enum Rule: String, Decodable, Hashable { + public struct Resource: Codable, Hashable { + public enum Rule: String, Codable, Hashable { case process case copy @@ -316,7 +319,7 @@ extension PackageInfo.Target { } } - public enum Localization: String, Decodable, Hashable { + public enum Localization: String, Codable, Hashable { case `default` case base } @@ -341,7 +344,7 @@ extension PackageInfo.Target { // MARK: Target.TargetType extension PackageInfo.Target { - public enum TargetType: String, Hashable, Decodable { + public enum TargetType: String, Hashable, Codable { case regular case executable case test @@ -358,7 +361,7 @@ extension PackageInfo.Target { /// A namespace for target-specific build settings. public enum TargetBuildSettingDescription { /// The tool for which a build setting is declared. - public enum Tool: String, Decodable, Hashable, CaseIterable { + public enum Tool: String, Codable, Hashable, CaseIterable { case c case cxx case swift @@ -366,7 +369,7 @@ extension PackageInfo.Target { } /// The name of the build setting. - public enum SettingName: String, Decodable, Hashable { + public enum SettingName: String, Codable, Hashable { case headerSearchPath case define case linkedLibrary @@ -377,7 +380,7 @@ extension PackageInfo.Target { } /// An individual build setting. - public struct Setting: Decodable, Hashable { + public struct Setting: Codable, Hashable { /// The tool associated with this setting. public let tool: Tool @@ -413,18 +416,18 @@ extension PackageInfo.Target { case tool, name, condition, value, kind } - public init(from decoder: Decoder) throws { - // Xcode 14 format - enum Kind: Codable, Equatable { - case headerSearchPath(String) - case define(String) - case linkedLibrary(String) - case linkedFramework(String) - case unsafeFlags([String]) - case enableUpcomingFeature(String) - case enableExperimentalFeature(String) - } + // Xcode 14 format + private enum Kind: Codable, Equatable { + case headerSearchPath(String) + case define(String) + case linkedLibrary(String) + case linkedFramework(String) + case unsafeFlags([String]) + case enableUpcomingFeature(String) + case enableExperimentalFeature(String) + } + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) tool = try container.decode(Tool.self, forKey: .tool) @@ -458,19 +461,43 @@ extension PackageInfo.Target { value = try container.decode([String].self, forKey: .value) } } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(tool, forKey: .tool) + try container.encodeIfPresent(condition, forKey: .condition) + switch name { + case .headerSearchPath: + try container.encode(Kind.headerSearchPath(value.first!), forKey: .kind) + case .define: + try container.encode(Kind.define(value.first!), forKey: .kind) + case .linkedLibrary: + try container.encode(Kind.linkedLibrary(value.first!), forKey: .kind) + case .linkedFramework: + try container.encode(Kind.linkedFramework(value.first!), forKey: .kind) + case .unsafeFlags: + try container.encode(Kind.unsafeFlags(value), forKey: .kind) + case .enableUpcomingFeature: + try container.encode(Kind.enableUpcomingFeature(value.first!), forKey: .kind) + case .enableExperimentalFeature: + try container.encode(Kind.enableExperimentalFeature(value.first!), forKey: .kind) + } + } } } } -// MARK: Decodable conformances +// MARK: Codable conformances -extension PackageInfo: Decodable { +extension PackageInfo: Codable { private enum CodingKeys: String, CodingKey { - case products, targets, platforms, cLanguageStandard, cxxLanguageStandard, swiftLanguageVersions + case name, products, targets, platforms, cLanguageStandard, cxxLanguageStandard, swiftLanguageVersions } public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) + name = try values.decode(String.self, forKey: .name) products = try values.decode([Product].self, forKey: .products) targets = try values.decode([Target].self, forKey: .targets) platforms = try values.decode([Platform].self, forKey: .platforms) @@ -482,7 +509,7 @@ extension PackageInfo: Decodable { } } -extension PackageInfo.Target.Dependency: Decodable { +extension PackageInfo.Target.Dependency: Codable { private enum CodingKeys: String, CodingKey { case target, product, byName } @@ -516,9 +543,33 @@ extension PackageInfo.Target.Dependency: Decodable { ) } } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .byName(name: name, condition: condition): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .byName) + try unkeyedContainer.encode(name) + if let condition { + try unkeyedContainer.encode(condition) + } + case let .product(name: name, package: package, moduleAliases: moduleAliases, condition: condition): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .product) + try unkeyedContainer.encode(name) + try unkeyedContainer.encode(package) + try unkeyedContainer.encode(moduleAliases) + try unkeyedContainer.encode(condition) + case let .target(name: name, condition: condition): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .target) + try unkeyedContainer.encode(name) + if let condition { + try unkeyedContainer.encode(condition) + } + } + } } -extension PackageInfo.Product.ProductType: Decodable { +extension PackageInfo.Product.ProductType: Codable { private enum CodingKeys: String, CodingKey { case library, executable, plugin, test } @@ -542,19 +593,25 @@ extension PackageInfo.Product.ProductType: Decodable { self = .plugin } } -} -extension PackageInfo.Target.TargetType { - /// Defines if the target would be processed when processing the package - public var isSupported: Bool { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { - case .regular, .system, .macro: - return true - default: - return false + case .executable: + try container.encode(CodingKeys.executable.rawValue, forKey: .executable) + case .plugin: + try container.encode(CodingKeys.plugin.rawValue, forKey: .plugin) + case .test: + try container.encode(CodingKeys.test.rawValue, forKey: .test) + case let .library(libraryType): + var nestedContainer = container.nestedUnkeyedContainer(forKey: .library) + try nestedContainer.encode(libraryType) } } +} +extension PackageInfo.Target.TargetType { /// Defines if target may have a public headers path /// Based on preconditions in https://github.com/apple/swift-package-manager/blob/main/Sources/PackageDescription/Target.swift public var supportsPublicHeaderPath: Bool { diff --git a/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift b/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift index b5558d73aab..189f9d5ab72 100644 --- a/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift +++ b/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TSCUtility /// Protocol that defines an interface to interact with the Swift Package Manager. @@ -45,22 +45,28 @@ public protocol SwiftPackageManagerControlling { } public final class SwiftPackageManagerController: SwiftPackageManagerControlling { - public init() {} + let system: Systeming + let fileHandler: FileHandling + + public init(system: Systeming, fileHandler: FileHandling) { + self.system = system + self.fileHandler = fileHandler + } public func resolve(at path: AbsolutePath, printOutput: Bool) throws { let command = buildSwiftPackageCommand(packagePath: path, extraArguments: ["resolve"]) printOutput ? - try System.shared.runAndPrint(command) : - try System.shared.run(command) + try system.runAndPrint(command) : + try system.run(command) } public func update(at path: AbsolutePath, printOutput: Bool) throws { let command = buildSwiftPackageCommand(packagePath: path, extraArguments: ["update"]) printOutput ? - try System.shared.runAndPrint(command) : - try System.shared.run(command) + try system.runAndPrint(command) : + try system.run(command) } public func setToolsVersion(at path: AbsolutePath, to version: Version) throws { @@ -68,7 +74,7 @@ public final class SwiftPackageManagerController: SwiftPackageManagerControlling let command = buildSwiftPackageCommand(packagePath: path, extraArguments: extraArguments) - try System.shared.run(command) + try system.run(command) } public func getToolsVersion(at path: AbsolutePath) throws -> Version { @@ -76,14 +82,14 @@ public final class SwiftPackageManagerController: SwiftPackageManagerControlling let command = buildSwiftPackageCommand(packagePath: path, extraArguments: extraArguments) - let rawVersion = try System.shared.capture(command).trimmingCharacters(in: .whitespacesAndNewlines) + let rawVersion = try system.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"]) - let json = try System.shared.capture(command) + let json = try system.capture(command) let data = Data(json.utf8) let decoder = JSONDecoder() @@ -109,22 +115,22 @@ public final class SwiftPackageManagerController: SwiftPackageManagerControlling let arm64Target = "arm64-apple-macosx" let x64Target = "x86_64-apple-macosx" - try System.shared.run( + try system.run( buildCommand + [ arm64Target, ] ) - try System.shared.run( + try system.run( buildCommand + [ x64Target, ] ) - if !FileHandler.shared.exists(outputPath) { - try FileHandler.shared.createFolder(outputPath) + if !fileHandler.exists(outputPath) { + try fileHandler.createFolder(outputPath) } - try System.shared.run([ + try system.run([ "lipo", "-create", "-output", outputPath.appending(component: product).pathString, buildPath.appending(components: arm64Target, "release", product).pathString, buildPath.appending(components: x64Target, "release", product).pathString, diff --git a/Sources/TuistSupport/System/Publisher+System.swift b/Sources/TuistSupport/System/Publisher+System.swift deleted file mode 100644 index ac866c73a46..00000000000 --- a/Sources/TuistSupport/System/Publisher+System.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Combine -import Foundation - -extension Publisher where Output == SystemEvent, Failure == Error { - /// Returns another observable where the standard output and error data are mapped - /// to a string. - public func mapToString() -> AnyPublisher, Error> { - map { $0.mapToString() }.eraseToAnyPublisher() - } -} - -extension Publisher where Output == SystemEvent, Failure == Error { - public func print() -> AnyPublisher, Error> { - handleEvents(receiveOutput: { event in - switch event { - case let .standardError(error): - logger.error("\(error)") - case let .standardOutput(output): - logger.info("\(output)") - } - }) - .eraseToAnyPublisher() - } - - /// Returns an observable that prints the standard error. - public func printStandardError() -> AnyPublisher, Error> { - handleEvents(receiveOutput: { event in - switch event { - case let .standardError(error): - if let data = error.data(using: .utf8) { - FileHandle.standardError.write(data) - } - default: - return - } - }) - .eraseToAnyPublisher() - } - - /// Returns an observable that collects and merges the standard output and error into a single string. - public func collectAndMergeOutput() -> AnyPublisher { - reduce("") { collected, event -> String in - var collected = collected - switch event { - case let .standardError(error): - collected.append(error) - case let .standardOutput(output): - collected.append(output) - } - return collected - }.eraseToAnyPublisher() - } - - /// It collects the standard output and error into an object that is sent - /// as a single event when the process completes. - public func collectOutput() -> AnyPublisher { - reduce(SystemCollectedOutput()) { collected, event -> SystemCollectedOutput in - var collected = collected - switch event { - case let .standardError(error): - collected.standardError.append(error) - case let .standardOutput(output): - collected.standardOutput.append(output) - } - return collected - }.eraseToAnyPublisher() - } -} - -extension Publisher { - public var stream: AsyncThrowingStream { - AsyncThrowingStream(Output.self) { continuation in - let cancellable = sink { completion in - switch completion { - case .finished: - continuation.finish() - case let .failure(error): - continuation.finish(throwing: error) - } - } receiveValue: { output in - continuation.yield(output) - } - - continuation.onTermination = { @Sendable [cancellable] _ in - cancellable.cancel() - } - } - } -} diff --git a/Sources/TuistSupport/System/SwiftVersionProviding.swift b/Sources/TuistSupport/System/SwiftVersionProviding.swift new file mode 100644 index 00000000000..9a75ca30776 --- /dev/null +++ b/Sources/TuistSupport/System/SwiftVersionProviding.swift @@ -0,0 +1,67 @@ +import Foundation +import Mockable + +@Mockable +public protocol SwiftVersionProviding { + /// Returns the Swift version. + /// + /// - Returns: Swift version. + /// - Throws: An error if Swift is not installed or it exists unsuccessfully. + func swiftVersion() throws -> String + + /// Returns the Swift version, including the build number. + /// + /// - Returns: Swift version including the build number. + /// - Throws: An error if Swift is not installed or it exists unsuccessfully. + func swiftlangVersion() throws -> String +} + +public final class SwiftVersionProvider: SwiftVersionProviding { + public static var shared: SwiftVersionProviding { + _shared.value + } + + // swiftlint:disable:next identifier_name + static let _shared: ThreadSafe = ThreadSafe(SwiftVersionProvider(System.shared)) + // swiftlint:disable force_try + + /// Regex expression used to get the Swift version (for example, 5.9) from the output of the 'swift --version' command. + private static let swiftVersionRegex = try! NSRegularExpression(pattern: "Apple Swift version\\s(.+)\\s\\(.+\\)", options: []) + + /// Regex expression used to get the Swiftlang version (for example, 5.7.0.127.4) from the output of the 'swift --version' + /// command. + private static let swiftlangVersionRegex = try! NSRegularExpression(pattern: "swiftlang-(.+)\\sclang", options: []) + + // swiftlint:enable force_try + + public func swiftVersion() throws -> String { + try cachedSwiftVersion.value + } + + public func swiftlangVersion() throws -> String { + try cachedSwiftlangVersion.value + } + + let cachedSwiftVersion: ThrowableCaching + let cachedSwiftlangVersion: ThrowableCaching + + init(_ system: Systeming) { + cachedSwiftVersion = ThrowableCaching { + let output = try system.capture(["/usr/bin/xcrun", "swift", "--version"]) + let range = NSRange(location: 0, length: output.count) + guard let match = SwiftVersionProvider.swiftVersionRegex.firstMatch(in: output, options: [], range: range) else { + throw SystemError.parseSwiftVersion(output) + } + return NSString(string: output).substring(with: match.range(at: 1)).spm_chomp() + } + + cachedSwiftlangVersion = ThrowableCaching { + let output = try system.capture(["/usr/bin/xcrun", "swift", "--version"]) + let range = NSRange(location: 0, length: output.count) + guard let match = SwiftVersionProvider.swiftlangVersionRegex.firstMatch(in: output, options: [], range: range) else { + throw SystemError.parseSwiftVersion(output) + } + return NSString(string: output).substring(with: match.range(at: 1)).spm_chomp() + } + } +} diff --git a/Sources/TuistSupport/System/System.swift b/Sources/TuistSupport/System/System.swift index 05bfcdd2952..c62e1a98688 100644 --- a/Sources/TuistSupport/System/System.swift +++ b/Sources/TuistSupport/System/System.swift @@ -1,6 +1,5 @@ -import Combine -import CombineExt import Foundation +import Path import TSCBasic extension ProcessResult { @@ -68,18 +67,12 @@ public enum SystemError: FatalError, Equatable { // swiftlint:disable:next type_body_length public final class System: Systeming { /// Shared system instance. - public static var shared: Systeming = System() - - // swiftlint:disable force_try - - /// Regex expression used to get the Swift version (for example, 5.7) from the output of the 'swift --version' command. - private static var swiftVersionRegex = try! NSRegularExpression(pattern: "Apple Swift version\\s(.+)\\s\\(.+\\)", options: []) - - /// Regex expression used to get the Swiftlang version (for example, 5.7.0.127.4) from the output of the 'swift --version' - /// command. - private static var swiftlangVersion = try! NSRegularExpression(pattern: "swiftlang-(.+)\\sclang", options: []) + public static var shared: Systeming { + _shared.value + } - // swiftlint:enable force_try + // swiftlint:disable:next identifier_name + static let _shared: ThreadSafe = ThreadSafe(System()) /// Convenience shortcut to the environment. public var env: [String: String] { @@ -129,28 +122,37 @@ public final class System: Systeming { } public func runAndPrint(_ arguments: [String]) throws { - try runAndPrint(arguments, verbose: false, environment: env) + try run(arguments, verbose: false, environment: env, redirection: streamToStandardOutputs) } - public func runAndPrint( - _ arguments: [String], - verbose: Bool, - environment: [String: String] - ) throws { - try runAndPrint( - arguments, - verbose: verbose, - environment: environment, - redirection: .none - ) + public func runAndPrint(_ arguments: [String], verbose: Bool, environment: [String: String]) throws { + try run(arguments, verbose: verbose, environment: environment, redirection: streamToStandardOutputs) + } + + private var streamToStandardOutputs: TSCBasic.Process.OutputRedirection { + return .stream { bytes in + FileHandle.standardOutput.write(Data(bytes)) + } stderr: { bytes in + FileHandle.standardError.write(Data(bytes)) + } } public func runAndCollectOutput(_ arguments: [String]) async throws -> SystemCollectedOutput { - var values = publisher(arguments) - .mapToString() - .collectOutput().values.makeAsyncIterator() + let process = Process( + arguments: arguments, + environment: env, + outputRedirection: .collect, + startNewProcessGroup: false + ) - return try await values.next()! + try process.launch() + + let result = try await process.waitUntilExit() + + return SystemCollectedOutput( + standardOutput: try result.utf8Output(), + standardError: try result.utf8stderrOutput() + ) } public func async(_ arguments: [String]) throws { @@ -166,114 +168,37 @@ public final class System: Systeming { try process.launch() } - @Atomic - var cachedSwiftVersion: String? - - @Atomic - var cachedSwiftlangVersion: String? - - public func swiftVersion() throws -> String { - if let cachedSwiftVersion { - return cachedSwiftVersion - } - let output = try capture(["/usr/bin/xcrun", "swift", "--version"]) - let range = NSRange(location: 0, length: output.count) - guard let match = System.swiftVersionRegex.firstMatch(in: output, options: [], range: range) else { - throw SystemError.parseSwiftVersion(output) - } - cachedSwiftVersion = NSString(string: output).substring(with: match.range(at: 1)).spm_chomp() - return cachedSwiftVersion! - } - - public func swiftlangVersion() throws -> String { - if let cachedSwiftlangVersion { - return cachedSwiftlangVersion - } - let output = try capture(["/usr/bin/xcrun", "swift", "--version"]) - let range = NSRange(location: 0, length: output.count) - guard let match = System.swiftlangVersion.firstMatch(in: output, options: [], range: range) else { - throw SystemError.parseSwiftVersion(output) - } - cachedSwiftlangVersion = NSString(string: output).substring(with: match.range(at: 1)).spm_chomp() - return cachedSwiftlangVersion! - } - public func which(_ name: String) throws -> String { try capture(["/usr/bin/env", "which", name]).spm_chomp() } // MARK: Helpers - /// Converts an array of arguments into a `Foundation.Process` - /// - Parameters: - /// - arguments: Arguments for the process, first item being the executable URL. - /// - environment: Environment - /// - Returns: A `Foundation.Process` - static func process( - _ arguments: [String], - environment: [String: String] - ) -> Foundation.Process { - let executablePath = arguments.first! - let process = Foundation.Process() - process.executableURL = URL(fileURLWithPath: executablePath) - process.arguments = Array(arguments.dropFirst()) - process.environment = environment - return process - } - - /// Pipe the output of one Process to another - /// - Parameters: - /// - processOne: First Process - /// - processTwo: Second Process - /// - Returns: The pipe - @discardableResult - static func pipe( - _ processOne: inout Foundation.Process, - _ processTwo: inout Foundation.Process - ) -> Pipe { - let processPipe = Pipe() - - processOne.standardOutput = processPipe - processTwo.standardInput = processPipe - return processPipe - } - - /// PIpe the output of a process into separate output and error pipes - /// - Parameter process: The process to pipe - /// - Returns: Tuple that contains the output and error Pipe. - static func pipeOutput(_ process: inout Foundation.Process) -> (stdOut: Pipe, stdErr: Pipe) { - let stdOut = Pipe() - let stdErr = Pipe() - - // Redirect output of Process Two - process.standardOutput = stdOut - process.standardError = stdErr - - return (stdOut, stdErr) - } - public func chmod( _ mode: FileMode, - path: AbsolutePath, + path: Path.AbsolutePath, options: Set ) throws { - try localFileSystem.chmod(mode, path: path, options: options) + try localFileSystem.chmod(mode, path: .init(validating: path.pathString), options: options) } - private func runAndPrint( + public func run( _ arguments: [String], - verbose: Bool, - environment: [String: String], - redirection: TSCBasic.Process.OutputRedirection + verbose: Bool = false, + environment: [String: String] = ProcessInfo.processInfo.environment, + redirection: TSCBasic.Process.OutputRedirection = .none ) throws { + // We have to collect both stderr and stdout because we want to stream the output + // the `ProcessResult` type will not contain either unless outputRedirection is set to `.collect` + var stdErrData: [UInt8] = [] + let process = Process( arguments: arguments, environment: environment, outputRedirection: .stream(stdout: { bytes in - FileHandle.standardOutput.write(Data(bytes)) redirection.outputClosures?.stdoutClosure(bytes) }, stderr: { bytes in - FileHandle.standardError.write(Data(bytes)) + stdErrData.append(contentsOf: bytes) redirection.outputClosures?.stderrClosure(bytes) }), startNewProcessGroup: false, @@ -284,140 +209,16 @@ public final class System: Systeming { try process.launch() let result = try process.waitUntilExit() - let output = try result.utf8Output() - logger.debug("\(output)") - - try result.throwIfErrored() - } - - public func publisher( - _ arguments: [String], - verbose: Bool, - environment: [String: String] - ) -> AnyPublisher, Error> { - .create { subscriber in - let synchronizationQueue = DispatchQueue(label: "io.tuist.support.system") - var errorData: [UInt8] = [] - let process = Process( - arguments: arguments, - environment: environment, - outputRedirection: .stream(stdout: { bytes in - synchronizationQueue.async { - subscriber.send(.standardOutput(Data(bytes))) - } - }, stderr: { bytes in - synchronizationQueue.async { - errorData.append(contentsOf: bytes) - subscriber.send(.standardError(Data(bytes))) - } - }), - startNewProcessGroup: false, - loggingHandler: verbose ? { stdoutStream.send($0).send("\n").flush() } : nil - ) - DispatchQueue.global().async { - do { - try process.launch() - var result = try process.waitUntilExit() - result = ProcessResult( - arguments: result.arguments, - environment: environment, - exitStatus: result.exitStatus, - output: result.output, - stderrOutput: result.stderrOutput.map { _ in errorData } - ) - try result.throwIfErrored() - synchronizationQueue.sync { - subscriber.send(completion: .finished) - } - } catch { - synchronizationQueue.sync { - subscriber.send(completion: .failure(error)) - } - } - } - return AnyCancellable { - if process.launched { - process.signal(9) // SIGKILL - } - } - } - } - - // swiftlint:disable:next function_body_length - private func publisher( - _ arguments: [String], - environment: [String: String], - pipeTo secondArguments: [String] - ) -> AnyPublisher, Error> { - .create { subscriber in - let synchronizationQueue = DispatchQueue(label: "io.tuist.support.system") - var errorData: [UInt8] = [] - var processOne = System.process(arguments, environment: environment) - var processTwo = System.process(secondArguments, environment: environment) - - System.pipe(&processOne, &processTwo) - - let pipes = System.pipeOutput(&processTwo) - - pipes.stdOut.fileHandleForReading.readabilityHandler = { fileHandle in - synchronizationQueue.async { - let data: Data = fileHandle.availableData - if !data.isEmpty { - subscriber.send(.standardOutput(Data(data))) - } - } - } - - pipes.stdErr.fileHandleForReading.readabilityHandler = { fileHandle in - synchronizationQueue.async { - let data: Data = fileHandle.availableData - errorData.append(contentsOf: data) - if !data.isEmpty { - subscriber.send(.standardError(Data(data))) - } - } - } - - DispatchQueue.global().async { - do { - try processOne.run() - try processTwo.run() - processOne.waitUntilExit() - - let exitStatus = ProcessResult.ExitStatus.terminated(code: processOne.terminationStatus) - let result = ProcessResult( - arguments: arguments, - environment: environment, - exitStatus: exitStatus, - output: .success([]), - stderrOutput: .success(errorData) - ) - try result.throwIfErrored() - synchronizationQueue.sync { - subscriber.send(completion: .finished) - } - } catch { - synchronizationQueue.sync { - subscriber.send(completion: .failure(error)) - } - } - } - return AnyCancellable { - pipes.stdOut.fileHandleForReading.readabilityHandler = nil - pipes.stdErr.fileHandleForReading.readabilityHandler = nil - if processOne.isRunning { - processOne.terminate() - } + switch result.exitStatus { + case let .signalled(code): + let data = Data(stdErrData) + throw TuistSupport.SystemError.signalled(command: result.command(), code: code, standardError: data) + case let .terminated(code): + if code != 0 { + let data = Data(stdErrData) + throw TuistSupport.SystemError.terminated(command: result.command(), code: code, standardError: data) } } } - - public func publisher(_ arguments: [String]) -> AnyPublisher, Error> { - publisher(arguments, verbose: false, environment: env) - } - - public func publisher(_ arguments: [String], pipeTo secondArguments: [String]) -> AnyPublisher, Error> { - publisher(arguments, environment: env, pipeTo: secondArguments) - } } diff --git a/Sources/TuistSupport/System/SystemEvent.swift b/Sources/TuistSupport/System/SystemEvent.swift deleted file mode 100644 index 37051d7018e..00000000000 --- a/Sources/TuistSupport/System/SystemEvent.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation -import TSCBasic - -/// It represents an event sent by a running process. -public enum SystemEvent { - /// Data sent through the standard output pipe. - case standardOutput(T) - - /// Data sent through the standard error pipe. - case standardError(T) - - /// Returns the wrapped value. - public var value: T { - switch self { - case let .standardError(value): return value - case let .standardOutput(value): return value - } - } - - /// Returns true if the event is a standard output event. - public var isStandardOutput: Bool { - switch self { - case .standardError: return false - case .standardOutput: return true - } - } - - /// Returns true if the event is a standard output event. - public var isStandardError: Bool { - switch self { - case .standardError: return true - case .standardOutput: return false - } - } -} - -extension SystemEvent: Equatable where T: Equatable { - public static func == (lhs: SystemEvent, rhs: SystemEvent) -> Bool { - switch (lhs, rhs) { - case let (.standardOutput(lhsValue), .standardOutput(rhsValue)): - return lhsValue == rhsValue - case let (.standardError(lhsValue), .standardError(rhsValue)): - return lhsValue == rhsValue - default: - return false - } - } -} - -extension SystemEvent where T == Data { - /// Maps the standard output and error from data to string using the utf8 encoding - func mapToString() -> SystemEvent { - switch self { - case let .standardError(data): - return .standardError(String(data: data, encoding: .utf8)!) - case let .standardOutput(data): - return .standardOutput(String(data: data, encoding: .utf8)!) - } - } -} diff --git a/Sources/TuistSupport/System/Systeming.swift b/Sources/TuistSupport/System/Systeming.swift index fba6e8b090c..b3f5c05fe52 100644 --- a/Sources/TuistSupport/System/Systeming.swift +++ b/Sources/TuistSupport/System/Systeming.swift @@ -1,5 +1,5 @@ -import Combine import Foundation +import Path import TSCBasic public protocol Systeming { @@ -46,28 +46,25 @@ public protocol Systeming { /// - Throws: An error if the command fails. func runAndPrint(_ arguments: [String], verbose: Bool, environment: [String: String]) throws - /// Runs a command in the shell and wraps the standard output. - /// - Parameters: - /// - arguments: Command. - func runAndCollectOutput(_ arguments: [String]) async throws -> SystemCollectedOutput - - /// Runs a command in the shell and wraps the standard output and error in a publisher. - /// - Parameters: - /// - arguments: Command. - func publisher(_ arguments: [String]) -> AnyPublisher, Error> - - /// Runs a command in the shell and wraps the standard output and error in a publisher. + /// Runs a command in the shell and redirects output based on the passed in parameter. + /// /// - Parameters: /// - arguments: Command. /// - verbose: When true it prints the command that will be executed before executing it. - /// - environment: Environment that should be used when running the command. - func publisher(_ arguments: [String], verbose: Bool, environment: [String: String]) -> AnyPublisher, Error> + /// - environment: Environment that should be used when running the task. + /// - redirection: Output Redirection behavior for the underlying `Process` + /// - Throws: An error if the command fails. + func run( + _ arguments: [String], + verbose: Bool, + environment: [String: String], + redirection: TSCBasic.Process.OutputRedirection + ) throws - /// Runs a command in the shell and wraps the standard output and error in a publisher. + /// Runs a command in the shell and wraps the standard output. /// - Parameters: /// - arguments: Command. - /// - secondArguments: Second Command. - func publisher(_ arguments: [String], pipeTo secondArguments: [String]) -> AnyPublisher, Error> + func runAndCollectOutput(_ arguments: [String]) async throws -> SystemCollectedOutput /// Runs a command in the shell asynchronously. /// When the process that triggers the command gets killed, the command continues its execution. @@ -77,18 +74,6 @@ public protocol Systeming { /// - Throws: An error if the command fails. func async(_ arguments: [String]) throws - /// Returns the Swift version. - /// - /// - Returns: Swift version. - /// - Throws: An error if Swift is not installed or it exists unsuccessfully. - func swiftVersion() throws -> String - - /// Returns the Swift version, including the build number. - /// - /// - Returns: Swift version including the build number. - /// - Throws: An error if Swift is not installed or it exists unsuccessfully. - func swiftlangVersion() throws -> String - /// Runs /usr/bin/which passing the given tool. /// /// - Parameter name: Tool whose path will be obtained using which. @@ -101,7 +86,7 @@ public protocol Systeming { /// - mode: Defines user file mode. /// - path: Path of file for which the permissions should be changed. /// - options: Options for changing permissions. - func chmod(_ mode: FileMode, path: AbsolutePath, options: Set) throws + func chmod(_ mode: FileMode, path: Path.AbsolutePath, options: Set) throws } extension Systeming { diff --git a/Sources/TuistSupport/UserInputReader.swift b/Sources/TuistSupport/UserInputReader.swift new file mode 100644 index 00000000000..51274168383 --- /dev/null +++ b/Sources/TuistSupport/UserInputReader.swift @@ -0,0 +1,94 @@ +import Foundation +import Mockable + +enum UserInputReaderError: FatalError, Equatable { + case noValuesProvided(String) + + var description: String { + switch self { + case let .noValuesProvided(prompt): + return "No values to choose from for the prompt \(prompt)" + } + } + + var type: ErrorType { + switch self { + case .noValuesProvided: + return .bug + } + } +} + +@Mockable +public protocol UserInputReading { + /// Reads an integer from the user. + /// - Parameters: + /// - prompt: The prompt to be shown to the user providing context and the allowed options. + /// - maxValueAllowed: The max value allowed given the list of options provided in the prompt. + func readInt(asking prompt: String, maxValueAllowed: Int) -> Int + + /// Reads a string from the user. + /// - Parameters: + /// - prompt: The prompt to be shown to the user providing context and the allowed options. + func readString(asking prompt: String) -> String + + /// Reads a value from the user. + /// - Parameters: + /// - prompt: The prompt to be shown to the user providing context and the allowed options. + /// - values: Values to choose from. + /// - valueDescription: A closure for extracting description for a value. The description is used in the list of choices + /// presented to the user in the CLI. + func readValue( + asking prompt: String, + values: [Value], + valueDescription: @escaping (Value) -> String + ) throws -> Value +} + +public struct UserInputReader: UserInputReading { + private var reader: (Bool) -> String? + + public init(reader: @escaping (Bool) -> String? = readLine) { + self.reader = reader + } + + public func readValue( + asking prompt: String, + values: [Value], + valueDescription: (Value) -> String + ) throws -> Value { + guard !values.isEmpty else { throw UserInputReaderError.noValuesProvided(prompt) } + + if values.count == 1, let onlyValue = values.first { + return onlyValue + } else { + let prompt = [prompt] + values.map(valueDescription).enumerated().map { index, value in + "\t\(index): \(value)" + } + let choice = readInt(asking: prompt.joined(separator: "\n"), maxValueAllowed: values.count) + return values[choice] + } + } + + public func readInt(asking prompt: String, maxValueAllowed: Int) -> Int { + while true { + logger.notice("\(prompt)") + if let input = reader(true), !input.isEmpty, let intValue = Int(input), intValue < maxValueAllowed { + return intValue + } else { + logger.notice("Invalid input. Please enter a valid integer.") + } + } + } + + public func readString(asking prompt: String) -> String { + while true { + logger.notice("\(prompt)") + if let input = reader(true), !input.isEmpty { + return input + } else { + logger.notice("The value is empty. Please, enter a non-empty value.") + } + } + } +} diff --git a/Sources/TuistSupport/Utils/CIChecker.swift b/Sources/TuistSupport/Utils/CIChecker.swift index 3668757a6ad..0c3f79e48f6 100644 --- a/Sources/TuistSupport/Utils/CIChecker.swift +++ b/Sources/TuistSupport/Utils/CIChecker.swift @@ -1,6 +1,8 @@ import Foundation +import Mockable -public protocol CIChecking: AnyObject { +@Mockable +public protocol CIChecking: AnyObject, Sendable { /// Returns true when the environment in which the tuist process is running is a CI environment. func isCI() -> Bool } diff --git a/Sources/TuistSupport/Utils/Cached.swift b/Sources/TuistSupport/Utils/Cached.swift new file mode 100644 index 00000000000..54359e55024 --- /dev/null +++ b/Sources/TuistSupport/Utils/Cached.swift @@ -0,0 +1,57 @@ +import Foundation + +@propertyWrapper +public final class Cached { + private let _cache: Caching + public var wrappedValue: T { + return _cache.value + } + + init(_ lazyValue: @Sendable @escaping () -> T) { + _cache = Caching(lazyValue) + } +} + +public final class Caching { + private let _value: ThreadSafe = ThreadSafe(nil) + public var value: T { + return _value.mutate { value in + if let value { + return value + } else { + let realizedValue = builder() + value = realizedValue + return realizedValue + } + } + } + + let builder: @Sendable () -> T + + public init(_ lazyValue: @Sendable @escaping () -> T) { + builder = lazyValue + } +} + +public final class ThrowableCaching { + private let _value: ThreadSafe = ThreadSafe(nil) + public var value: T { + get throws { + return try _value.mutate { value in + if let value { + return value + } else { + let realizedValue = try builder() + value = realizedValue + return realizedValue + } + } + } + } + + let builder: @Sendable () throws -> T + + public init(_ lazyValue: @Sendable @escaping () throws -> T) { + builder = lazyValue + } +} diff --git a/Sources/TuistSupport/Utils/ColorizeSwift.swift b/Sources/TuistSupport/Utils/ColorizeSwift.swift index 0ed6f38b022..d9b9ac2d96c 100644 --- a/Sources/TuistSupport/Utils/ColorizeSwift.swift +++ b/Sources/TuistSupport/Utils/ColorizeSwift.swift @@ -57,9 +57,6 @@ public enum TerminalStyle { } extension String { - /// Enable/disable colorization - public static var isColorizationEnabled = true - public func bold() -> String { applyStyle(TerminalStyle.bold) } @@ -93,7 +90,6 @@ extension String { } public func reset() -> String { - guard String.isColorizationEnabled else { return self } return "\u{001B}[0m" + self } @@ -110,7 +106,6 @@ extension String { } private func applyStyle(_ codeStyle: TerminalStyleCode) -> String { - guard String.isColorizationEnabled else { return self } let str = replacingOccurrences(of: TerminalStyle.reset.open, with: TerminalStyle.reset.open + codeStyle.open) return codeStyle.open + str + TerminalStyle.reset.open diff --git a/Sources/TuistSupport/Utils/DateService.swift b/Sources/TuistSupport/Utils/DateService.swift new file mode 100644 index 00000000000..434308e5ef4 --- /dev/null +++ b/Sources/TuistSupport/Utils/DateService.swift @@ -0,0 +1,15 @@ +import Foundation +import Mockable + +@Mockable +public protocol DateServicing: Sendable { + func now() -> Date +} + +public struct DateService: DateServicing { + public init() {} + + public func now() -> Date { + Date() + } +} diff --git a/Sources/TuistSupport/Utils/DerivedDataLocator.swift b/Sources/TuistSupport/Utils/DerivedDataLocator.swift index edf0cd7b1c4..bbecde39a1f 100644 --- a/Sources/TuistSupport/Utils/DerivedDataLocator.swift +++ b/Sources/TuistSupport/Utils/DerivedDataLocator.swift @@ -1,6 +1,6 @@ import CryptoKit import Foundation -import TSCBasic +import Path public protocol DerivedDataLocating { func locate( @@ -9,13 +9,17 @@ public protocol DerivedDataLocating { } public final class DerivedDataLocator: DerivedDataLocating { - public init() {} + private let devEnvironment: DeveloperEnvironmenting + + public init(devEnvironment: DeveloperEnvironmenting = DeveloperEnvironment.shared) { + self.devEnvironment = devEnvironment + } public func locate( for projectPath: AbsolutePath ) throws -> AbsolutePath { let hash = try XcodeProjectPathHasher.hashString(for: projectPath.pathString) - return DeveloperEnvironment.shared.derivedDataDirectory + return devEnvironment.derivedDataDirectory .appending(component: "\(projectPath.basenameWithoutExt)-\(hash)") } } diff --git a/Sources/TuistSupport/Utils/DeveloperEnvironment.swift b/Sources/TuistSupport/Utils/DeveloperEnvironment.swift index 2cb5d0d9b66..f0cb0cd6694 100644 --- a/Sources/TuistSupport/Utils/DeveloperEnvironment.swift +++ b/Sources/TuistSupport/Utils/DeveloperEnvironment.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path public protocol DeveloperEnvironmenting { /// Returns the derived data directory selected in the environment. @@ -13,7 +13,12 @@ public final class DeveloperEnvironment: DeveloperEnvironmenting { /// Shared instance to be used publicly. /// Since the environment doesn't change during the execution of Tuist, we can cache /// state internally to speed up future access to environment attributes. - public internal(set) static var shared: DeveloperEnvironmenting = DeveloperEnvironment() + public static var shared: DeveloperEnvironmenting { + _shared.value + } + + // swiftlint:disable identifier_name + static let _shared: ThreadSafe = ThreadSafe(DeveloperEnvironment()) /// File handler instance. let fileHandler: FileHandling @@ -24,40 +29,41 @@ public final class DeveloperEnvironment: DeveloperEnvironmenting { private init(fileHandler: FileHandling) { self.fileHandler = fileHandler - } - // swiftlint:disable identifier_name - - /// https://pewpewthespells.com/blog/xcode_build_locations.html/// https://pewpewthespells.com/blog/xcode_build_locations.html - @Atomic private var _derivedDataDirectory: AbsolutePath? - public var derivedDataDirectory: AbsolutePath { - if let _derivedDataDirectory { - return _derivedDataDirectory - } - let location: AbsolutePath - if let customLocation = try? System.shared.capture([ - "/usr/bin/defaults", - "read", - "com.apple.dt.Xcode IDECustomDerivedDataLocation", - ]) { - location = try! AbsolutePath(validating: customLocation.chomp()) // swiftlint:disable:this force_try - } else { - // Default location - // swiftlint:disable:next force_try - location = fileHandler.homeDirectory.appending(try! RelativePath(validating: "Library/Developer/Xcode/DerivedData/")) + derivedDataDirectoryCache = ThrowableCaching { + let location: AbsolutePath + if let customLocation = try? System.shared.capture([ + "/usr/bin/defaults", + "read", + "com.apple.dt.Xcode IDECustomDerivedDataLocation", + ]) { + location = try! AbsolutePath(validating: customLocation.chomp()) // swiftlint:disable:this force_try + } else { + // Default location + location = fileHandler.homeDirectory + .appending(try! RelativePath( // swiftlint:disable:this force_try + validating: "Library/Developer/Xcode/DerivedData/" + )) + } + return location } - _derivedDataDirectory = location - return location } - @Atomic private var _architecture: MacArchitecture? + /// https://pewpewthespells.com/blog/xcode_build_locations.html + private let derivedDataDirectoryCache: ThrowableCaching + public var derivedDataDirectory: Path.AbsolutePath { + // swiftlint:disable:next force_try + try! derivedDataDirectoryCache.value + } + public var architecture: MacArchitecture { - if let _architecture { - return _architecture - } + // swiftlint:disable:next force_try + try! architectureCache.value + } + + private let architectureCache = ThrowableCaching { // swiftlint:disable:next force_try let output = try! System.shared.capture(["/usr/bin/uname", "-m"]).chomp() - _architecture = MacArchitecture(rawValue: output) - return _architecture! + return MacArchitecture(rawValue: output)! } // swiftlint:enable identifier_name } diff --git a/Sources/TuistSupport/Utils/Environment.swift b/Sources/TuistSupport/Utils/Environment.swift index e33e554194a..93e3037b460 100644 --- a/Sources/TuistSupport/Utils/Environment.swift +++ b/Sources/TuistSupport/Utils/Environment.swift @@ -1,10 +1,10 @@ -import Darwin.C +import Darwin import Foundation -import TSCBasic +import Path /// Protocol that defines the interface of a local environment controller. /// It manages the local directory where tuistenv stores the tuist versions and user settings. -public protocol Environmenting: AnyObject { +public protocol Environmenting: AnyObject, Sendable { /// Returns the versions directory. var versionsDirectory: AbsolutePath { get } @@ -21,15 +21,15 @@ public protocol Environmenting: AnyObject { /// Returns all the environment variables that are specific to Tuist (prefixed with TUIST_) var tuistVariables: [String: String] { get } - /// Returns all the environment variables that are specific to Tuist configuration (prefixed with TUIST_CONFIG_) - var tuistConfigVariables: [String: String] { get } - /// Returns all the environment variables that can be included during the manifest loading process var manifestLoadingVariables: [String: String] { get } /// Returns true if Tuist is running with verbose mode enabled. var isVerbose: Bool { get } + /// Returns the path to the cache directory. Configurable via the `XDG_CACHE_HOME` environment variable + var cacheDirectory: AbsolutePath? { get } + /// Returns the path to the directory where the async queue events are persisted. var queueDirectory: AbsolutePath { get } @@ -44,8 +44,13 @@ public protocol Environmenting: AnyObject { } /// Local environment controller. -public class Environment: Environmenting { - public static var shared: Environmenting = Environment() +public final class Environment: Environmenting { + public static var shared: Environmenting { + _shared.value + } + + // swiftlint:disable:next identifier_name + static let _shared: ThreadSafe = ThreadSafe(Environment()) /// Returns the default local directory. static let defaultDirectory = try! AbsolutePath( // swiftlint:disable:this force_try @@ -82,19 +87,30 @@ public class Environment: Environmenting { /// Sets up the local environment. public func bootstrap() throws { - for item in [directory, versionsDirectory] { - if !fileHandler.exists(item) { - try fileHandler.createFolder(item) - } + for item in [directory, versionsDirectory] where !fileHandler.exists(item) { + try fileHandler.createFolder(item) } } /// Returns true if the output of Tuist should be coloured. public var shouldOutputBeColoured: Bool { - if let coloredOutput = ProcessInfo.processInfo.environment[Constants.EnvironmentVariables.colouredOutput] { - return Constants.trueValues.contains(coloredOutput) + let noColor = if let noColorEnvVariable = ProcessInfo.processInfo.environment["NO_COLOR"] { + Constants.trueValues.contains(noColorEnvVariable) + } else { + false + } + let ciColorForce = if let ciColorForceEnvVariable = ProcessInfo.processInfo.environment["CLICOLOR_FORCE"] { + Constants.trueValues.contains(ciColorForceEnvVariable) + } else { + false + } + if noColor { + return false + } else if ciColorForce { + return true } else { - return isStandardOutputInteractive + let isPiped = isatty(fileno(stdout)) == 0 + return !isPiped } } @@ -136,6 +152,14 @@ public class Environment: Environmenting { } } + public var cacheDirectory: AbsolutePath? { + if let cacheDirectoryPathString = ProcessInfo.processInfo.environment["XDG_CACHE_HOME"] { + return try? AbsolutePath(validating: cacheDirectoryPathString) + } else { + return nil + } + } + public var automationPath: AbsolutePath? { ProcessInfo.processInfo.environment[Constants.EnvironmentVariables.automationPath] .map { try! AbsolutePath(validating: $0) } // swiftlint:disable:this force_try @@ -151,12 +175,7 @@ public class Environment: Environmenting { /// Returns all the environment variables that are specific to Tuist (prefixed with TUIST_) public var tuistVariables: [String: String] { - ProcessInfo.processInfo.environment.filter { $0.key.hasPrefix("TUIST_") }.filter { !$0.key.hasPrefix("TUIST_CONFIG_") } - } - - /// Returns all the environment variables that are specific to Tuist config (prefixed with TUIST_CONFIG_) - public var tuistConfigVariables: [String: String] { - ProcessInfo.processInfo.environment.filter { $0.key.hasPrefix("TUIST_CONFIG_") } + ProcessInfo.processInfo.environment.filter { $0.key.hasPrefix("TUIST_") } } public var manifestLoadingVariables: [String: String] { diff --git a/Sources/TuistSupport/Utils/FileArchiver.swift b/Sources/TuistSupport/Utils/FileArchiver.swift index 894a4b1537a..32b806bfe09 100644 --- a/Sources/TuistSupport/Utils/FileArchiver.swift +++ b/Sources/TuistSupport/Utils/FileArchiver.swift @@ -1,27 +1,33 @@ +import FileSystem import Foundation -import TSCBasic +import Mockable +import Path /// An interface to archive files in a zip file. +@Mockable public protocol FileArchiving { /// Zips files and outputs them in a zip file with the given name. /// - Parameter name: Name of the output zip file. func zip(name: String) throws -> AbsolutePath /// Call this method to delete the temporary directory where the .zip file has been generated. - func delete() throws + func delete() async throws } public class FileArchiver: FileArchiving { /// Paths to be archived. private let paths: [AbsolutePath] + private let fileSystem: FileSystem + /// Temporary directory in which the .zip file will be generated. private var temporaryDirectory: AbsolutePath /// Initializes the archiver with a list of files to archive. /// - Parameter paths: Paths to archive - public init(paths: [AbsolutePath]) throws { + public init(paths: [AbsolutePath], fileSystem: FileSystem = FileSystem()) throws { self.paths = paths + self.fileSystem = fileSystem temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false).path } @@ -37,7 +43,7 @@ public class FileArchiver: FileArchiving { return destinationZipPath } - public func delete() throws { - try FileHandler.shared.delete(temporaryDirectory) + public func delete() async throws { + try await fileSystem.remove(temporaryDirectory) } } diff --git a/Sources/TuistSupport/Utils/FileArchivingFactory.swift b/Sources/TuistSupport/Utils/FileArchivingFactory.swift index 0d759bd8d73..fdc5d027a15 100644 --- a/Sources/TuistSupport/Utils/FileArchivingFactory.swift +++ b/Sources/TuistSupport/Utils/FileArchivingFactory.swift @@ -1,6 +1,8 @@ -import TSCBasic +import Mockable +import Path /// An interface that defines a factory of archiver and unarchivers. +@Mockable public protocol FileArchivingFactorying { /// Returns an archiver to archive the given paths. /// - Parameter paths: Files to archive. diff --git a/Sources/TuistSupport/Utils/FileHandler.swift b/Sources/TuistSupport/Utils/FileHandler.swift index 7a996d89328..5e5b8337f3d 100644 --- a/Sources/TuistSupport/Utils/FileHandler.swift +++ b/Sources/TuistSupport/Utils/FileHandler.swift @@ -1,14 +1,16 @@ import CryptoKit import Foundation +import Path import TSCBasic import ZIPFoundation public enum FileHandlerError: FatalError, Equatable { - case invalidTextEncoding(AbsolutePath) - case writingError(AbsolutePath) - case fileNotFound(AbsolutePath) - case unreachableFileSize(AbsolutePath) - case expectedAFile(AbsolutePath) + case invalidTextEncoding(Path.AbsolutePath) + case writingError(Path.AbsolutePath) + case fileNotFound(Path.AbsolutePath) + case unreachableFileSize(Path.AbsolutePath) + case expectedAFile(Path.AbsolutePath) + case propertyListDecodeError(Path.AbsolutePath, description: String) public var description: String { switch self { @@ -22,6 +24,8 @@ public enum FileHandlerError: FatalError, Equatable { return "Could not get the file size at path \(path.pathString)" case let .expectedAFile(path): return "Could not find a file at path \(path.pathString))" + case let .propertyListDecodeError(path, description): + return "The property list file at path \(path.pathString) is invalid and cannot be decoded:\n\(description)" } } @@ -29,7 +33,7 @@ public enum FileHandlerError: FatalError, Equatable { switch self { case .invalidTextEncoding: return .bug - case .writingError, .fileNotFound, .unreachableFileSize, .expectedAFile: + case .writingError, .fileNotFound, .unreachableFileSize, .expectedAFile, .propertyListDecodeError: return .abort } } @@ -39,51 +43,77 @@ public enum FileHandlerError: FatalError, Equatable { /// methods to interact with the system files and folders. public protocol FileHandling: AnyObject { /// Returns the current path. - var currentPath: AbsolutePath { get } + var currentPath: Path.AbsolutePath { get } /// Returns `AbsolutePath` to home directory - var homeDirectory: AbsolutePath { get } - - func replace(_ to: AbsolutePath, with: AbsolutePath) throws - func exists(_ path: AbsolutePath) -> Bool - func move(from: AbsolutePath, to: AbsolutePath) throws - func copy(from: AbsolutePath, to: AbsolutePath) throws - func readFile(_ at: AbsolutePath) throws -> Data - func readTextFile(_ at: AbsolutePath) throws -> String - func readPlistFile(_ at: AbsolutePath) throws -> T + var homeDirectory: Path.AbsolutePath { get } + + func replace(_ to: Path.AbsolutePath, with: Path.AbsolutePath) throws + func exists(_ path: Path.AbsolutePath) -> Bool + func move(from: Path.AbsolutePath, to: Path.AbsolutePath) throws + func copy(from: Path.AbsolutePath, to: Path.AbsolutePath) throws + func readFile(_ at: Path.AbsolutePath) throws -> Data + func readTextFile(_ at: Path.AbsolutePath) throws -> String + func readPlistFile(_ at: Path.AbsolutePath) throws -> T /// Determine temporary directory either default for user or specified by ENV variable - func determineTemporaryDirectory() throws -> AbsolutePath - func temporaryDirectory() throws -> AbsolutePath - func inTemporaryDirectory(_ closure: @escaping (AbsolutePath) async throws -> Void) async throws - func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws - func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Void) throws - func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Result) throws -> Result - func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Result) throws -> Result - func write(_ content: String, path: AbsolutePath, atomically: Bool) throws - func locateDirectoryTraversingParents(from: AbsolutePath, path: String) -> AbsolutePath? - func locateDirectory(_ path: String, traversingFrom from: AbsolutePath) throws -> AbsolutePath? - func glob(_ path: AbsolutePath, glob: String) -> [AbsolutePath] - func throwingGlob(_ path: AbsolutePath, glob: String) throws -> [AbsolutePath] - func linkFile(atPath: AbsolutePath, toPath: AbsolutePath) throws - func createFolder(_ path: AbsolutePath) throws - func delete(_ path: AbsolutePath) throws - func isFolder(_ path: AbsolutePath) -> Bool - func touch(_ path: AbsolutePath) throws - func contentsOfDirectory(_ path: AbsolutePath) throws -> [AbsolutePath] - func urlSafeBase64MD5(path: AbsolutePath) throws -> String - func fileSize(path: AbsolutePath) throws -> UInt64 - func changeExtension(path: AbsolutePath, to newExtension: String) throws -> AbsolutePath - func resolveSymlinks(_ path: AbsolutePath) throws -> AbsolutePath - func fileAttributes(at path: AbsolutePath) throws -> [FileAttributeKey: Any] - func filesAndDirectoriesContained(in path: AbsolutePath) throws -> [AbsolutePath]? - func zipItem(at sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws - func unzipItem(at sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws + func determineTemporaryDirectory() throws -> Path.AbsolutePath + func temporaryDirectory() throws -> Path.AbsolutePath + func inTemporaryDirectory(_ closure: @escaping (Path.AbsolutePath) async throws -> Void) async throws + func inTemporaryDirectory(_ closure: (Path.AbsolutePath) throws -> Void) throws + func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (Path.AbsolutePath) throws -> Void) throws + func inTemporaryDirectory(_ closure: (Path.AbsolutePath) throws -> Result) throws -> Result + func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (Path.AbsolutePath) throws -> Result) throws -> Result + func write(_ content: String, path: Path.AbsolutePath, atomically: Bool) throws + func locateDirectoryTraversingParents(from: Path.AbsolutePath, path: String) -> Path.AbsolutePath? + func locateDirectory(_ path: String, traversingFrom from: Path.AbsolutePath) throws -> Path.AbsolutePath? + func files( + in path: Path.AbsolutePath, + filter: ((URL) -> Bool)?, + nameFilter: Set?, + extensionFilter: Set? + ) -> Set + func files( + in path: Path.AbsolutePath, + nameFilter: Set?, + extensionFilter: Set? + ) -> Set + func glob(_ path: Path.AbsolutePath, glob: String) -> [Path.AbsolutePath] + func throwingGlob(_ path: Path.AbsolutePath, glob: String) throws -> [Path.AbsolutePath] + func linkFile(atPath: Path.AbsolutePath, toPath: Path.AbsolutePath) throws + func createFolder(_ path: Path.AbsolutePath) throws + func isFolder(_ path: Path.AbsolutePath) -> Bool + func touch(_ path: Path.AbsolutePath) throws + func contentsOfDirectory(_ path: Path.AbsolutePath) throws -> [Path.AbsolutePath] + func urlSafeBase64MD5(path: Path.AbsolutePath) throws -> String + func fileSize(path: Path.AbsolutePath) throws -> UInt64 + func changeExtension(path: Path.AbsolutePath, to newExtension: String) throws -> Path.AbsolutePath + func resolveSymlinks(_ path: Path.AbsolutePath) throws -> Path.AbsolutePath + func fileAttributes(at path: Path.AbsolutePath) throws -> [FileAttributeKey: Any] + func filesAndDirectoriesContained(in path: Path.AbsolutePath) throws -> [Path.AbsolutePath]? + func zipItem(at sourcePath: Path.AbsolutePath, to destinationPath: Path.AbsolutePath) throws + func unzipItem(at sourcePath: Path.AbsolutePath, to destinationPath: Path.AbsolutePath) throws +} + +extension FileHandling { + public func files( + in path: Path.AbsolutePath, + nameFilter: Set?, + extensionFilter: Set? + ) -> Set { + files(in: path, filter: nil, nameFilter: nameFilter, extensionFilter: extensionFilter) + } } public class FileHandler: FileHandling { // MARK: - Attributes - public static var shared: FileHandling = FileHandler() + public static var shared: FileHandling { + _shared.value + } + + // swiftlint:disable:next identifier_name + static let _shared: ThreadSafe = ThreadSafe(FileHandler()) + private let fileManager: FileManager private let propertyListDecoder = PropertyListDecoder() @@ -94,15 +124,15 @@ public class FileHandler: FileHandling { self.fileManager = fileManager } - public var currentPath: AbsolutePath { + public var currentPath: Path.AbsolutePath { try! AbsolutePath(validating: fileManager.currentDirectoryPath) // swiftlint:disable:this force_try } - public var homeDirectory: AbsolutePath { + public var homeDirectory: Path.AbsolutePath { try! AbsolutePath(validating: NSHomeDirectory()) // swiftlint:disable:this force_try } - public func replace(_ to: AbsolutePath, with: AbsolutePath) throws { + public func replace(_ to: Path.AbsolutePath, with: Path.AbsolutePath) throws { // To support cases where the destination is on a different volume // we need to create a temporary directory that is suitable // for performing a `replaceItemAt` @@ -123,62 +153,65 @@ public class FileHandler: FileHandling { _ = try fileManager.replaceItemAt(to.url, withItemAt: tempUrl) } - public func temporaryDirectory() throws -> AbsolutePath { + public func temporaryDirectory() throws -> Path.AbsolutePath { let directory = try TemporaryDirectory(removeTreeOnDeinit: false) return directory.path } - public func determineTemporaryDirectory() throws -> AbsolutePath { - try determineTempDirectory() + public func determineTemporaryDirectory() throws -> Path.AbsolutePath { + try .init(validating: determineTempDirectory().pathString) } - public func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Result) throws -> Result { - try withTemporaryDirectory(removeTreeOnDeinit: true, closure) + public func inTemporaryDirectory(_ closure: (Path.AbsolutePath) throws -> Result) throws -> Result { + try withTemporaryDirectory(removeTreeOnDeinit: true) { path in + try closure(.init(validating: path.pathString)) + } } - public func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Void) throws { - try withTemporaryDirectory(removeTreeOnDeinit: removeOnCompletion, closure) + public func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (Path.AbsolutePath) throws -> Void) throws { + try withTemporaryDirectory(removeTreeOnDeinit: removeOnCompletion) { path in + try closure(.init(validating: path.pathString)) + } } - public func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws { - try withTemporaryDirectory(removeTreeOnDeinit: true, closure) + public func inTemporaryDirectory(_ closure: (Path.AbsolutePath) throws -> Void) throws { + try withTemporaryDirectory(removeTreeOnDeinit: true) { path in + try closure(.init(validating: path.pathString)) + } } - public func inTemporaryDirectory(_ closure: @escaping (AbsolutePath) async throws -> Void) async throws { + public func inTemporaryDirectory(_ closure: @escaping (Path.AbsolutePath) async throws -> Void) async throws { let directory = try TemporaryDirectory(removeTreeOnDeinit: true) try await closure(directory.path) } public func inTemporaryDirectory( removeOnCompletion: Bool, - _ closure: (AbsolutePath) throws -> Result + _ closure: (Path.AbsolutePath) throws -> Result ) throws -> Result { - try withTemporaryDirectory(removeTreeOnDeinit: removeOnCompletion, closure) + try withTemporaryDirectory(removeTreeOnDeinit: removeOnCompletion) { path in + try closure(try .init(validating: path.pathString)) + } } - public func exists(_ path: AbsolutePath) -> Bool { + public func exists(_ path: Path.AbsolutePath) -> Bool { let exists = fileManager.fileExists(atPath: path.pathString) - logger.debug("Checking if \(path) exists... \(exists)") return exists } - public func copy(from: AbsolutePath, to: AbsolutePath) throws { - logger.debug("Copying file from \(from) to \(to)") + public func copy(from: Path.AbsolutePath, to: Path.AbsolutePath) throws { try fileManager.copyItem(atPath: from.pathString, toPath: to.pathString) } - public func move(from: AbsolutePath, to: AbsolutePath) throws { - logger.debug("Moving file from \(from) to \(to)") + public func move(from: Path.AbsolutePath, to: Path.AbsolutePath) throws { try fileManager.moveItem(atPath: from.pathString, toPath: to.pathString) } - public func readFile(_ at: AbsolutePath) throws -> Data { - logger.debug("Reading contents of file at path \(at)") + public func readFile(_ at: Path.AbsolutePath) throws -> Data { return try Data(contentsOf: at.url) } - public func readTextFile(_ at: AbsolutePath) throws -> String { - logger.debug("Reading contents of text file at path \(at)") + public func readTextFile(_ at: Path.AbsolutePath) throws -> String { let data = try Data(contentsOf: at.url) if let content = String(data: data, encoding: .utf8) { return content @@ -187,28 +220,32 @@ public class FileHandler: FileHandling { } } - public func readPlistFile(_ at: AbsolutePath) throws -> T { - logger.debug("Reading contents of plist file at path \(at)") + public func readPlistFile(_ at: Path.AbsolutePath) throws -> T { guard let data = fileManager.contents(atPath: at.pathString) else { throw FileHandlerError.fileNotFound(at) } - return try propertyListDecoder.decode(T.self, from: data) + do { + return try propertyListDecoder.decode(T.self, from: data) + } catch { + if let debugDescription = (error as NSError).userInfo["NSDebugDescription"] as? String { + throw FileHandlerError.propertyListDecodeError(at, description: debugDescription) + } else { + throw FileHandlerError.propertyListDecodeError(at, description: error.localizedDescription) + } + } } - public func linkFile(atPath: AbsolutePath, toPath: AbsolutePath) throws { - logger.debug("Creating a link from \(atPath) to \(toPath)") + public func linkFile(atPath: Path.AbsolutePath, toPath: Path.AbsolutePath) throws { try fileManager.linkItem(atPath: atPath.pathString, toPath: toPath.pathString) } - public func write(_ content: String, path: AbsolutePath, atomically: Bool) throws { - logger.debug("Writing contents to file \(path) atomically \(atomically)") + public func write(_ content: String, path: Path.AbsolutePath, atomically: Bool) throws { do { try content.write(to: path.url, atomically: atomically, encoding: .utf8) } catch {} } - public func locateDirectory(_ path: String, traversingFrom from: AbsolutePath) throws -> AbsolutePath? { - logger.debug("Traversing \(from) to locate \(path)") + public func locateDirectory(_ path: String, traversingFrom from: Path.AbsolutePath) throws -> Path.AbsolutePath? { let extendedPath = from.appending(try RelativePath(validating: path)) if exists(extendedPath) { return extendedPath @@ -219,16 +256,61 @@ public class FileHandler: FileHandling { } } - public func glob(_ path: AbsolutePath, glob: String) -> [AbsolutePath] { + public func files( + in path: Path.AbsolutePath, + filter: ((URL) -> Bool)?, + nameFilter: Set?, + extensionFilter: Set? + ) -> Set { + var results = Set() + + let enumerator = fileManager.enumerator( + at: path.url, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles, .skipsPackageDescendants] + ) + + func filterCandidate(with url: URL) -> Bool { + if let extensionFilter { + guard extensionFilter.contains(url.pathExtension) else { + return false + } + } + if let nameFilter { + guard nameFilter.contains(url.lastPathComponent) else { + return false + } + } + if let filter { + guard filter(url) else { + return false + } + } + return true + } + + while let candidateURL = enumerator?.nextObject() as? Foundation.URL { + guard filterCandidate(with: candidateURL) else { + continue + } + // Symlinks need to be resolved for resulting absolute URLs to point to the right place. + let url = candidateURL.resolvingSymlinksInPath() + let absolutePath = AbsolutePath(stringLiteral: url.path) + results.insert(absolutePath) + } + + return results + } + + public func glob(_ path: Path.AbsolutePath, glob: String) -> [Path.AbsolutePath] { path.glob(glob) } - public func throwingGlob(_ path: AbsolutePath, glob: String) throws -> [AbsolutePath] { + public func throwingGlob(_ path: Path.AbsolutePath, glob: String) throws -> [Path.AbsolutePath] { try path.throwingGlob(glob) } - public func createFolder(_ path: AbsolutePath) throws { - logger.debug("Creating folder at path \(path)") + public func createFolder(_ path: Path.AbsolutePath) throws { try fileManager.createDirectory( at: path.url, withIntermediateDirectories: true, @@ -236,15 +318,7 @@ public class FileHandler: FileHandling { ) } - public func delete(_ path: AbsolutePath) throws { - logger.debug("Deleting item at path \(path)") - if exists(path) { - try fileManager.removeItem(atPath: path.pathString) - } - } - - public func touch(_ path: AbsolutePath) throws { - logger.debug("Touching \(path)") + public func touch(_ path: Path.AbsolutePath) throws { try fileManager.createDirectory( at: path.removingLastComponent().url, withIntermediateDirectories: true, @@ -253,18 +327,16 @@ public class FileHandler: FileHandling { try Data().write(to: path.url) } - public func isFolder(_ path: AbsolutePath) -> Bool { + public func isFolder(_ path: Path.AbsolutePath) -> Bool { var isDirectory = ObjCBool(true) let exists = fileManager.fileExists(atPath: path.pathString, isDirectory: &isDirectory) return exists && isDirectory.boolValue } - public func locateDirectoryTraversingParents(from: AbsolutePath, path: String) -> AbsolutePath? { - logger.debug("Traversing \(from) to locate \(path)") - + public func locateDirectoryTraversingParents(from: Path.AbsolutePath, path: String) -> Path.AbsolutePath? { let configPath = from.appending(component: path) - let root = try! AbsolutePath(validating: "/") // swiftlint:disable:this force_try + let root = try! Path.AbsolutePath(validating: "/") // swiftlint:disable:this force_try if FileHandler.shared.exists(configPath) { return configPath } else if from == root { @@ -274,29 +346,29 @@ public class FileHandler: FileHandling { } } - public func contentsOfDirectory(_ path: AbsolutePath) throws -> [AbsolutePath] { + public func contentsOfDirectory(_ path: Path.AbsolutePath) throws -> [Path.AbsolutePath] { try fileManager.contentsOfDirectory(atPath: path.pathString).map { try AbsolutePath(validating: $0, relativeTo: path) } } - public func createSymbolicLink(at path: AbsolutePath, destination: AbsolutePath) throws { + public func createSymbolicLink(at path: Path.AbsolutePath, destination: Path.AbsolutePath) throws { try fileManager.createSymbolicLink(atPath: path.pathString, withDestinationPath: destination.pathString) } - public func resolveSymlinks(_ path: AbsolutePath) throws -> AbsolutePath { - try TSCBasic.resolveSymlinks(path) + public func resolveSymlinks(_ path: Path.AbsolutePath) throws -> Path.AbsolutePath { + try .init(validating: TSCBasic.resolveSymlinks(.init(validating: path.pathString)).pathString) } - public func fileAttributes(at path: AbsolutePath) throws -> [FileAttributeKey: Any] { + public func fileAttributes(at path: Path.AbsolutePath) throws -> [FileAttributeKey: Any] { try fileManager.attributesOfItem(atPath: path.pathString) } - public func filesAndDirectoriesContained(in path: AbsolutePath) throws -> [AbsolutePath]? { + public func filesAndDirectoriesContained(in path: Path.AbsolutePath) throws -> [Path.AbsolutePath]? { try fileManager.subpaths(atPath: path.pathString)?.map { path.appending(try RelativePath(validating: $0)) } } // MARK: - MD5 - public func urlSafeBase64MD5(path: AbsolutePath) throws -> String { + public func urlSafeBase64MD5(path: Path.AbsolutePath) throws -> String { let data = try Data(contentsOf: path.url) let digestData = Data(Insecure.MD5.hash(data: data)) return digestData.base64EncodedString() @@ -306,7 +378,7 @@ public class FileHandler: FileHandling { // MARK: - File Attributes - public func fileSize(path: AbsolutePath) throws -> UInt64 { + public func fileSize(path: Path.AbsolutePath) throws -> UInt64 { let attr = try fileManager.attributesOfItem(atPath: path.pathString) guard let size = attr[FileAttributeKey.size] as? UInt64 else { throw FileHandlerError.unreachableFileSize(path) } return size @@ -314,7 +386,7 @@ public class FileHandler: FileHandling { // MARK: - Extension - public func changeExtension(path: AbsolutePath, to fileExtension: String) throws -> AbsolutePath { + public func changeExtension(path: Path.AbsolutePath, to fileExtension: String) throws -> Path.AbsolutePath { guard isFolder(path) == false else { throw FileHandlerError.expectedAFile(path) } let sanitizedExtension = fileExtension.starts(with: ".") ? String(fileExtension.dropFirst()) : fileExtension guard path.extension != sanitizedExtension else { return path } @@ -324,11 +396,18 @@ public class FileHandler: FileHandling { return newPath } - public func zipItem(at sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - try fileManager.zipItem(at: sourcePath.asURL, to: destinationPath.asURL, shouldKeepParent: false) + public func zipItem(at sourcePath: Path.AbsolutePath, to destinationPath: Path.AbsolutePath) throws { + try fileManager.zipItem( + at: URL(fileURLWithPath: sourcePath.pathString), + to: URL(fileURLWithPath: destinationPath.pathString), + shouldKeepParent: false + ) } - public func unzipItem(at sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - try fileManager.unzipItem(at: sourcePath.asURL, to: destinationPath.asURL) + public func unzipItem(at sourcePath: Path.AbsolutePath, to destinationPath: Path.AbsolutePath) throws { + try fileManager.unzipItem( + at: URL(fileURLWithPath: sourcePath.pathString), + to: URL(fileURLWithPath: destinationPath.pathString) + ) } } diff --git a/Sources/TuistSupport/Utils/FileUnarchiver.swift b/Sources/TuistSupport/Utils/FileUnarchiver.swift index 8f3f5818a5a..29e75ff8f39 100644 --- a/Sources/TuistSupport/Utils/FileUnarchiver.swift +++ b/Sources/TuistSupport/Utils/FileUnarchiver.swift @@ -1,13 +1,15 @@ +import FileSystem import Foundation -import TSCBasic +import Mockable +import Path -/// An interface to unarchive files from a zip file. +@Mockable public protocol FileUnarchiving { /// Unarchives the files into a temporary directory and returns the path to that directory. func unzip() throws -> AbsolutePath /// Call this method to delete the temporary directory where the .zip file has been generated. - func delete() throws + func delete() async throws } public class FileUnarchiver: FileUnarchiving { @@ -17,10 +19,13 @@ public class FileUnarchiver: FileUnarchiving { /// Temporary directory in which the .zip file will be generated. private var temporaryDirectory: AbsolutePath + private let fileSystem: FileSystem + /// Initializes the unarchiver with the path to the file to unarchive. /// - Parameter path: Path to the .zip file to unarchive. - public init(path: AbsolutePath) throws { + public init(path: AbsolutePath, fileSystem: FileSystem = FileSystem()) throws { self.path = path + self.fileSystem = fileSystem temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false).path } @@ -29,7 +34,7 @@ public class FileUnarchiver: FileUnarchiving { return temporaryDirectory } - public func delete() throws { - try FileHandler.shared.delete(temporaryDirectory) + public func delete() async throws { + try await fileSystem.remove(temporaryDirectory) } } diff --git a/Sources/TuistSupport/Utils/GitEnvironment.swift b/Sources/TuistSupport/Utils/GitEnvironment.swift deleted file mode 100644 index 5aa488641b6..00000000000 --- a/Sources/TuistSupport/Utils/GitEnvironment.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Combine -import Foundation -import TSCUtility - -public protocol GitEnvironmenting { - /// Returns the authentication type that should be used with GitHub. - func githubAuthentication() -> AnyPublisher - - /// It returns the environment's Git credentials for GitHub. - /// This is useful for operations such pulling newer Tuist versions from GitHub - /// without hitting API limits. - func githubCredentials() -> AnyPublisher -} - -public enum GitHubAuthentication: Equatable { - /// Token-based authentication - case token(String) - - /// Username/Password-based authentication - case credentials(GithubCredentials) -} - -public struct GithubCredentials: Equatable { - /// GitHub password - let username: String - - /// GitHub username - let password: String -} - -public enum GitEnvironmentError: FatalError { - case githubCredentialsFillError(String) - - public var type: ErrorType { - switch self { - case .githubCredentialsFillError: return .bug - } - } - - public var description: String { - switch self { - case let .githubCredentialsFillError(message): - return "Trying to get your environment's credentials for https://github.com failed with the following error: \(message)" - } - } -} - -public class GitEnvironment: GitEnvironmenting { - let environment: () -> [String: String] - - public init(environment: @escaping () -> [String: String] = { ProcessInfo.processInfo.environment }) { - self.environment = environment - } - - public func githubAuthentication() -> AnyPublisher { - let env = environment() - if let environmentToken = env[Constants.EnvironmentVariables.githubAPIToken] { - return .init(value: .token(environmentToken)) - } else { - return githubCredentials().map { (credentials: GithubCredentials?) -> GitHubAuthentication? in - credentials.map { GitHubAuthentication.credentials($0) } - } - .eraseToAnyPublisher() - } - } - - // https://github.com/Carthage/Carthage/blob/19a7f97112052394f3ecc33dac3c67e5384b7514/Source/CarthageKit/GitHub.swift#L85 - public func githubCredentials() -> AnyPublisher { - System.shared.publisher( - ["/usr/bin/env", "echo", "url=https://github.com"], - pipeTo: ["/usr/bin/env", "git", "credential", "fill"] - ) - .mapToString() - .collectAndMergeOutput() - .flatMap { (output: String) -> AnyPublisher in - // protocol=https - // host=github.com - // username=pepibumur - // password=foo - let lines = output.split(separator: "\n") - let values = lines.reduce(into: [String: String]()) { result, next in - let components = next.split(separator: "=") - guard components.count == 2 else { return } - result[String(components.first!).spm_chomp()] = String(components.last!).spm_chomp() - } - guard let username = values["username"], - let password = values["password"] else { return AnyPublisher(value: nil) } - return AnyPublisher(value: GithubCredentials(username: username, password: password)) - } - .eraseToAnyPublisher() - } -} diff --git a/Sources/TuistSupport/Utils/GitHandler.swift b/Sources/TuistSupport/Utils/GitHandler.swift index dd84b7512bb..d0abfdd01ab 100644 --- a/Sources/TuistSupport/Utils/GitHandler.swift +++ b/Sources/TuistSupport/Utils/GitHandler.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TSCUtility public protocol GitHandling { @@ -42,11 +42,14 @@ public protocol GitHandling { /// Uses the system to execute git commands. public final class GitHandler: GitHandling { private let system: Systeming + private let environment: Environmenting public init( - system: Systeming = System.shared + system: Systeming = System.shared, + environment: Environmenting = Environment.shared ) { self.system = system + self.environment = environment } public func clone(url: String, into path: AbsolutePath) throws { @@ -75,7 +78,7 @@ public final class GitHandler: GitHandling { } private func run(command: String...) throws { - if Environment.shared.isVerbose { + if environment.isVerbose { try system.runAndPrint(command, verbose: true, environment: System.shared.env) } else { try system.run(command) @@ -83,7 +86,7 @@ public final class GitHandler: GitHandling { } private func capture(command: String...) throws -> String { - if Environment.shared.isVerbose { + if environment.isVerbose { return try system.capture(command, verbose: true, environment: System.shared.env) } else { return try system.capture(command) diff --git a/Sources/TuistSupport/Utils/Glob.swift b/Sources/TuistSupport/Utils/Glob.swift index c3792732bf9..145000d444a 100644 --- a/Sources/TuistSupport/Utils/Glob.swift +++ b/Sources/TuistSupport/Utils/Glob.swift @@ -7,37 +7,22 @@ import Foundation // swiftlint:disable:next identifier_name -public let GlobBehaviorBashV3 = Glob.Behavior( - supportsGlobstar: false, - includesFilesFromRootOfGlobstar: false, - includesDirectoriesInResults: true, - includesFilesInResultsIfTrailingSlash: false -) - -// swiftlint:disable:next identifier_name -public let GlobBehaviorBashV4 = Glob.Behavior( +let GlobBehaviorBashV4 = Glob.Behavior( supportsGlobstar: true, // Matches Bash v4 with "shopt -s globstar" option includesFilesFromRootOfGlobstar: true, includesDirectoriesInResults: true, includesFilesInResultsIfTrailingSlash: false ) -// swiftlint:disable:next identifier_name -public let GlobBehaviorGradle = Glob.Behavior( - supportsGlobstar: true, - includesFilesFromRootOfGlobstar: true, - includesDirectoriesInResults: false, - includesFilesInResultsIfTrailingSlash: true -) /** Finds files on the file system using pattern matching. */ -public class Glob: Collection { +final class Glob: Collection { /** * Different glob implementations have different behaviors, so the behavior of this * implementation is customizable. */ - public struct Behavior { + public struct Behavior: Sendable { // If true then a globstar ("**") causes matching to be done recursively in subdirectories. // If false then "**" is treated the same as "*" let supportsGlobstar: Bool @@ -66,16 +51,12 @@ public class Glob: Collection { } } - public static var defaultBehavior = GlobBehaviorBashV4 - - public let behavior: Behavior + public let behavior: Behavior = GlobBehaviorBashV4 var paths = [String]() public var startIndex: Int { paths.startIndex } public var endIndex: Int { paths.endIndex } - public init(pattern: String, behavior: Behavior = Glob.defaultBehavior) { - self.behavior = behavior - + public init(pattern: String) { var adjustedPattern = pattern let hasTrailingGlobstarSlash = pattern.hasSuffix("**/") var includeFiles = !hasTrailingGlobstarSlash @@ -113,15 +94,37 @@ public class Glob: Collection { } private func expandGlobstar(pattern: String) -> [String] { - guard pattern.contains("**") else { + // Split pattern string by slash to find globstar. + let patternComponents = pattern.components(separatedBy: "/") + + // We are only interested in the first globstar since that is where we want to separate the pattern string. + guard let pivot = patternComponents.firstIndex(of: "**") else { return [pattern] } var results = [String]() - var parts = pattern.components(separatedBy: "**") - let firstPart = parts.removeFirst() - var lastPart = parts.joined(separator: "**") + // Part before the first globstar + let firstPartLowerBound = 0 + let firstPartUpperBound = pivot + let firstPartComponents: ArraySlice = if firstPartLowerBound < firstPartUpperBound { + patternComponents[firstPartLowerBound ..< firstPartUpperBound] + } else { + [] + } + let firstPart = firstPartComponents.joined(separator: "/") + + // Part after the first globstar + let lastPartLowerBound = pivot + 1 + let lastPartUpperBound = patternComponents.count + let lastPartComponents: ArraySlice = if lastPartLowerBound < lastPartUpperBound { + patternComponents[lastPartLowerBound ..< lastPartUpperBound] + } else { + [] + } + var lastPart = lastPartComponents.joined(separator: "/") + + // Find subdirectories let fileManager = FileManager.default var directories = fileManager.subdirectoriesResolvingSymbolicLinks(atPath: firstPart) diff --git a/Sources/TuistSupport/Utils/HTTPRedirectListener.swift b/Sources/TuistSupport/Utils/HTTPRedirectListener.swift deleted file mode 100644 index 5d516fe853a..00000000000 --- a/Sources/TuistSupport/Utils/HTTPRedirectListener.swift +++ /dev/null @@ -1,137 +0,0 @@ -import Foundation -import Swifter -import TSCBasic - -public protocol HTTPRedirectListening: Any { - /// Starts an HTTP server at the given port and blocks the process until a request is sent to the given path. - /// - Parameters: - /// - port: Port for the HTTP server. - /// - path: Path we are expecting the browser to redirect the user to. - /// - redirectMessage: Text returned to the browser when it redirects the user to the given path. - /// - logoURL: The logo to show in the redirect page. - /// - Returns: Either the query parameterrs of the redirect URL, or an error if the HTTP server fails to start. - func listen(port: UInt16, path: String, redirectMessage: String, logoURL: URL) -> Swift - .Result<[String: String]?, HTTPRedirectListenerError> -} - -public enum HTTPRedirectListenerError: FatalError { - case httpServer(Error) - - /// Error type. - public var type: ErrorType { - switch self { - case .httpServer: return .abort - } - } - - /// Error description. - public var description: String { - switch self { - case let .httpServer(error): - return "The redirect HTTP server faild to start with the following error: \(error)." - } - } -} - -private var runningSemaphore: DispatchSemaphore! - -public final class HTTPRedirectListener: HTTPRedirectListening { - private let signalHandler: SignalHandling - - /// Default initializer. - public init(signalHandler: SignalHandling = SignalHandler()) { - self.signalHandler = signalHandler - } - - // MARK: - HTTPRedirectListening - - public func listen( - port: UInt16, - path: String, - redirectMessage: String, - logoURL: URL - ) -> Swift.Result<[String: String]?, HTTPRedirectListenerError> { - precondition( - runningSemaphore == nil, - "Trying to start a redirect server for localhost:\(port)\(path) when there's already one running." - ) - let httpServer = HttpServer() - var result: Swift.Result<[String: String]?, HTTPRedirectListenerError> = .success(nil) - - runningSemaphore = DispatchSemaphore(value: 0) - httpServer[path] = { request in - result = .success(request.queryParams.reduce(into: [String: String]()) { $0[$1.0] = $1.1 }) - DispatchQueue.global().async { runningSemaphore?.signal() } - return HttpResponse.ok(.html(self.html(logoURL: logoURL, redirectMessage: redirectMessage))) - } - - // Stop the server if the user sends an interruption signal by pressing CTRL+C - signalHandler.trap { _ in - runningSemaphore.signal() - } - - do { - logger.pretty("Press \(.keystroke("CTRL + C")) once to cancel the process.") - try httpServer.start(port) - runningSemaphore.wait() - } catch { - result = .failure(.httpServer(error)) - } - - runningSemaphore = nil - return result - } - - private func html(logoURL: URL, redirectMessage: String) -> String { - """ - - - - - - - Redirecting - - - - - -
- - -
-
-
-

- Open your terminal 👩‍💻 -

-
-

- \(redirectMessage) -

-
- -
-
- - - - """ - } -} diff --git a/Sources/TuistSupport/Utils/Opener.swift b/Sources/TuistSupport/Utils/Opener.swift index ea02cffdc7e..999b7d0d99d 100644 --- a/Sources/TuistSupport/Utils/Opener.swift +++ b/Sources/TuistSupport/Utils/Opener.swift @@ -1,5 +1,6 @@ import Foundation -import TSCBasic +import Mockable +import Path enum OpeningError: FatalError, Equatable { case notFound(AbsolutePath) @@ -19,6 +20,7 @@ enum OpeningError: FatalError, Equatable { } } +@Mockable public protocol Opening: AnyObject { func open(path: AbsolutePath, wait: Bool) throws func open(path: AbsolutePath) throws diff --git a/Sources/TuistSupport/Utils/PrintableString.swift b/Sources/TuistSupport/Utils/PrintableString.swift deleted file mode 100644 index 070912ea519..00000000000 --- a/Sources/TuistSupport/Utils/PrintableString.swift +++ /dev/null @@ -1,159 +0,0 @@ -import Foundation - -public struct PrintableString: Encodable, Decodable, Equatable { - /// Contains a string that can be interpolated with options. - let rawString: String - let pretty: String -} - -extension PrintableString: ExpressibleByStringLiteral { - public init(stringLiteral: String) { - rawString = stringLiteral - pretty = stringLiteral - } -} - -extension PrintableString: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - pretty - } - - public var debugDescription: String { - rawString - } -} - -extension PrintableString: ExpressibleByStringInterpolation { - public init(stringInterpolation: StringInterpolation) { - rawString = stringInterpolation.unformatted - pretty = stringInterpolation.string - } - - public struct StringInterpolation: StringInterpolationProtocol { - var unformatted: String - var string: String - - public init(literalCapacity _: Int, interpolationCount _: Int) { - string = "" - unformatted = "" - } - - public mutating func appendLiteral(_ literal: String) { - string.append(literal) - unformatted.append(literal) - } - - public mutating func appendInterpolation(_ token: PrintableString.Token) { - string.append(token.description) - unformatted.append(token.unformatted) - } - - public mutating func appendInterpolation(_ value: String) { - string.append(value) - unformatted.append(value) - } - - public mutating func appendInterpolation(_ value: CustomStringConvertible) { - string.append(value.description) - unformatted.append(value.description) - } - } -} - -extension PrintableString { - public indirect enum Token: ExpressibleByStringLiteral { - case raw(String) - case command(Token) - case keystroke(Token) - case bold(Token) - case error(Token) - case success(Token) - case warning(Token) - case info(Token) - - public init(stringLiteral: String) { - self = .raw(stringLiteral) - } - - public var unformatted: String { - switch self { - case let .raw(string): - return string - case let .command(token): - return token.description - case let .keystroke(token): - return token.description - case let .bold(token): - return token.description - case let .error(token): - return token.description - case let .success(token): - return token.description - case let .warning(token): - return token.description - case let .info(token): - return token.description - } - } - - public var description: String { - guard Environment.shared.shouldOutputBeColoured else { - return unformatted - } - - switch self { - case let .raw(string): - return string - case let .command(token), let .keystroke(token): - return token.description.cyan() - case let .bold(token): - return token.description.bold() - case let .error(token): - return token.description.red() - case let .success(token): - return token.description.green() - case let .warning(token): - return token.description.yellow() - case let .info(token): - return token.description.lightBlue() - } - } - } -} - -extension Logger { - /// Log a message passing with the `Logger.Level.notice` log level. - /// - /// `pretty` is always printed to the console, and is omitted to the logger as `notice`. - /// - /// - parameters: - /// - message: The message to be logged. `message` can be used with any string interpolation literal. - /// - metadata: One-off metadata to attach to this log message - /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it - /// defaults to `#file`). - /// - function: The function this log message originates from (there's usually no need to pass it explicitly as - /// it defaults to `#function`). - /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it - /// defaults to `#line`). - public func pretty( - _ message: @autoclosure () -> PrintableString, - metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #file, function: String = #function, line: UInt = #line - ) { - let printableString = message() - - log( - level: .notice, Logger.Message(stringLiteral: printableString.rawString), - metadata: metadata().map { $0.merging(.pretty, uniquingKeysWith: { $1 }) } ?? .pretty, - file: file, - function: function, - line: line - ) - - if Environment.shared.shouldOutputBeColoured { - FileHandle.standardOutput.print(printableString.pretty) - } else { - FileHandle.standardOutput.print(printableString.rawString) - } - } -} diff --git a/Sources/TuistSupport/Utils/TemporaryDirectory.swift b/Sources/TuistSupport/Utils/TemporaryDirectory.swift index 0f71362ba5a..eefa5f20248 100644 --- a/Sources/TuistSupport/Utils/TemporaryDirectory.swift +++ b/Sources/TuistSupport/Utils/TemporaryDirectory.swift @@ -1,4 +1,5 @@ import Foundation +import Path import TSCBasic import TSCLibc @@ -8,7 +9,7 @@ public final class TemporaryDirectory { let prefix: String /// The full path of the temporary directory. - public let path: AbsolutePath + public let path: Path.AbsolutePath /// If true, try to remove the whole directory tree before deallocating. let shouldRemoveTreeOnDeinit: Bool @@ -24,14 +25,15 @@ public final class TemporaryDirectory { /// /// - Throws: MakeDirectoryError public init( - dir: AbsolutePath? = nil, + dir: Path.AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false ) throws { shouldRemoveTreeOnDeinit = removeTreeOnDeinit self.prefix = prefix // Construct path to the temporary directory. - let path = try determineTempDirectory(dir).appending(try RelativePath(validating: prefix + ".XXXXXX")) + let path = try determineTempDirectory(dir.map { try .init(validating: $0.pathString) }) + .appending(try RelativePath(validating: prefix + ".XXXXXX")) // Convert path to a C style string terminating with null char to be an valid input // to mkdtemp method. The XXXXXX in this string will be replaced by a random string @@ -42,7 +44,7 @@ public final class TemporaryDirectory { throw MakeDirectoryError.other(errno) } - self.path = try AbsolutePath(validating: String(cString: template)) + self.path = try Path.AbsolutePath(validating: String(cString: template)) } /// Remove the temporary file before deallocating. diff --git a/Sources/TuistSupport/Utils/ThreadSafe.swift b/Sources/TuistSupport/Utils/ThreadSafe.swift new file mode 100644 index 00000000000..2f4330d70b3 --- /dev/null +++ b/Sources/TuistSupport/Utils/ThreadSafe.swift @@ -0,0 +1,60 @@ +import Foundation + +/// Type that ensures thread safe access to the underlying value. +public final class ThreadSafe: @unchecked Sendable { + private let _lock: UnsafeMutablePointer = { + let lock = UnsafeMutablePointer.allocate(capacity: 1) + lock.initialize(to: os_unfair_lock()) + return lock + }() + + private var _value: T + + /// Returns the value boxed by `ThreadSafe` + public var value: T { + return withValue { $0 } + } + + /** + Mutates in place the value boxed by `ThreadSafe` + + Example: + ``` + let array = ThreadSafe([1,2,3]) + array.mutate { $0.append(4) } + ``` + - Parameter with : block used to mutate the underlying value + */ + @discardableResult + public func mutate(_ body: (inout T) throws -> Result) rethrows -> Result { + os_unfair_lock_lock(_lock) + defer { os_unfair_lock_unlock(_lock) } + return try body(&_value) + } + + /// Like `mutate`, but passes the value as readonly, returning the result of the closure. + /// + /// Example: + /// ``` + /// let array = ThreadSafe([1, 2, 3]) + /// let sum = array.withValue { $0.reduce(0, +) } // 6 + /// ``` + public func withValue(_ body: (T) throws -> Result) rethrows -> Result { + return try mutate { + try body($0) + } + } + + /** + + Example: + ``` + let array = ThreadSafe([1,2,3]) // ThreadSafe> + let optional = ThreadSafe(nil) + let optionalString: ThreadSafe = ThreadSafe("Initial Value") + ``` + + - Parameter initial : initial value used within the Atmoic box + */ + public init(_ initial: T) { _value = initial } +} diff --git a/Sources/TuistSupport/Utils/WarningController.swift b/Sources/TuistSupport/Utils/WarningController.swift index afa4a2b88e1..b3aef82f645 100644 --- a/Sources/TuistSupport/Utils/WarningController.swift +++ b/Sources/TuistSupport/Utils/WarningController.swift @@ -4,7 +4,7 @@ import Foundation It represents the interface of a tool that can collect warnings during the execution of a program and flush them when the program decides. */ -public protocol WarningControlling { +public protocol WarningControlling: Sendable { /// Appends a new warning to the list of warnings to be shown. /// - Parameter warning: The warning to be appended. func append(warning: String) @@ -13,7 +13,7 @@ public protocol WarningControlling { func flush() } -public final class WarningController: WarningControlling { +public final class WarningController: WarningControlling, @unchecked Sendable { private let warningsQueue = DispatchQueue(label: "io.tuist.TuistSupport.WarningController") private var _warnings: Set = Set() private var warnings: Set { @@ -25,7 +25,7 @@ public final class WarningController: WarningControlling { } } - public static var shared: WarningControlling = WarningController() + public static let shared: WarningControlling = WarningController() init() {} diff --git a/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift index d0080767717..727a23677d9 100644 --- a/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift +++ b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCode+Extensions.swift @@ -1,12 +1,3 @@ -// https://github.com/rhodgkins/SwiftHTTPStatusCodes -// -// HTTPStatusCodes+Extensions.swift -// HTTPStatusCodes -// -// Created by Richard Hodgkins on 07/06/2016. -// Copyright © 2016 Rich H. All rights reserved. -// - import Foundation extension HTTPStatusCode { @@ -122,7 +113,8 @@ extension HTTPStatusCode { /// /// Used in the resumable requests proposal to resume aborted PUT or POST requests. /// - /// - seealso: [Original proposal](https://web.archive.org/web/20151013212135/http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal) + /// - seealso: [Original + /// proposal](https://web.archive.org/web/20151013212135/http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal) @available(*, unavailable, renamed: "earlyHints", message: "Replaced by RFC standard code with different meaning") public static let checkpoint = __Unavailable diff --git a/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCodes.swift b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCodes.swift index 122e34f2914..086773e3136 100644 --- a/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCodes.swift +++ b/Sources/TuistSupport/Vendored/HTTPStatusCodes/HTTPStatusCodes.swift @@ -6,13 +6,14 @@ // import Foundation -/// HTTP status codes as per the [IANA HTTP status code registry](http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml). +/// HTTP status codes as per the [IANA HTTP status code +/// registry](http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml). /// /// Last updated: Fri, 21 Sep 2018 16:10:10 GMT /// /// - seealso: [Wikipedia page - List of HTTP status codes](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) /// - seealso: [HTTP protocol standard - Status Code Definitions](https://tools.ietf.org/html/rfc2616#section-10) -@objc public enum HTTPStatusCode: Int { +@objc public enum HTTPStatusCode: Int, Sendable { /// Continue: 100 /// /// - seealso: [RFC7231, Section 6.2.1](http://www.iana.org/go/rfc7231#section-6.2.1) @@ -276,7 +277,8 @@ import Foundation /// /// **Category**: Internet Information Services /// - /// - seealso: [Error message when you try to log on to Exchange 2007 by using Outlook Web Access: "440 Login Timeout"](http://support.microsoft.com/kb/941201/en-us) + /// - seealso: [Error message when you try to log on to Exchange 2007 by using Outlook Web Access: "440 Login + /// Timeout"](http://support.microsoft.com/kb/941201/en-us) @objc(HTTPStatusCodeIISLoginTimeout) case iisLoginTimeout = 440 @@ -299,7 +301,8 @@ import Foundation /// Blocked by Windows Parental Controls: 450 /// - /// A Microsoft extension. This error is given when Windows Parental Controls are turned on and are blocking access to the given webpage. + /// A Microsoft extension. This error is given when Windows Parental Controls are turned on and are blocking access to the + /// given webpage. case blockedByWindowsParentalControls = 450 /// Unavailable For Legal Reasons: 451 @@ -323,16 +326,19 @@ import Foundation /// nginx HTTP To HTTPS: 497 /// - /// An expansion of the 400 Bad Request response code, used when the client has made a HTTP request to a port listening for HTTPS requests. + /// An expansion of the 400 Bad Request response code, used when the client has made a HTTP request to a port listening for + /// HTTPS requests. /// /// **Category**: nginx case nginxHTTPToHTTPS = 497 /// Token Expired: 498 /// - /// Returned by [ArcGIS for Server](https://en.wikipedia.org/wiki/ArcGIS_Server). A code of 498 indicates an expired or otherwise invalid token. + /// Returned by [ArcGIS for Server](https://en.wikipedia.org/wiki/ArcGIS_Server). A code of 498 indicates an expired or + /// otherwise invalid token. /// - /// - seealso: [Using token-based authentication](http://help.arcgis.com/en/arcgisserver/10.0/apis/soap/index.htm#Using_token_authentication.htm) + /// - seealso: [Using token-based + /// authentication](http://help.arcgis.com/en/arcgisserver/10.0/apis/soap/index.htm#Using_token_authentication.htm) case tokenExpired = 498 /// nginx Client Closed Request: 499 @@ -390,7 +396,8 @@ import Foundation /// Bandwidth Limit Exceeded: 509 /// - /// The server has exceeded the bandwidth specified by the server administrator; this is often used by shared hosting providers to limit the bandwidth of customers. + /// The server has exceeded the bandwidth specified by the server administrator; this is often used by shared hosting + /// providers to limit the bandwidth of customers. /// /// - seealso: case bandwidthLimitExceeded = 509 @@ -407,11 +414,13 @@ import Foundation /// Site is frozen: 530 /// - /// Used by the [Pantheon](https://en.wikipedia.org/wiki/Pantheon_(software)) web platform to indicate a site that has been frozen due to inactivity. + /// Used by the [Pantheon](https://en.wikipedia.org/wiki/Pantheon_(software)) web platform to indicate a site that has been + /// frozen due to inactivity. case siteIsFrozen = 530 /// Network Connect Timeout Error: 599 /// - /// This status code is not specified in any RFCs, but is used by some HTTP proxies to signal a network connect timeout behind the proxy to a client in front of the proxy. + /// This status code is not specified in any RFCs, but is used by some HTTP proxies to signal a network connect timeout behind + /// the proxy to a client in front of the proxy. case networkConnectTimeoutError = 599 } diff --git a/Sources/TuistSupport/Xcode/Xcode.swift b/Sources/TuistSupport/Xcode/Xcode.swift index 04ee58324ad..3d15604645d 100644 --- a/Sources/TuistSupport/Xcode/Xcode.swift +++ b/Sources/TuistSupport/Xcode/Xcode.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path public enum XcodeError: FatalError, Equatable { case infoPlistNotFound(AbsolutePath) @@ -73,3 +73,21 @@ public struct Xcode { self.infoPlist = infoPlist } } + +#if DEBUG + extension Xcode { + public static func test( + path: AbsolutePath = try! AbsolutePath(validating: "/Applications/Xcode.app"), // swiftlint:disable:this force_try + infoPlist: Xcode.InfoPlist = .test() + ) -> Xcode { + Xcode(path: path, infoPlist: infoPlist) + } + } + + extension Xcode.InfoPlist { + public static func test(version: String = "3.2.1") -> Xcode.InfoPlist { + Xcode.InfoPlist(version: version) + } + } + +#endif diff --git a/Sources/TuistSupport/Xcode/XcodeController.swift b/Sources/TuistSupport/Xcode/XcodeController.swift index d739d1f141a..8cf3662613b 100644 --- a/Sources/TuistSupport/Xcode/XcodeController.swift +++ b/Sources/TuistSupport/Xcode/XcodeController.swift @@ -1,8 +1,10 @@ import Foundation -import TSCBasic +import Mockable +import Path import TSCUtility -public protocol XcodeControlling { +@Mockable +public protocol XcodeControlling: Sendable { /// Returns the selected Xcode. It uses xcode-select to determine /// the Xcode that is selected in the environment. /// @@ -17,15 +19,25 @@ public protocol XcodeControlling { func selectedVersion() throws -> Version } -public class XcodeController: XcodeControlling { +public final class XcodeController: XcodeControlling, @unchecked Sendable { public init() {} /// Shared instance. - public static var shared: XcodeControlling = XcodeController() + public static var shared: XcodeControlling { + _shared.value + } + + // swiftlint:disable:next identifier_name + static let _shared: ThreadSafe = ThreadSafe(XcodeController()) /// Cached response of `xcode-select` command - @Atomic - private var selectedXcode: Xcode? + private let selectedXcode = ThrowableCaching { + guard let path = try? System.shared.capture(["xcode-select", "-p"]).spm_chomp() else { + return nil + } + + return try Xcode.read(path: try AbsolutePath(validating: path).parentDirectory.parentDirectory) + } /// Returns the selected Xcode. It uses xcode-select to determine /// the Xcode that is selected in the environment. @@ -33,19 +45,7 @@ public class XcodeController: XcodeControlling { /// - Returns: Selected Xcode. /// - Throws: An error if it can't be obtained. public func selected() throws -> Xcode? { - // Return cached value if available - if let cached = selectedXcode { - return cached - } - - // e.g. /Applications/Xcode.app/Contents/Developer - guard let path = try? System.shared.capture(["xcode-select", "-p"]).spm_chomp() else { - return nil - } - - let xcode = try Xcode.read(path: try AbsolutePath(validating: path).parentDirectory.parentDirectory) - selectedXcode = xcode - return xcode + return try selectedXcode.value } enum XcodeVersionError: FatalError { diff --git a/Sources/TuistSupportTesting/Credentials/MockCredentialsStore.swift b/Sources/TuistSupportTesting/Credentials/MockCredentialsStore.swift deleted file mode 100644 index b6606c92187..00000000000 --- a/Sources/TuistSupportTesting/Credentials/MockCredentialsStore.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -@testable import TuistSupport - -public final class MockCredentialsStore: CredentialsStoring { - public init() {} - public var credentials: [URL: Credentials] = [:] - - public func store(credentials: Credentials, serverURL: URL) throws { - self.credentials[serverURL] = credentials - } - - public func read(serverURL: URL) throws -> Credentials? { - credentials[serverURL] - } - - public func get(serverURL: URL) throws -> Credentials { - credentials[serverURL]! - } - - public func delete(serverURL: URL) throws { - credentials.removeValue(forKey: serverURL) - } -} diff --git a/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift b/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift index 24917b844a7..36335f6d855 100644 --- a/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift +++ b/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest @@ -122,6 +122,24 @@ extension XCTestCase { XCTFail("No error was thrown", file: file, line: line) } + public func XCTAssertThrowsSpecific( + _ closure: () async throws -> some Any, + _ error: Error, + file: StaticString = #file, + line: UInt = #line + ) async { + do { + _ = try await closure() + } catch let closureError as Error { + XCTAssertEqual(closureError, error, file: file, line: line) + return + } catch let closureError { + XCTFail("\(error) is not equal to: \(closureError)", file: file, line: line) + return + } + XCTFail("No error was thrown", file: file, line: line) + } + public func XCTAssertThrowsSpecific( _ closure: @autoclosure () async throws -> some Any, _ error: Error, @@ -140,6 +158,19 @@ extension XCTestCase { XCTFail("No error was thrown", file: file, line: line) } + public func XCTAssertThrows( + _ closure: @autoclosure () async throws -> some Any, + file: StaticString = #file, + line: UInt = #line + ) async { + do { + _ = try await closure() + XCTFail("No error was thrown", file: file, line: line) + } catch { + // Succeeded + } + } + public func XCTAssertCodableEqualToJson( _ subject: C, _ json: String, @@ -271,77 +302,6 @@ extension XCTestCase { return element } - // MARK: - HTTPResource - - public func XCTAssertHTTPResourceMethod( - _ resource: HTTPResource, - _ method: String, - file: StaticString = #file, - line: UInt = #line - ) { - let request = resource.request() - XCTAssertEqual( - request.httpMethod!, - method, - "Expected the HTTP request method \(method) but got \(request.httpMethod!)", - file: file, - line: line - ) - } - - public func XCTAssertHTTPResourceContainsHeader( - _ resource: HTTPResource, - header: String, - value: String, - file: StaticString = #file, - line: UInt = #line - ) { - let request = resource.request() - let headers = request.allHTTPHeaderFields ?? [:] - guard let headerValue = headers[header] else { - XCTFail("The request doesn't contain the header \(header)", file: file, line: line) - return - } - XCTAssertEqual( - headerValue, - value, - "Expected header \(header) to have value \(value) but got \(headerValue)", - file: file, - line: line - ) - } - - public func XCTAssertHTTPResourcePath( - _ resource: HTTPResource, - path: String, - file: StaticString = #file, - line: UInt = #line - ) { - let request = resource.request() - let url = request.url! - let components = URLComponents(string: url.absoluteString)! - let requestPath = components.path - XCTAssertEqual(requestPath, path, "Expected the path \(path) but got \(requestPath)", file: file, line: line) - } - - public func XCTAssertHTTPResourceURL( - _ resource: HTTPResource, - url: URL, - file: StaticString = #file, - line: UInt = #line - ) { - let request = resource.request() - let requestUrl = request.url! - let components = URLComponents(string: requestUrl.absoluteString)! - XCTAssertEqual( - components.url!, - url, - "Expected the URL \(url.absoluteString) but got \(components.url!)", - file: file, - line: line - ) - } - @discardableResult public func XCTAssertContainsElementOfType( _ collection: [Any], _ type: T.Type, diff --git a/Sources/TuistSupportTesting/HTTP/HTTPResource+TestData.swift b/Sources/TuistSupportTesting/HTTP/HTTPResource+TestData.swift deleted file mode 100644 index 7cad274e50a..00000000000 --- a/Sources/TuistSupportTesting/HTTP/HTTPResource+TestData.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import TuistSupport - -extension HTTPResource { - public static func void() -> HTTPResource { - HTTPResource { - URLRequest(url: URL(string: "https://test.tuist.io")!) - } parse: { _, _ in - () - } parseError: { _, _ in - fatalError("The code execution shouldn't have reached this point") - } - } - - public static func noop() -> HTTPResource { - HTTPResource { - URLRequest(url: URL(string: "https://test.tuist.io")!) - } parse: { _, _ in - () - } parseError: { _, _ in - TestError("noop HTTPResource") - } - } -} diff --git a/Sources/TuistSupportTesting/HTTP/MockFileClient.swift b/Sources/TuistSupportTesting/HTTP/MockFileClient.swift index a25bd0f8a28..02c8be98dd2 100644 --- a/Sources/TuistSupportTesting/HTTP/MockFileClient.swift +++ b/Sources/TuistSupportTesting/HTTP/MockFileClient.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport // swiftlint:disable large_tuple diff --git a/Sources/TuistSupportTesting/HTTP/MockHTTPRequestDispatcher.swift b/Sources/TuistSupportTesting/HTTP/MockHTTPRequestDispatcher.swift deleted file mode 100644 index c77e92a0700..00000000000 --- a/Sources/TuistSupportTesting/HTTP/MockHTTPRequestDispatcher.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Combine -import Foundation -import TuistSupport - -public class MockHTTPRequestDispatcher: HTTPRequestDispatching { - public var requests: [URLRequest] = [] - - public func dispatch(resource: HTTPResource) async throws -> (object: T, response: HTTPURLResponse) { - if T.self != Void.self { - fatalError( - """ - MockHTTPRequestDispatcher only supports resources with Void as its generic value. \ - Use HTTPResource.noop from TuistSupportTesting. - """ - ) - } - requests.append(resource.request()) - let response = HTTPURLResponse() - // swiftlint:disable:next force_cast - return (object: () as! T, response: response) - } -} diff --git a/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift b/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift index fc59a7c3060..37b847b77bd 100644 --- a/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift +++ b/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TSCUtility @testable import TuistSupport @@ -39,6 +39,7 @@ public final class MockSwiftPackageManagerController: SwiftPackageManagerControl invokedLoadPackageInfo = true return try loadPackageInfoStub?(path) ?? .init( + name: "Package", products: [], targets: [], platforms: [], diff --git a/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift b/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift index 6e54d165da5..0acb567a9c2 100644 --- a/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift +++ b/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift @@ -1,12 +1,13 @@ -import TSCBasic +import Path import TSCUtility -import TuistGraph +import XcodeGraph @testable import TuistSupport // MARK: - Test package extension PackageInfo { public static func test( + name: String = "Package", products: [Product] = [], targets: [Target] = [], platforms: [Platform] = [], @@ -15,6 +16,7 @@ extension PackageInfo { swiftLanguageVersions: [TSCUtility.Version]? = nil ) -> Self { .init( + name: name, products: products, targets: targets, platforms: platforms, @@ -576,6 +578,7 @@ extension PackageInfo { public static var test: PackageInfo { .init( + name: "tuist", products: [ .init(name: "Tuist", type: .library(.static), targets: ["Tuist"]), ], @@ -691,6 +694,7 @@ extension PackageInfo { public static var aDependency: PackageInfo { .init( + name: "ALibrary", products: [ .init(name: "ALibrary", type: .library(.automatic), targets: ["ALibrary", "ALibraryUtils"]), ], @@ -737,6 +741,7 @@ extension PackageInfo { static var anotherDependency: PackageInfo { .init( + name: "AnotherLibrary", products: [ .init(name: "AnotherLibrary", type: .library(.automatic), targets: ["AnotherLibrary"]), ], @@ -893,6 +898,7 @@ extension PackageInfo { public static var alamofire: PackageInfo { .init( + name: "Alamofire", products: [ .init(name: "Alamofire", type: .library(.automatic), targets: ["Alamofire"]), ], @@ -1251,6 +1257,7 @@ extension PackageInfo { public static var googleAppMeasurement: PackageInfo { .init( + name: "GoogleAppMeasurement", products: [ .init( name: "GoogleAppMeasurement", @@ -1392,6 +1399,7 @@ extension PackageInfo { public static var googleUtilities: PackageInfo { .init( + name: "GoogleUtilities", products: [ .init(name: "GULAppDelegateSwizzler", type: .library(.automatic), targets: ["GULAppDelegateSwizzler"]), .init(name: "GULMethodSwizzler", type: .library(.automatic), targets: ["GULMethodSwizzler"]), @@ -1463,6 +1471,7 @@ extension PackageInfo { public static var nanopb: PackageInfo { .init( + name: "nanopb", products: [ .init(name: "nanopb", type: .library(.automatic), targets: ["nanopb"]), ], diff --git a/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift b/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift index 80616164846..07e0a26d806 100644 --- a/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift +++ b/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift @@ -1,5 +1,6 @@ +import Difference import Foundation -import TSCBasic +import Path import XCTest @testable import TuistSupport @@ -90,6 +91,24 @@ public final class MockFileHandler: FileHandler { try closure(temporaryDirectory()) } + public var stubFiles: ((AbsolutePath, ((URL) -> Bool)?, Set?, Set?) -> Set)? + override public func files( + in path: AbsolutePath, + filter: ((URL) -> Bool)?, + nameFilter: Set?, + extensionFilter: Set? + ) -> Set { + guard let stubFiles else { + return super.files( + in: path, + filter: filter, + nameFilter: nameFilter, + extensionFilter: extensionFilter + ) + } + return stubFiles(path, filter, nameFilter, extensionFilter) + } + public var stubGlob: ((AbsolutePath, String) -> [AbsolutePath])? override public func glob(_ path: AbsolutePath, glob: String) -> [AbsolutePath] { guard let stubGlob else { @@ -118,14 +137,14 @@ open class TuistTestCase: XCTestCase { do { // Environment environment = try MockEnvironment() - Environment.shared = environment + Environment._shared.mutate { $0 = environment } } catch { XCTFail("Failed to setup environment") } // FileHandler fileHandler = MockFileHandler(temporaryDirectory: { try self.temporaryPath() }) - FileHandler.shared = fileHandler + FileHandler._shared.mutate { $0 = fileHandler } } override open func tearDown() { @@ -167,6 +186,26 @@ open class TuistTestCase: XCTestCase { return paths } + public func XCTAssertBetterEqual( + _ expected: @autoclosure () throws -> T, + _ received: @autoclosure () throws -> T, + file: StaticString = #filePath, + line: UInt = #line + ) { + do { + let expected = try expected() + let received = try received() + XCTAssertTrue( + expected == received, + "Found difference for \n" + diff(expected, received).joined(separator: ", "), + file: file, + line: line + ) + } catch { + XCTFail("Caught error while testing: \(error)", file: file, line: line) + } + } + public func XCTAssertPrinterOutputContains(_ expected: String, file: StaticString = #file, line: UInt = #line) { XCTAssertPrinterContains(expected, at: .warning, >=, file: file, line: line) } diff --git a/Sources/TuistSupportTesting/TestCase/TuistUnitTestCase.swift b/Sources/TuistSupportTesting/TestCase/TuistUnitTestCase.swift index 1e3b498dfe0..58098cbba39 100644 --- a/Sources/TuistSupportTesting/TestCase/TuistUnitTestCase.swift +++ b/Sources/TuistSupportTesting/TestCase/TuistUnitTestCase.swift @@ -6,39 +6,46 @@ import XCTest open class TuistUnitTestCase: TuistTestCase { public var system: MockSystem! public var developerEnvironment: MockDeveloperEnvironment! - public var xcodeController: MockXcodeController! + public var xcodeController: MockXcodeControlling! + public var swiftVersionProvider: MockSwiftVersionProviding! override open func setUp() { super.setUp() // System system = MockSystem() - System.shared = system + System._shared.mutate { $0 = system } + + swiftVersionProvider = MockSwiftVersionProviding() + SwiftVersionProvider._shared.mutate { $0 = swiftVersionProvider } // Xcode controller - xcodeController = MockXcodeController() - XcodeController.shared = xcodeController + xcodeController = MockXcodeControlling() + XcodeController._shared.mutate { $0 = xcodeController } // Developer environment developerEnvironment = MockDeveloperEnvironment() - DeveloperEnvironment.shared = developerEnvironment + DeveloperEnvironment._shared.mutate { $0 = developerEnvironment } } override open func tearDown() { // System system = nil - System.shared = System() + System._shared.mutate { $0 = System() } + + swiftVersionProvider = nil + SwiftVersionProvider._shared.mutate { $0 = SwiftVersionProvider(System.shared) } // Xcode controller xcodeController = nil - XcodeController.shared = XcodeController() + XcodeController._shared.mutate { $0 = XcodeController() } // Environment environment = nil - Environment.shared = Environment() + Environment._shared.mutate { $0 = Environment() } // Developer environment developerEnvironment = nil - DeveloperEnvironment.shared = DeveloperEnvironment() + DeveloperEnvironment._shared.mutate { $0 = DeveloperEnvironment() } super.tearDown() } diff --git a/Sources/TuistSupportTesting/Utils/MockCIChecker.swift b/Sources/TuistSupportTesting/Utils/MockCIChecker.swift deleted file mode 100644 index c55a68f9e8e..00000000000 --- a/Sources/TuistSupportTesting/Utils/MockCIChecker.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import TuistSupport - -public final class MockCIChecker: CIChecking { - var isCIStub: Bool = false - - public func isCI() -> Bool { - isCIStub - } -} diff --git a/Sources/TuistSupportTesting/Utils/MockDerivedDataLocator.swift b/Sources/TuistSupportTesting/Utils/MockDerivedDataLocator.swift index 165b1227eeb..190f253ffcf 100644 --- a/Sources/TuistSupportTesting/Utils/MockDerivedDataLocator.swift +++ b/Sources/TuistSupportTesting/Utils/MockDerivedDataLocator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport public final class MockDerivedDataLocator: DerivedDataLocating { diff --git a/Sources/TuistSupportTesting/Utils/MockDeveloperEnvironment.swift b/Sources/TuistSupportTesting/Utils/MockDeveloperEnvironment.swift index 99dc031bef0..c5443aac229 100644 --- a/Sources/TuistSupportTesting/Utils/MockDeveloperEnvironment.swift +++ b/Sources/TuistSupportTesting/Utils/MockDeveloperEnvironment.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path @testable import TuistSupport public final class MockDeveloperEnvironment: DeveloperEnvironmenting { diff --git a/Sources/TuistSupportTesting/Utils/MockEnvironment.swift b/Sources/TuistSupportTesting/Utils/MockEnvironment.swift index 8faa0156186..73f1c637cfb 100644 --- a/Sources/TuistSupportTesting/Utils/MockEnvironment.swift +++ b/Sources/TuistSupportTesting/Utils/MockEnvironment.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest @@ -22,7 +22,6 @@ public class MockEnvironment: Environmenting { public var shouldOutputBeColoured: Bool = false public var isStandardOutputInteractive: Bool = false public var tuistVariables: [String: String] = [:] - public var tuistConfigVariables: [String: String] = [:] public var manifestLoadingVariables: [String: String] = [:] public var isStatsEnabled: Bool = true public var isGitHubActions: Bool = false @@ -39,6 +38,10 @@ public class MockEnvironment: Environmenting { nil } + public var cacheDirectory: AbsolutePath? { + directory.path.appending(components: ".cache") + } + public var queueDirectory: AbsolutePath { queueDirectoryStub ?? directory.path.appending(component: Constants.AsyncQueue.directoryName) } diff --git a/Sources/TuistSupportTesting/Utils/MockFileArchivingFactory.swift b/Sources/TuistSupportTesting/Utils/MockFileArchivingFactory.swift deleted file mode 100644 index 0cc49ef5dfb..00000000000 --- a/Sources/TuistSupportTesting/Utils/MockFileArchivingFactory.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -public class MockFileArchivingFactory: FileArchivingFactorying { - public init() {} - - public var invokedMakeFileArchiver = false - public var invokedMakeFileArchiverCount = 0 - public var invokedMakeFileArchiverParameters: (paths: [AbsolutePath], Void)? - public var invokedMakeFileArchiverParametersList = [(paths: [AbsolutePath], Void)]() - public var stubbedMakeFileArchiverError: Error? - public var stubbedMakeFileArchiverResult: FileArchiving! - - public func makeFileArchiver(for paths: [AbsolutePath]) throws -> FileArchiving { - invokedMakeFileArchiver = true - invokedMakeFileArchiverCount += 1 - invokedMakeFileArchiverParameters = (paths, ()) - invokedMakeFileArchiverParametersList.append((paths, ())) - if let error = stubbedMakeFileArchiverError { - throw error - } - return stubbedMakeFileArchiverResult - } - - public var invokedMakeFileUnarchiver = false - public var invokedMakeFileUnarchiverCount = 0 - public var invokedMakeFileUnarchiverParameters: (path: AbsolutePath, Void)? - public var invokedMakeFileUnarchiverParametersList = [(path: AbsolutePath, Void)]() - public var stubbedMakeFileUnarchiverError: Error? - public var stubbedMakeFileUnarchiverResult: FileUnarchiving! - - public func makeFileUnarchiver(for path: AbsolutePath) throws -> FileUnarchiving { - invokedMakeFileUnarchiver = true - invokedMakeFileUnarchiverCount += 1 - invokedMakeFileUnarchiverParameters = (path, ()) - invokedMakeFileUnarchiverParametersList.append((path, ())) - if let error = stubbedMakeFileUnarchiverError { - throw error - } - return stubbedMakeFileUnarchiverResult - } -} - -public class MockFileArchiver: FileArchiving { - public init() {} - - public var invokedZip = false - public var invokedZipCount = 0 - public var invokedZipParameters: (name: String, Void)? - public var invokedZipParametersList = [(name: String, Void)]() - public var stubbedZipError: Error? - public var stubbedZipResult: AbsolutePath! - - public func zip(name: String) throws -> AbsolutePath { - invokedZip = true - invokedZipCount += 1 - invokedZipParameters = (name, ()) - invokedZipParametersList.append((name, ())) - if let error = stubbedZipError { - throw error - } - return stubbedZipResult - } - - public var invokedDelete = false - public var invokedDeleteCount = 0 - public var stubbedDeleteError: Error? - - public func delete() throws { - invokedDelete = true - invokedDeleteCount += 1 - if let error = stubbedDeleteError { - throw error - } - } -} - -public class MockFileUnarchiver: FileUnarchiving { - public init() {} - - public var invokedUnzip = false - public var invokedUnzipCount = 0 - public var stubbedUnzipError: Error? - public var stubbedUnzipResult: AbsolutePath! - - public func unzip() throws -> AbsolutePath { - invokedUnzip = true - invokedUnzipCount += 1 - if let error = stubbedUnzipError { - throw error - } - return stubbedUnzipResult - } - - public var invokedDelete = false - public var invokedDeleteCount = 0 - public var stubbedDeleteError: Error? - - public func delete() throws { - invokedDelete = true - invokedDeleteCount += 1 - if let error = stubbedDeleteError { - throw error - } - } -} diff --git a/Sources/TuistSupportTesting/Utils/MockGitEnvironment.swift b/Sources/TuistSupportTesting/Utils/MockGitEnvironment.swift deleted file mode 100644 index 67d88e83269..00000000000 --- a/Sources/TuistSupportTesting/Utils/MockGitEnvironment.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Combine -import Foundation -import TuistSupport - -public final class MockGitEnvironment: GitEnvironmenting { - public var invokedGithubAuthentication = false - public var invokedGithubAuthenticationCount = 0 - public var stubbedGithubAuthenticationResult: Result! - - public func githubAuthentication() -> AnyPublisher { - invokedGithubAuthentication = true - invokedGithubAuthenticationCount += 1 - return .init(result: stubbedGithubAuthenticationResult) - } - - public var invokedGithubCredentials = false - public var invokedGithubCredentialsCount = 0 - public var stubbedGithubCredentialsResult: Result! - - public func githubCredentials() -> AnyPublisher { - invokedGithubCredentials = true - invokedGithubCredentialsCount += 1 - return .init(result: stubbedGithubCredentialsResult) - } -} diff --git a/Sources/TuistSupportTesting/Utils/MockGitHandler.swift b/Sources/TuistSupportTesting/Utils/MockGitHandler.swift index 07ee49e2e0a..f9946552837 100644 --- a/Sources/TuistSupportTesting/Utils/MockGitHandler.swift +++ b/Sources/TuistSupportTesting/Utils/MockGitHandler.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TSCUtility import TuistSupport diff --git a/Sources/TuistSupportTesting/Utils/MockHTTPRedirectListener.swift b/Sources/TuistSupportTesting/Utils/MockHTTPRedirectListener.swift deleted file mode 100644 index 57180f77b8c..00000000000 --- a/Sources/TuistSupportTesting/Utils/MockHTTPRedirectListener.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -@testable import TuistSupport - -public final class MockHTTPRedirectListener: HTTPRedirectListening { - public init() {} - - public var listenStub: ((UInt16, String, String, URL) -> Swift.Result<[String: String]?, HTTPRedirectListenerError>)? - - public func listen( - port: UInt16, - path: String, - redirectMessage: String, - logoURL: URL - ) -> Swift.Result<[String: String]?, HTTPRedirectListenerError> { - if let listenStub { - return listenStub(port, path, redirectMessage, logoURL) - } else { - return Result.failure(.httpServer(TestError("non-stubbed called to listen"))) - } - } -} diff --git a/Sources/TuistSupportTesting/Utils/MockOpener.swift b/Sources/TuistSupportTesting/Utils/MockOpener.swift deleted file mode 100644 index a24f402fa9a..00000000000 --- a/Sources/TuistSupportTesting/Utils/MockOpener.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport - -// swiftlint:disable large_tuple - -public final class MockOpener: Opening { - var openStub: Error? - var openArgs: [(String, Bool, AbsolutePath?)] = [] - var openCallCount: UInt = 0 - - public func open(target: String, wait: Bool) throws { - openCallCount += 1 - openArgs.append((target, wait, nil)) - if let openStub { throw openStub } - } - - public func open(path: AbsolutePath, wait: Bool) throws { - try open(target: path.pathString, wait: wait) - } - - public func open(path: AbsolutePath) throws { - try open(target: path.pathString, wait: false) - } - - public func open(url: URL) throws { - try open(target: url.absoluteString, wait: false) - } - - public func open(path: AbsolutePath, application: AbsolutePath) throws { - try open(path: path, application: application, wait: true) - } - - public func open(path: AbsolutePath, application: AbsolutePath, wait _: Bool) throws { - openCallCount += 1 - openArgs.append((path.pathString, false, application)) - if let openStub { throw openStub } - } -} - -// swiftlint:enable large_tuple diff --git a/Sources/TuistSupportTesting/Utils/MockSystem.swift b/Sources/TuistSupportTesting/Utils/MockSystem.swift index 1d1a80eead8..0aa7391ceae 100644 --- a/Sources/TuistSupportTesting/Utils/MockSystem.swift +++ b/Sources/TuistSupportTesting/Utils/MockSystem.swift @@ -1,5 +1,5 @@ -import Combine import Foundation +import Path import TSCBasic import XCTest @testable import TuistSupport @@ -11,8 +11,6 @@ public final class MockSystem: Systeming { public var stubs: [String: (stderror: String?, stdout: String?, exitstatus: Int?)] = [:] private var calls: [String] = [] public var whichStub: ((String) throws -> String?)? - public var swiftVersionStub: (() throws -> String)? - public var swiftlangVersionStub: (() throws -> String)? public init() {} @@ -61,85 +59,31 @@ public final class MockSystem: Systeming { _ = try capture(arguments, verbose: false, environment: env) } - public func runAndCollectOutput(_ arguments: [String]) async throws -> SystemCollectedOutput { - var values = publisher(arguments) - .mapToString() - .collectOutput() - .eraseToAnyPublisher() - .stream - .makeAsyncIterator() - - return try await values.next()! - } - - public func publisher(_ arguments: [String]) -> AnyPublisher, Error> { - publisher(arguments, verbose: false, environment: env) - } - - public func publisher( + public func run( _ arguments: [String], verbose _: Bool, - environment _: [String: String] - ) -> AnyPublisher, Error> { - AnyPublisher, Error>.create { subscriber in - let command = arguments.joined(separator: " ") - guard let stub = self.stubs[command] else { - subscriber - .send(completion: .failure( - TuistSupport.SystemError - .terminated(command: arguments.first!, code: 1, standardError: Data()) - )) - return AnyCancellable {} - } - guard stub.exitstatus == 0 else { - if let error = stub.stderror { - subscriber.send(.standardError(error.data(using: .utf8)!)) - } - subscriber - .send(completion: .failure( - TuistSupport.SystemError - .terminated(command: arguments.first!, code: 1, standardError: Data()) - )) - - return AnyCancellable {} - } - if let stdout = stub.stdout { - subscriber.send(.standardOutput(stdout.data(using: .utf8)!)) - } - subscriber.send(completion: .finished) - return AnyCancellable {} - } + environment _: [String: String], + redirection _: TSCBasic.Process.OutputRedirection + ) throws { + _ = try capture(arguments, verbose: false, environment: env) } - public func publisher(_ arguments: [String], pipeTo _: [String]) -> AnyPublisher, Error> { - AnyPublisher, Error>.create { subscriber in - let command = arguments.joined(separator: " ") - guard let stub = self.stubs[command] else { - subscriber - .send(completion: .failure( - TuistSupport.SystemError - .terminated(command: arguments.first!, code: 1, standardError: Data()) - )) - return AnyCancellable {} - } - guard stub.exitstatus == 0 else { - if let error = stub.stderror { - subscriber.send(.standardError(error.data(using: .utf8)!)) - } - subscriber - .send(completion: .failure( - TuistSupport.SystemError - .terminated(command: arguments.first!, code: 1, standardError: Data()) - )) - - return AnyCancellable {} - } - if let stdout = stub.stdout { - subscriber.send(.standardOutput(stdout.data(using: .utf8)!)) - } - subscriber.send(completion: .finished) - return AnyCancellable {} + public func runAndCollectOutput(_ arguments: [String]) async throws -> SystemCollectedOutput { + let command = arguments.joined(separator: " ") + guard let stub = stubs[command] else { + throw TuistSupport.SystemError + .terminated(command: arguments.first!, code: 1, standardError: Data()) + } + + guard stub.exitstatus == 0 else { + throw TuistSupport.SystemError + .terminated(command: arguments.first!, code: 1, standardError: stub.stderror?.data(using: .utf8) ?? Data()) } + + return SystemCollectedOutput( + standardOutput: stub.stdout ?? "", + standardError: stub.stderror ?? "" + ) } public func async(_ arguments: [String]) throws { @@ -152,22 +96,6 @@ public final class MockSystem: Systeming { } } - public func swiftVersion() throws -> String { - if let swiftVersionStub { - return try swiftVersionStub() - } else { - throw TestError("Call to non-stubbed method swiftVersion") - } - } - - public func swiftlangVersion() throws -> String { - if let swiftlangVersion = swiftlangVersionStub { - return try swiftlangVersion() - } else { - throw TestError("Call to non-stubbed method swiftlangVersion") - } - } - public func which(_ name: String) throws -> String { if let path = try whichStub?(name) { return path @@ -176,10 +104,10 @@ public final class MockSystem: Systeming { } } - public var chmodStub: ((FileMode, AbsolutePath, Set) throws -> Void)? + public var chmodStub: ((FileMode, Path.AbsolutePath, Set) throws -> Void)? public func chmod( _ mode: FileMode, - path: AbsolutePath, + path: Path.AbsolutePath, options: Set ) throws { try chmodStub?(mode, path, options) diff --git a/Sources/TuistSupportTesting/Utils/TestingLogHandler.swift b/Sources/TuistSupportTesting/Utils/TestingLogHandler.swift index d4277a8db65..5a9edfe16cc 100644 --- a/Sources/TuistSupportTesting/Utils/TestingLogHandler.swift +++ b/Sources/TuistSupportTesting/Utils/TestingLogHandler.swift @@ -2,7 +2,7 @@ import Foundation import TuistSupport public struct TestingLogHandler: LogHandler { - static var collected: [Logger.Level: [String]] { + public static var collected: [Logger.Level: [String]] { collectionQueue.sync { collectedLogs } diff --git a/Sources/TuistSupportTesting/Xcode/Mocks/MockXcodeController.swift b/Sources/TuistSupportTesting/Xcode/Mocks/MockXcodeController.swift deleted file mode 100644 index 259dcc702c5..00000000000 --- a/Sources/TuistSupportTesting/Xcode/Mocks/MockXcodeController.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import struct TSCUtility.Version -import TuistSupport -import XCTest - -public final class MockXcodeController: XcodeControlling { - public var selectedStub: Result? - public var selectedVersionStub: Result = .success(Version(0, 0, 0)) - - public func selected() throws -> Xcode? { - guard let selectedStub else { return nil } - - switch selectedStub { - case let .failure(error): throw error - case let .success(xcode): return xcode - } - } - - public func selectedVersion() throws -> Version { - switch selectedVersionStub { - case let .failure(error): throw error - case let .success(version): return version - } - } -} diff --git a/Sources/TuistSupportTesting/Xcode/TestData/Xcode+TestData.swift b/Sources/TuistSupportTesting/Xcode/TestData/Xcode+TestData.swift deleted file mode 100644 index 3a1539716ff..00000000000 --- a/Sources/TuistSupportTesting/Xcode/TestData/Xcode+TestData.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import TSCBasic - -import TuistSupport - -extension Xcode { - static func test( - path: AbsolutePath = try! AbsolutePath(validating: "/Applications/Xcode.app"), // swiftlint:disable:this force_try - infoPlist: Xcode.InfoPlist = .test() - ) -> Xcode { - Xcode(path: path, infoPlist: infoPlist) - } -} - -extension Xcode.InfoPlist { - static func test(version: String = "3.2.1") -> Xcode.InfoPlist { - Xcode.InfoPlist(version: version) - } -} diff --git a/Sources/tuist/TuistApp.swift b/Sources/tuist/TuistApp.swift index addec28f353..87dfc7e0af7 100644 --- a/Sources/tuist/TuistApp.swift +++ b/Sources/tuist/TuistApp.swift @@ -1,4 +1,5 @@ import Foundation +import Path import TSCBasic import TuistKit import TuistLoader @@ -6,16 +7,29 @@ import TuistSupport @main @_documentation(visibility: private) -private enum TuistApp { +private enum TuistServer { static func main() async throws { if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar(Constants.EnvironmentVariables.verbose, value: "true") } - TuistSupport.LogOutput.bootstrap() + let machineReadableCommands = [DumpCommand.self] + // swiftformat:disable all + let isCommandMachineReadable = CommandLine.arguments.count > 1 && machineReadableCommands.map { $0._commandName }.contains(CommandLine.arguments[1]) + // swiftformat:enable all + if isCommandMachineReadable || CommandLine.arguments.contains("--json") { + TuistSupport.LogOutput.bootstrap( + config: LoggingConfig( + loggerType: .json, + verbose: ProcessEnv.vars[Constants.EnvironmentVariables.verbose] != nil + ) + ) + } else { + TuistSupport.LogOutput.bootstrap() + } try TuistSupport.Environment.shared.bootstrap() - await TuistCommand.main() + try await TuistCommand.main() } } diff --git a/Sources/tuistbenchmark/Benchmark/Measure.swift b/Sources/tuistbenchmark/Benchmark/Measure.swift index 98208a2c9d0..d3872b0f112 100644 --- a/Sources/tuistbenchmark/Benchmark/Measure.swift +++ b/Sources/tuistbenchmark/Benchmark/Measure.swift @@ -1,5 +1,6 @@ import Foundation import TSCBasic +import TSCUtility struct MeasureResult { var fixture: String @@ -49,6 +50,7 @@ final class Measure { fixturePath: AbsolutePath ) throws -> [TimeInterval] { try (0 ..< runs).map { _ in + try withTemporaryDirectory(removeTreeOnDeinit: true) { temporaryDirectoryPath in let temporaryPath = temporaryDirectoryPath.appending(component: "fixture") try fileHandler.copy(path: fixturePath, to: temporaryPath) diff --git a/Sources/tuistbenchmark/main.swift b/Sources/tuistbenchmark/main.swift index 0f82ad68d27..ec2ee192ad8 100644 --- a/Sources/tuistbenchmark/main.swift +++ b/Sources/tuistbenchmark/main.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import TSCBasic +import Path import TSCUtility public struct TuistBenchmarkCommand: ParsableCommand { diff --git a/Sources/tuistenv/main.swift b/Sources/tuistenv/main.swift deleted file mode 100644 index 9e7d74a134f..00000000000 --- a/Sources/tuistenv/main.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import TuistEnvKit -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 cd868cad6a3..62c68870b1e 100644 --- a/Sources/tuistfixturegenerator/Generator/Generator.swift +++ b/Sources/tuistfixturegenerator/Generator/Generator.swift @@ -1,5 +1,6 @@ import Foundation import TSCBasic +import TSCUtility class Generator { private let fileSystem: FileSystem diff --git a/Sources/tuistfixturegenerator/Templates/ManifestTemplate.swift b/Sources/tuistfixturegenerator/Templates/ManifestTemplate.swift index 6f8fc873318..bda876983df 100644 --- a/Sources/tuistfixturegenerator/Templates/ManifestTemplate.swift +++ b/Sources/tuistfixturegenerator/Templates/ManifestTemplate.swift @@ -27,9 +27,9 @@ class ManifestTemplate { """ private let targetTemplate = """ - Target( + .target( name: "{TargetName}", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.{TargetName}", infoPlist: .default, diff --git a/Sources/tuistfixturegenerator/main.swift b/Sources/tuistfixturegenerator/main.swift index aec25e2eedb..7278c9169d2 100644 --- a/Sources/tuistfixturegenerator/main.swift +++ b/Sources/tuistfixturegenerator/main.swift @@ -1,7 +1,5 @@ import ArgumentParser import Foundation -import TSCBasic -import TSCUtility public struct TuistFixtureGeneratorCommand: ParsableCommand { public init() {} diff --git a/Templates/AppProject.stencil b/Templates/AppProject.stencil deleted file mode 100644 index ea3d0b6ff7d..00000000000 --- a/Templates/AppProject.stencil +++ /dev/null @@ -1,29 +0,0 @@ -import ProjectDescription -import ProjectDescriptionHelpers -import MyPlugin - -/* - +-------------+ - | | - | App | Contains {{ name }} App target and {{ name }} unit-test target - | | - +------+-------------+-------+ - | depends on | - | | - +----v-----+ +-----v-----+ - | | | | - | Kit | | UI | Two independent frameworks to share code and start modularising your app - | | | | - +----------+ +-----------+ - - */ - -// MARK: - Project - -// Local plugin loaded -let localHelper = LocalHelper(name: "MyPlugin") - -// Creates our project using a helper function defined in ProjectDescriptionHelpers -let project = Project.app(name: "{{ name }}", - destinations: .{{ platform }}, - additionalTargets: ["{{name}}Kit", "{{name}}UI"]) diff --git a/Templates/AppTests.stencil b/Templates/AppTests.stencil deleted file mode 100644 index fb7776aa513..00000000000 --- a/Templates/AppTests.stencil +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class {{ name }}Tests: XCTestCase { - func test_twoPlusTwo_isFour() { - XCTAssertEqual(2+2, 4) - } -} \ No newline at end of file diff --git a/Templates/Config.stencil b/Templates/Config.stencil deleted file mode 100644 index 66e92c6e0c4..00000000000 --- a/Templates/Config.stencil +++ /dev/null @@ -1,7 +0,0 @@ -import ProjectDescription - -let config = Config( - plugins: [ - .local(path: .relativeToManifest("../../Plugins/{{ name }}")), - ] -) diff --git a/Templates/KitSource.stencil b/Templates/KitSource.stencil deleted file mode 100644 index 89d02ef60f6..00000000000 --- a/Templates/KitSource.stencil +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public final class {{ name }}Kit { - public static func hello() { - print("Hello, from your Kit framework") - } -} diff --git a/Templates/KitTests.stencil b/Templates/KitTests.stencil deleted file mode 100644 index c7cfa778905..00000000000 --- a/Templates/KitTests.stencil +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class {{ name }}KitTests: XCTestCase { - func test_example() { - XCTAssertEqual("{{ name }}Kit", "{{ name }}Kit") - } -} \ No newline at end of file diff --git a/Templates/LaunchScreen+macOS.stencil b/Templates/LaunchScreen+macOS.stencil deleted file mode 100644 index be1034ddd63..00000000000 --- a/Templates/LaunchScreen+macOS.stencil +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/Templates/LaunchScreen+tvOS.stencil b/Templates/LaunchScreen+tvOS.stencil deleted file mode 100644 index eadb1feac0c..00000000000 --- a/Templates/LaunchScreen+tvOS.stencil +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Templates/LocalHelper.stencil b/Templates/LocalHelper.stencil deleted file mode 100644 index 6f49a442dac..00000000000 --- a/Templates/LocalHelper.stencil +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -public struct LocalHelper { - let name: String - - public init(name: String) { - self.name = name - } -} diff --git a/Templates/Package.stencil b/Templates/Package.stencil deleted file mode 100644 index c532f25aeac..00000000000 --- a/Templates/Package.stencil +++ /dev/null @@ -1,15 +0,0 @@ -// swift-tools-version: 5.8 - -import PackageDescription - -let package = Package( - name: "MyPlugin", - products: [ - .executable(name: "tuist-my-cli", targets: ["tuist-my-cli"]), - ], - targets: [ - .executableTarget( - name: "tuist-my-cli" - ), - ] -) diff --git a/Templates/Package.swift b/Templates/Package.swift deleted file mode 100644 index 9f926b36a46..00000000000 --- a/Templates/Package.swift +++ /dev/null @@ -1 +0,0 @@ -import PackageDescription diff --git a/Templates/Plugin.stencil b/Templates/Plugin.stencil deleted file mode 100644 index 86a1e55b856..00000000000 --- a/Templates/Plugin.stencil +++ /dev/null @@ -1,3 +0,0 @@ -import ProjectDescription - -let plugin = Plugin(name: "MyPlugin") \ No newline at end of file diff --git a/Templates/Project+Templates.stencil b/Templates/Project+Templates.stencil deleted file mode 100644 index 2d157bda45c..00000000000 --- a/Templates/Project+Templates.stencil +++ /dev/null @@ -1,74 +0,0 @@ -import ProjectDescription - -/// Project helpers are functions that simplify the way you define your project. -/// Share code to create targets, settings, dependencies, -/// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks" -/// See https://docs.tuist.io/guides/helpers/ - -extension Project { - /// Helper function to create the Project for this ExampleApp - public static func app(name: String, destinations: Destinations, additionalTargets: [String]) -> Project { - var targets = makeAppTargets(name: name, - destinations: destinations, - dependencies: additionalTargets.map { TargetDependency.target(name: $0) }) - targets += additionalTargets.flatMap({ makeFrameworkTargets(name: $0, destinations: destinations) }) - return Project(name: name, - organizationName: "tuist.io", - targets: targets) - } - - // MARK: - Private - - /// Helper function to create a framework target and an associated unit test target - private static func makeFrameworkTargets(name: String, destinations: Destinations) -> [Target] { - let sources = Target(name: name, - destinations: destinations, - product: .framework, - bundleId: "io.tuist.\(name)", - infoPlist: .default, - sources: ["Targets/\(name)/Sources/**"], - resources: [], - dependencies: []) - let tests = Target(name: "\(name)Tests", - destinations: destinations, - product: .unitTests, - bundleId: "io.tuist.\(name)Tests", - infoPlist: .default, - sources: ["Targets/\(name)/Tests/**"], - resources: [], - dependencies: [.target(name: name)]) - return [sources, tests] - } - - /// Helper function to create the application target and the unit test target. - private static func makeAppTargets(name: String, destinations: Destinations, dependencies: [TargetDependency]) -> [Target] { - let infoPlist: [String: Plist.Value] = [ - "CFBundleShortVersionString": "1.0", - "CFBundleVersion": "1", - "UILaunchStoryboardName": "LaunchScreen" - ] - - let mainTarget = Target( - name: name, - destinations: destinations, - product: .app, - bundleId: "io.tuist.\(name)", - infoPlist: .extendingDefault(with: infoPlist), - sources: ["Targets/\(name)/Sources/**"], - resources: ["Targets/\(name)/Resources/**"], - dependencies: dependencies - ) - - let testTarget = Target( - name: "\(name)Tests", - destinations: destinations, - product: .unitTests, - bundleId: "io.tuist.\(name)Tests", - infoPlist: .default, - sources: ["Targets/\(name)/Tests/**"], - dependencies: [ - .target(name: "\(name)") - ]) - return [mainTarget, testTarget] - } -} diff --git a/Templates/UISource.stencil b/Templates/UISource.stencil deleted file mode 100644 index 3fc2ac74a17..00000000000 --- a/Templates/UISource.stencil +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public final class {{ name }}UI { - public static func hello() { - print("Hello, from your UI framework") - } -} diff --git a/Templates/UITests.stencil b/Templates/UITests.stencil deleted file mode 100644 index 67c7bf615a8..00000000000 --- a/Templates/UITests.stencil +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class {{ name }}UITests: XCTestCase { - func test_example() { - XCTAssertEqual("{{ name }}UI", "{{ name }}UI") - } -} \ No newline at end of file diff --git a/Templates/default/AppDelegate.stencil b/Templates/default/AppDelegate.stencil deleted file mode 100644 index 99814119a8c..00000000000 --- a/Templates/default/AppDelegate.stencil +++ /dev/null @@ -1,43 +0,0 @@ -{% if platform == "macOS" %}import Cocoa -import {{ name }}Kit -import {{ name }}UI - -@main -class AppDelegate: NSObject, NSApplicationDelegate { - - @IBOutlet weak var window: NSWindow! - - func applicationDidFinishLaunching(_ aNotification: Notification) { - // Insert code here to initialize your application - } - - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application - } - -}{% else %}import UIKit -import {{ name }}Kit -import {{ name }}UI - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil - ) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - let viewController = UIViewController() - viewController.view.backgroundColor = .white - window?.rootViewController = viewController - window?.makeKeyAndVisible() - - {{ name }}Kit.hello() - {{ name }}UI.hello() - - return true - } - -}{% endif %} diff --git a/Templates/default/AppProject.stencil b/Templates/default/AppProject.stencil new file mode 100644 index 00000000000..3d4a202d722 --- /dev/null +++ b/Templates/default/AppProject.stencil @@ -0,0 +1,38 @@ +import ProjectDescription + +let project = Project( + name: "{{ name }}", + targets: [ + .target( + name: "{{ name }}", + destinations: .{{ platform }}, + product: .app, + bundleId: "io.tuist.{{ bundle_identifier }}", +{% if platform == "iOS" or platform == "tvOS" %} + infoPlist: .extendingDefault( + with: [ + "UILaunchScreen": [ + "UIColorName": "", + "UIImageName": "", + ], + ] + ), +{% elif platform == "macOS" %} + infoPlist: .default, +{% endif %} + sources: ["{{ name }}/Sources/**"], + resources: ["{{ name }}/Resources/**"], + dependencies: [] + ), + .target( + name: "{{ name }}Tests", + destinations: .{{ platform }}, + product: .unitTests, + bundleId: "io.tuist.{{ bundle_identifier }}Tests", + infoPlist: .default, + sources: ["{{ name }}/Tests/**"], + resources: [], + dependencies: [.target(name: "{{ name }}")] + ), + ] +) diff --git a/Templates/default/AppTests.stencil b/Templates/default/AppTests.stencil new file mode 100644 index 00000000000..71b0f0da106 --- /dev/null +++ b/Templates/default/AppTests.stencil @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +final class {{ class_name }}Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2+2, 4) + } +} \ No newline at end of file diff --git a/Templates/default/Config.stencil b/Templates/default/Config.stencil new file mode 100644 index 00000000000..9123464874e --- /dev/null +++ b/Templates/default/Config.stencil @@ -0,0 +1,9 @@ +import ProjectDescription + +let config = Config( +// Create an account with "tuist auth" and a project with "tuist project create" +// then uncomment the section below and set the project full-handle. +// * Read more: https://docs.tuist.io/guides/quick-start/gather-insights +// +// fullHandle: "{account_handle}/{project_handle}", +) diff --git a/Templates/swiftui/ContentView.stencil b/Templates/default/ContentView.stencil similarity index 100% rename from Templates/swiftui/ContentView.stencil rename to Templates/default/ContentView.stencil diff --git a/Templates/Gitignore.stencil b/Templates/default/Gitignore.stencil similarity index 98% rename from Templates/Gitignore.stencil rename to Templates/default/Gitignore.stencil index 551a3f50447..24b244f9c45 100644 --- a/Templates/Gitignore.stencil +++ b/Templates/default/Gitignore.stencil @@ -67,4 +67,4 @@ graph.dot Derived/ ### Tuist managed dependencies ### -Tuist/Dependencies +Tuist/.build \ No newline at end of file diff --git a/Templates/default/Package.stencil b/Templates/default/Package.stencil new file mode 100644 index 00000000000..fe99d5c070e --- /dev/null +++ b/Templates/default/Package.stencil @@ -0,0 +1,22 @@ +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + // Customize the product types for specific package product + // Default is .staticFramework + // productTypes: ["Alamofire": .framework,] + productTypes: [:] + ) +#endif + +let package = Package( + name: "{{ name }}", + dependencies: [ + // Add your own dependencies here: + // .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), + // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies + ] +) diff --git a/Templates/swiftui/Preview Content/Preview Assets.xcassets/Contents.json b/Templates/default/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Templates/swiftui/Preview Content/Preview Assets.xcassets/Contents.json rename to Templates/default/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Templates/default/app.stencil b/Templates/default/app.stencil new file mode 100644 index 00000000000..51b074cc1b7 --- /dev/null +++ b/Templates/default/app.stencil @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct {{ class_name }}App: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Templates/default/default.swift b/Templates/default/default.swift index e8da38ecc41..a3afd888d50 100644 --- a/Templates/default/default.swift +++ b/Templates/default/default.swift @@ -3,14 +3,8 @@ import ProjectDescription let nameAttribute: Template.Attribute = .required("name") let platformAttribute: Template.Attribute = .optional("platform", default: "iOS") let projectPath = "." -let appPath = "Targets/\(nameAttribute)" -let kitFrameworkPath = "Targets/\(nameAttribute)Kit" -let uiFrameworkPath = "Targets/\(nameAttribute)UI" -let taskPath = "Plugins/\(nameAttribute)" - -func templatePath(_ path: String) -> Path { - "../\(path)" -} +let appPath = "./\(nameAttribute)" +let classNameAttribute: Template.Attribute = .required("class_name") let template = Template( description: "Default template", @@ -19,65 +13,45 @@ let template = Template( platformAttribute, ], items: [ - .file( - path: "Tuist/ProjectDescriptionHelpers/Project+Templates.swift", - templatePath: templatePath("Project+Templates.stencil") - ), .file( path: projectPath + "/Project.swift", - templatePath: templatePath("AppProject.stencil") + templatePath: "AppProject.stencil" ), .file( - path: appPath + "/Sources/AppDelegate.swift", - templatePath: "AppDelegate.stencil" + path: projectPath + "/Tuist/Package.swift", + templatePath: "Package.stencil" ), .file( - path: appPath + "/Resources/LaunchScreen.storyboard", - templatePath: templatePath("LaunchScreen+\(platformAttribute).stencil") + path: projectPath + "/Tuist/Config.swift", + templatePath: "Config.stencil" ), .file( - path: appPath + "/Tests/AppTests.swift", - templatePath: templatePath("AppTests.stencil") + path: appPath + "/Sources/\(classNameAttribute)App.swift", + templatePath: "app.stencil" ), .file( - path: kitFrameworkPath + "/Sources/\(nameAttribute)Kit.swift", - templatePath: templatePath("/KitSource.stencil") + path: appPath + "/Sources/ContentView.swift", + templatePath: "ContentView.stencil" ), - .file( - path: kitFrameworkPath + "/Tests/\(nameAttribute)KitTests.swift", - templatePath: templatePath("/KitTests.stencil") + .directory( + path: appPath + "/Resources", + sourcePath: "\(platformAttribute)/Assets.xcassets" ), - .file( - path: uiFrameworkPath + "/Sources/\(nameAttribute)UI.swift", - templatePath: templatePath("/UISource.stencil") - ), - .file( - path: uiFrameworkPath + "/Tests/\(nameAttribute)UITests.swift", - templatePath: templatePath("/UITests.stencil") - ), - .file( - path: taskPath + "/Sources/tuist-my-cli/main.swift", - templatePath: templatePath("/main.stencil") - ), - .file( - path: taskPath + "/ProjectDescriptionHelpers/LocalHelper.swift", - templatePath: templatePath("/LocalHelper.stencil") - ), - .file( - path: taskPath + "/Package.swift", - templatePath: templatePath("/Package.stencil") + .directory( + path: appPath + "/Resources", + sourcePath: "Preview Content" ), .file( - path: taskPath + "/Plugin.swift", - templatePath: templatePath("/Plugin.stencil") + path: appPath + "/Tests/\(classNameAttribute)Tests.swift", + templatePath: "AppTests.stencil" ), .file( path: ".gitignore", - templatePath: templatePath("Gitignore.stencil") + templatePath: "Gitignore.stencil" ), .file( - path: "Tuist/Config.swift", - templatePath: templatePath("Config.stencil") + path: ".mise.toml", + templatePath: "mise.stencil" ), ] ) diff --git a/Templates/swiftui/ios/Assets.xcassets/AccentColor.colorset/Contents.json b/Templates/default/ios/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Templates/swiftui/ios/Assets.xcassets/AccentColor.colorset/Contents.json rename to Templates/default/ios/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Templates/swiftui/ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/Templates/default/ios/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Templates/swiftui/ios/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Templates/default/ios/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Templates/swiftui/ios/Assets.xcassets/Contents.json b/Templates/default/ios/Assets.xcassets/Contents.json similarity index 100% rename from Templates/swiftui/ios/Assets.xcassets/Contents.json rename to Templates/default/ios/Assets.xcassets/Contents.json diff --git a/Templates/swiftui/macos/Assets.xcassets/AccentColor.colorset/Contents.json b/Templates/default/macos/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Templates/swiftui/macos/Assets.xcassets/AccentColor.colorset/Contents.json rename to Templates/default/macos/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Templates/swiftui/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/Templates/default/macos/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Templates/swiftui/macos/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Templates/default/macos/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Templates/swiftui/macos/Assets.xcassets/Contents.json b/Templates/default/macos/Assets.xcassets/Contents.json similarity index 100% rename from Templates/swiftui/macos/Assets.xcassets/Contents.json rename to Templates/default/macos/Assets.xcassets/Contents.json diff --git a/Templates/default/mise.stencil b/Templates/default/mise.stencil new file mode 100644 index 00000000000..27d11d87d86 --- /dev/null +++ b/Templates/default/mise.stencil @@ -0,0 +1,2 @@ +[tools] +tuist = "{{ tuist_version }}" diff --git a/Templates/swiftui/tvos/Assets.xcassets/AccentColor.colorset/Contents.json b/Templates/default/tvos/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/AccentColor.colorset/Contents.json rename to Templates/default/tvos/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json rename to Templates/default/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json diff --git a/Templates/swiftui/tvos/Assets.xcassets/Contents.json b/Templates/default/tvos/Assets.xcassets/Contents.json similarity index 100% rename from Templates/swiftui/tvos/Assets.xcassets/Contents.json rename to Templates/default/tvos/Assets.xcassets/Contents.json diff --git a/Templates/default/watchos/Assets.xcassets/AccentColor.colorset/Contents.json b/Templates/default/watchos/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/Templates/default/watchos/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Templates/default/watchos/Assets.xcassets/AppIcon.appiconset/Contents.json b/Templates/default/watchos/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..49c81cd8c4c --- /dev/null +++ b/Templates/default/watchos/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/Templates/default/watchos/Assets.xcassets/Contents.json b/Templates/default/watchos/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Templates/default/watchos/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Templates/main.stencil b/Templates/main.stencil deleted file mode 100644 index 6f059694c4b..00000000000 --- a/Templates/main.stencil +++ /dev/null @@ -1 +0,0 @@ -print("Hello, from your Tuist Task") \ No newline at end of file diff --git a/Templates/swiftui/app.stencil b/Templates/swiftui/app.stencil deleted file mode 100644 index be85e855a64..00000000000 --- a/Templates/swiftui/app.stencil +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI -import {{ name }}UI - -@main -struct {{ name }}App: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/Templates/swiftui/swiftui.swift b/Templates/swiftui/swiftui.swift deleted file mode 100644 index a1f937a4dc2..00000000000 --- a/Templates/swiftui/swiftui.swift +++ /dev/null @@ -1,87 +0,0 @@ -import ProjectDescription - -let nameAttribute: Template.Attribute = .required("name") -let platformAttribute: Template.Attribute = .optional("platform", default: "iOS") -let projectPath = "." -let appPath = "Targets/\(nameAttribute)" -let kitFrameworkPath = "Targets/\(nameAttribute)Kit" -let uiFrameworkPath = "Targets/\(nameAttribute)UI" -let taskPath = "Plugins/\(nameAttribute)" - -func templatePath(_ path: String) -> Path { - "../\(path)" -} - -let template = Template( - description: "SwiftUI template", - attributes: [ - nameAttribute, - platformAttribute, - ], - items: [ - .file( - path: "Tuist/ProjectDescriptionHelpers/Project+Templates.swift", - templatePath: templatePath("Project+Templates.stencil") - ), - .file( - path: projectPath + "/Project.swift", - templatePath: templatePath("AppProject.stencil") - ), - .file( - path: appPath + "/Sources/\(nameAttribute)App.swift", - templatePath: "app.stencil" - ), - .file( - path: uiFrameworkPath + "/Sources/ContentView.swift", - templatePath: "ContentView.stencil" - ), - .directory( - path: appPath + "/Resources", - sourcePath: "\(platformAttribute)/Assets.xcassets" - ), - .directory( - path: appPath + "/Resources", - sourcePath: "Preview Content" - ), - .file( - path: appPath + "/Tests/\(nameAttribute)Tests.swift", - templatePath: templatePath("AppTests.stencil") - ), - .file( - path: kitFrameworkPath + "/Sources/\(nameAttribute)Kit.swift", - templatePath: templatePath("KitSource.stencil") - ), - .file( - path: kitFrameworkPath + "/Tests/\(nameAttribute)KitTests.swift", - templatePath: templatePath("/KitTests.stencil") - ), - .file( - path: uiFrameworkPath + "/Tests/\(nameAttribute)UITests.swift", - templatePath: templatePath("/UITests.stencil") - ), - .file( - path: taskPath + "/Sources/tuist-my-cli/main.swift", - templatePath: templatePath("/main.stencil") - ), - .file( - path: taskPath + "/ProjectDescriptionHelpers/LocalHelper.swift", - templatePath: templatePath("/LocalHelper.stencil") - ), - .file( - path: taskPath + "/Package.swift", - templatePath: templatePath("/Package.stencil") - ), - .file( - path: taskPath + "/Plugin.swift", - templatePath: templatePath("/Plugin.stencil") - ), - .file( - path: ".gitignore", - templatePath: templatePath("Gitignore.stencil") - ), - .file( - path: "Tuist/Config.swift", - templatePath: templatePath("Config.stencil") - ), - ] -) diff --git a/Tests/Fixtures/App.app/App b/Tests/Fixtures/App.app/App new file mode 100755 index 00000000000..208e0b87498 Binary files /dev/null and b/Tests/Fixtures/App.app/App differ diff --git a/Tests/Fixtures/App.app/Info.plist b/Tests/Fixtures/App.app/Info.plist new file mode 100644 index 00000000000..a78db5c4f65 Binary files /dev/null and b/Tests/Fixtures/App.app/Info.plist differ diff --git a/Tests/Fixtures/App.app/PkgInfo b/Tests/Fixtures/App.app/PkgInfo new file mode 100644 index 00000000000..bd04210fb49 --- /dev/null +++ b/Tests/Fixtures/App.app/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/Info.plist b/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/Info.plist new file mode 100644 index 00000000000..6e3075fc8cf Binary files /dev/null and b/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/Info.plist differ diff --git a/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/WidgetExtension b/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/WidgetExtension new file mode 100755 index 00000000000..c6767ba72ec Binary files /dev/null and b/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/WidgetExtension differ diff --git a/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/_CodeSignature/CodeResources b/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/_CodeSignature/CodeResources new file mode 100644 index 00000000000..292230e4c9e --- /dev/null +++ b/Tests/Fixtures/App.app/PlugIns/WidgetExtension.appex/_CodeSignature/CodeResources @@ -0,0 +1,101 @@ + + + + + files + + Info.plist + + M+EhKux414cAifJGPQ0gC+5lLNo= + + + files2 + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Tests/Fixtures/App.app/Watch/WatchApp.app/Info.plist b/Tests/Fixtures/App.app/Watch/WatchApp.app/Info.plist new file mode 100644 index 00000000000..245f11a9827 Binary files /dev/null and b/Tests/Fixtures/App.app/Watch/WatchApp.app/Info.plist differ diff --git a/Tests/Fixtures/App.app/Watch/WatchApp.app/PkgInfo b/Tests/Fixtures/App.app/Watch/WatchApp.app/PkgInfo new file mode 100644 index 00000000000..bd04210fb49 --- /dev/null +++ b/Tests/Fixtures/App.app/Watch/WatchApp.app/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/Tests/Fixtures/App.app/Watch/WatchApp.app/WatchApp b/Tests/Fixtures/App.app/Watch/WatchApp.app/WatchApp new file mode 100755 index 00000000000..8be8ccb5601 Binary files /dev/null and b/Tests/Fixtures/App.app/Watch/WatchApp.app/WatchApp differ diff --git a/Tests/Fixtures/App.app/Watch/WatchApp.app/_CodeSignature/CodeResources b/Tests/Fixtures/App.app/Watch/WatchApp.app/_CodeSignature/CodeResources new file mode 100644 index 00000000000..f755042dcd5 --- /dev/null +++ b/Tests/Fixtures/App.app/Watch/WatchApp.app/_CodeSignature/CodeResources @@ -0,0 +1,105 @@ + + + + + files + + Info.plist + + hah3Tps6zoCP36G2gsJywyduq/Q= + + PkgInfo + + n57qDP4tZfLD1rCS43W0B4LQjzE= + + + files2 + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Tests/Fixtures/App.app/_CodeSignature/CodeResources b/Tests/Fixtures/App.app/_CodeSignature/CodeResources new file mode 100644 index 00000000000..ba22254a283 --- /dev/null +++ b/Tests/Fixtures/App.app/_CodeSignature/CodeResources @@ -0,0 +1,183 @@ + + + + + files + + Info.plist + + LMSjjtLNFmRMcWKpAKv7/uOIy/Y= + + PkgInfo + + n57qDP4tZfLD1rCS43W0B4LQjzE= + + PlugIns/WidgetExtension.appex/Info.plist + + M+EhKux414cAifJGPQ0gC+5lLNo= + + PlugIns/WidgetExtension.appex/WidgetExtension + + wuFuBXfDFlwvjBKQEHi2lMpdRG8= + + PlugIns/WidgetExtension.appex/_CodeSignature/CodeResources + + zRUzx29Q43didBRZnWGFS6FeRpc= + + Watch/WatchApp.app/Info.plist + + hah3Tps6zoCP36G2gsJywyduq/Q= + + Watch/WatchApp.app/PkgInfo + + n57qDP4tZfLD1rCS43W0B4LQjzE= + + Watch/WatchApp.app/WatchApp + + bsE9lAiZxARSsQa7z6aPK99fbJs= + + Watch/WatchApp.app/_CodeSignature/CodeResources + + QkdL/h9BtAjocxTD84a2KzXUBtc= + + + files2 + + PlugIns/WidgetExtension.appex/Info.plist + + hash2 + + iOTYEXxkcxGAHP3KPCiaZxXAbbD16OTPT/usdx9UuDs= + + + PlugIns/WidgetExtension.appex/WidgetExtension + + hash2 + + 9fBzdW56wDiAA/ntQGFCqqcmQnsyeVdO28tpR+0866E= + + + PlugIns/WidgetExtension.appex/_CodeSignature/CodeResources + + hash2 + + v9uqoWoeuW7NTYR+ZNybG7bW2RFmKqilMbLsAW7TWzc= + + + Watch/WatchApp.app/Info.plist + + hash2 + + AD4rqnTgghykHbbbmWqJcLbG2O9MV4vIJ4CsHXDn/h0= + + + Watch/WatchApp.app/PkgInfo + + hash2 + + glAhkclISwTWhTdPmHmgBmBpxJuKyuegSwHTjQfo7KA= + + + Watch/WatchApp.app/WatchApp + + hash2 + + b4p1RJNjPG1G0bB7oEawJf1XTer4q9KmTtPhN6ob3hs= + + + Watch/WatchApp.app/_CodeSignature/CodeResources + + hash2 + + 7Z9prAGlKkCVbLn0J2xhX5ih4hE0TJ4wggihKmEx5m0= + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Tests/Fixtures/DylibXCFramework.xcframework/Info.plist b/Tests/Fixtures/DylibXCFramework.xcframework/Info.plist new file mode 100644 index 00000000000..85324f93ff8 --- /dev/null +++ b/Tests/Fixtures/DylibXCFramework.xcframework/Info.plist @@ -0,0 +1,27 @@ + + + + + AvailableLibraries + + + BinaryPath + libDylibXCFramework.dylib + LibraryIdentifier + macos-arm64 + LibraryPath + libDylibXCFramework.dylib + SupportedArchitectures + + arm64 + + SupportedPlatform + macos + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/Tests/Fixtures/DylibXCFramework.xcframework/macos-arm64/libDylibXCFramework.dylib b/Tests/Fixtures/DylibXCFramework.xcframework/macos-arm64/libDylibXCFramework.dylib new file mode 100755 index 00000000000..2858fd94161 Binary files /dev/null and b/Tests/Fixtures/DylibXCFramework.xcframework/macos-arm64/libDylibXCFramework.dylib differ diff --git a/Tests/Fixtures/Info.plist b/Tests/Fixtures/Info.plist deleted file mode 100644 index 31a1e61f77c..00000000000 Binary files a/Tests/Fixtures/Info.plist and /dev/null differ diff --git a/Tests/Fixtures/Carthage/2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap b/Tests/Fixtures/PrebuiltFramework/2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap similarity index 100% rename from Tests/Fixtures/Carthage/2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap rename to Tests/Fixtures/PrebuiltFramework/2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap diff --git a/Tests/Fixtures/Carthage/773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap b/Tests/Fixtures/PrebuiltFramework/773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap similarity index 100% rename from Tests/Fixtures/Carthage/773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap rename to Tests/Fixtures/PrebuiltFramework/773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework.dSYM/Contents/Info.plist b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework.dSYM/Contents/Info.plist similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework.dSYM/Contents/Info.plist rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework.dSYM/Contents/Info.plist diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework.dSYM/Contents/Resources/DWARF/RxBlocking b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework.dSYM/Contents/Resources/DWARF/RxBlocking similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework.dSYM/Contents/Resources/DWARF/RxBlocking rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework.dSYM/Contents/Resources/DWARF/RxBlocking diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Headers/RxBlocking-Swift.h b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Headers/RxBlocking-Swift.h similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Headers/RxBlocking-Swift.h rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Headers/RxBlocking-Swift.h diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Info.plist b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Info.plist similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Info.plist rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Info.plist diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64-apple-ios.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/arm64.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7-apple-ios.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/armv7.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386-apple-ios-simulator.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/i386.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64-apple-ios-simulator.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftdoc b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftdoc similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftdoc rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftdoc diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftmodule b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftmodule similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftmodule rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/RxBlocking.swiftmodule/x86_64.swiftmodule diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/Modules/module.modulemap b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/module.modulemap similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/Modules/module.modulemap rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/Modules/module.modulemap diff --git a/Tests/Fixtures/Carthage/RxBlocking.framework/RxBlocking b/Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/RxBlocking similarity index 100% rename from Tests/Fixtures/Carthage/RxBlocking.framework/RxBlocking rename to Tests/Fixtures/PrebuiltFramework/RxBlocking.framework/RxBlocking diff --git a/Tests/Fixtures/WorkspaceWithPlugins/LocalPlugin/ProjectDescriptionHelpers/Project+Template.swift b/Tests/Fixtures/WorkspaceWithPlugins/LocalPlugin/ProjectDescriptionHelpers/Project+Template.swift index e7c0b3d87e6..83cc0a70290 100644 --- a/Tests/Fixtures/WorkspaceWithPlugins/LocalPlugin/ProjectDescriptionHelpers/Project+Template.swift +++ b/Tests/Fixtures/WorkspaceWithPlugins/LocalPlugin/ProjectDescriptionHelpers/Project+Template.swift @@ -2,9 +2,9 @@ import ProjectDescription extension Target { public static func app(name: String, dependencies: [TargetDependency] = []) -> Target { - Target( + .target( name: name, - platform: .iOS, + destinations: [.iPhone, .iPad], product: .app, bundleId: .bundleId(for: name), infoPlist: .default, @@ -15,9 +15,9 @@ extension Target { } public static func framework(name: String, dependencies: [TargetDependency] = []) -> Target { - Target( + .target( name: name, - platform: .iOS, + destinations: [.iPhone, .iPad], product: .framework, bundleId: .bundleId(for: name), infoPlist: .default, diff --git a/Tests/ProjectDescriptionTests/CoreDataModelTests.swift b/Tests/ProjectDescriptionTests/CoreDataModelTests.swift index d07031a88b3..b7b56b85fa8 100644 --- a/Tests/ProjectDescriptionTests/CoreDataModelTests.swift +++ b/Tests/ProjectDescriptionTests/CoreDataModelTests.swift @@ -6,7 +6,7 @@ import XCTest final class CoreDataModelTests: XCTestCase { func test_toJSON() { - let subject = CoreDataModel("path", currentVersion: "current") + let subject: CoreDataModel = .coreDataModel("path", currentVersion: "current") XCTAssertCodable(subject) } } diff --git a/Tests/ProjectDescriptionTests/Dependencies/CarthageDependenciesTests.swift b/Tests/ProjectDescriptionTests/Dependencies/CarthageDependenciesTests.swift deleted file mode 100644 index 582ee170234..00000000000 --- a/Tests/ProjectDescriptionTests/Dependencies/CarthageDependenciesTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -import XCTest - -@testable import ProjectDescription - -final class CarthageDependenciesTests: XCTestCase { - func test_carthageDependencies_codable() throws { - let subject: CarthageDependencies = [ - .github(path: "Dependency/Dependency", requirement: .revision("xyz")), - .git(path: "Git/Git", requirement: .atLeast("1.2.3")), - ] - XCTAssertCodable(subject) - } - - // MARK: - Carthage Origin tests - - func test_carthageDependency_github_codable() throws { - let subject: CarthageDependencies.Dependency = .github(path: "Path/Path", requirement: .branch("branch_name")) - XCTAssertCodable(subject) - } - - func test_carthageDependency_git_codable() throws { - let subject: CarthageDependencies.Dependency = .git(path: "Git/Git", requirement: .exact("1.5.123")) - XCTAssertCodable(subject) - } - - func test_carthageDependency_binary_codable() throws { - let subject: CarthageDependencies.Dependency = .binary( - path: "file:///some/Path/MyFramework.json", - requirement: .upToNext("5.6.9") - ) - XCTAssertCodable(subject) - } - - // MARK: - Carthage Requirement tests - - func test_carthageRequirement_exact_codable() throws { - let subject: CarthageDependencies.Requirement = .exact("1.0.0") - XCTAssertCodable(subject) - } - - func test_carthageRequirement_upToNext_codable() throws { - let subject: CarthageDependencies.Requirement = .upToNext("3.2.0") - XCTAssertCodable(subject) - } - - func test_carthageRequirement_atLeast_codable() throws { - let subject: CarthageDependencies.Requirement = .atLeast("3.2.0") - XCTAssertCodable(subject) - } - - func test_carthageRequirement_branch_codable() throws { - let subject: CarthageDependencies.Requirement = .branch("branch") - XCTAssertCodable(subject) - } - - func test_carthageRequirement_revision_codable() throws { - let subject: CarthageDependencies.Requirement = .revision("revision") - XCTAssertCodable(subject) - } -} diff --git a/Tests/ProjectDescriptionTests/Dependencies/DependenciesTests.swift b/Tests/ProjectDescriptionTests/Dependencies/DependenciesTests.swift deleted file mode 100644 index eef16175539..00000000000 --- a/Tests/ProjectDescriptionTests/Dependencies/DependenciesTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import XCTest - -@testable import ProjectDescription - -final class DependenciesTests: XCTestCase { - func test_dependencies_codable() throws { - let subject = Dependencies( - carthage: [ - .github(path: "Dependency1/Dependency1", requirement: .branch("BranchName")), - .git(path: "Dependency2/Dependency2", requirement: .upToNext("1.2.3")), - ], - swiftPackageManager: .init(), - platforms: [.iOS, .macOS, .tvOS, .watchOS] - ) - XCTAssertCodable(subject) - } -} diff --git a/Tests/ProjectDescriptionTests/Dependencies/SwiftPackageManagerDependenciesTests.swift b/Tests/ProjectDescriptionTests/Dependencies/SwiftPackageManagerDependenciesTests.swift deleted file mode 100644 index 92811b40f60..00000000000 --- a/Tests/ProjectDescriptionTests/Dependencies/SwiftPackageManagerDependenciesTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import XCTest - -@testable import ProjectDescription - -final class SwiftPackageManagerDependenciesTests: XCTestCase { - func test_swiftPackageManagerDependencies_codable() { - let subject: SwiftPackageManagerDependencies = .init() - XCTAssertCodable(subject) - } -} diff --git a/Tests/ProjectDescriptionTests/PluginLocationTests.swift b/Tests/ProjectDescriptionTests/PluginLocationTests.swift index 23b6a429d4e..3f3dd4b9a9b 100644 --- a/Tests/ProjectDescriptionTests/PluginLocationTests.swift +++ b/Tests/ProjectDescriptionTests/PluginLocationTests.swift @@ -3,7 +3,7 @@ import XCTest final class PluginLocationTests: XCTestCase { func test_codable_local() throws { - let subject = PluginLocation.local(path: .init("/some/path")) + let subject = PluginLocation.local(path: .path("/some/path")) XCTAssertCodable(subject) } diff --git a/Tests/ProjectDescriptionTests/SchemeTests.swift b/Tests/ProjectDescriptionTests/SchemeTests.swift index fd4b6af129e..d90a9b4693a 100644 --- a/Tests/ProjectDescriptionTests/SchemeTests.swift +++ b/Tests/ProjectDescriptionTests/SchemeTests.swift @@ -10,16 +10,16 @@ final class SchemeTests: XCTestCase { func test_codable() throws { // Given - let subject = Scheme( + let subject: Scheme = .scheme( name: "scheme", shared: true, - buildAction: BuildAction( + buildAction: .buildAction( targets: [.init(projectPath: nil, target: "target")], preActions: mockExecutionAction("build_action"), postActions: mockExecutionAction("build_action") ), testAction: TestAction.targets( - [TestableTarget(target: .init(projectPath: nil, target: "target"))], + [.testableTarget(target: .init(projectPath: nil, target: "target"))], arguments: Arguments( environmentVariables: ["test": "b"], launchArguments: [LaunchArgument(name: "test", isEnabled: true)] @@ -38,6 +38,13 @@ final class SchemeTests: XCTestCase { arguments: Arguments( environmentVariables: ["run": "b"], launchArguments: [LaunchArgument(name: "run", isEnabled: true)] + ), + options: RunActionOptions( + language: "en", + region: "US", + storeKitConfigurationPath: nil, + simulatedLocation: nil, + enableGPUFrameCaptureMode: .autoEnabled ) ) ) @@ -52,16 +59,16 @@ final class SchemeTests: XCTestCase { func test_defaultConfigurationNames() throws { // Given / When - let subject = Scheme( + let subject: Scheme = .scheme( name: "scheme", shared: true, - buildAction: BuildAction( - targets: [.init(projectPath: nil, target: "target")], + buildAction: .buildAction( + targets: [.target("target")], preActions: mockExecutionAction("build_action"), postActions: mockExecutionAction("build_action") ), testAction: TestAction.targets( - [.init(target: .init(projectPath: nil, target: "target"))], + [.testableTarget(target: .init(projectPath: nil, target: "target"))], arguments: Arguments( environmentVariables: ["test": "b"], launchArguments: [LaunchArgument(name: "test", isEnabled: true)] @@ -80,6 +87,13 @@ final class SchemeTests: XCTestCase { arguments: Arguments( environmentVariables: ["run": "b"], launchArguments: [LaunchArgument(name: "run", isEnabled: true)] + ), + options: RunActionOptions( + language: "en", + region: "US", + storeKitConfigurationPath: nil, + simulatedLocation: nil, + enableGPUFrameCaptureMode: .autoEnabled ) ) ) @@ -87,13 +101,15 @@ final class SchemeTests: XCTestCase { // Then XCTAssertEqual(subject.runAction?.configuration.rawValue, "Release") XCTAssertEqual(subject.testAction?.configuration.rawValue, "Debug") + XCTAssertEqual(subject.runAction?.options.language, "en") + XCTAssertEqual(subject.runAction?.options.region, "US") } // MARK: - Helpers private func mockExecutionAction(_ actionName: String) -> [ExecutionAction] { [ - ExecutionAction( + .executionAction( title: "Run Script", scriptText: "echo \(actionName)", target: TargetReference( diff --git a/Tests/ProjectDescriptionTests/SettingsTests.swift b/Tests/ProjectDescriptionTests/SettingsTests.swift index 6fcde917381..1c495865855 100644 --- a/Tests/ProjectDescriptionTests/SettingsTests.swift +++ b/Tests/ProjectDescriptionTests/SettingsTests.swift @@ -115,10 +115,10 @@ final class SettingsTests: XCTestCase { "VERSION_INFO_PREFIX": "A_Prefix", "VERSION_INFO_SUFFIX": "A_Suffix", "SWIFT_VERSION": "5.2.1", - "OTHER_SWIFT_FLAGS": "first second third", + "OTHER_SWIFT_FLAGS": ["first", "second", "third"], "ENABLE_BITCODE": "YES", "DEBUG_INFORMATION_FORMAT": "dwarf", - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "FIRST SECOND THIRD", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": ["FIRST", "SECOND", "THIRD"], "SWIFT_OBJC_BRIDGING_HEADER": "/my/bridging/header/path.h", "OTHER_CFLAGS": ["$(inherited)", "-my-c-flag"], "OTHER_LDFLAGS": ["$(inherited)", "-my-linker-flag"], @@ -138,15 +138,44 @@ final class SettingsTests: XCTestCase { ]) } + func test_settingsDictionary_otherSwiftFlags() { + /// Given/When + let settingsVariadic = SettingsDictionary() + .otherSwiftFlags("FIRST", "SECOND", "THIRD") + + let settingsArray = SettingsDictionary() + .otherSwiftFlags(["FIRST", "SECOND", "THIRD"]) + + /// Then + XCTAssertEqual(settingsVariadic, [ + "OTHER_SWIFT_FLAGS": ["FIRST", "SECOND", "THIRD"], + ]) + + XCTAssertEqual(settingsArray, [ + "OTHER_SWIFT_FLAGS": ["FIRST", "SECOND", "THIRD"], + ]) + + XCTAssertEqual(settingsVariadic, settingsArray) + } + func test_settingsDictionary_swiftActiveCompilationConditions() { /// Given/When - let settings = SettingsDictionary() + let settingsVariadic = SettingsDictionary() .swiftActiveCompilationConditions("FIRST", "SECOND", "THIRD") + let settingsArray = SettingsDictionary() + .swiftActiveCompilationConditions(["FIRST", "SECOND", "THIRD"]) + /// Then - XCTAssertEqual(settings, [ - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "FIRST SECOND THIRD", + XCTAssertEqual(settingsVariadic, [ + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": ["FIRST", "SECOND", "THIRD"], + ]) + + XCTAssertEqual(settingsArray, [ + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": ["FIRST", "SECOND", "THIRD"], ]) + + XCTAssertEqual(settingsVariadic, settingsArray) } func test_settingsDictionary_SwiftCompilationMode() { diff --git a/Tests/ProjectDescriptionTests/TargetDependencyTests.swift b/Tests/ProjectDescriptionTests/TargetDependencyTests.swift index 49ba9326077..6956962db5e 100644 --- a/Tests/ProjectDescriptionTests/TargetDependencyTests.swift +++ b/Tests/ProjectDescriptionTests/TargetDependencyTests.swift @@ -54,7 +54,7 @@ final class TargetDependencyTests: XCTestCase { } func test_instanceTarget() { - let target = Target(name: "Target", platform: .iOS, product: .framework, bundleId: "bundleId") + let target: Target = .target(name: "Target", destinations: .iOS, product: .framework, bundleId: "bundleId") let subject = TargetDependency.target(target) XCTAssertEqual(subject, TargetDependency.target(name: "Target")) } diff --git a/Tests/ProjectDescriptionTests/TargetTests.swift b/Tests/ProjectDescriptionTests/TargetTests.swift index ba141b55786..736fd735088 100644 --- a/Tests/ProjectDescriptionTests/TargetTests.swift +++ b/Tests/ProjectDescriptionTests/TargetTests.swift @@ -6,13 +6,13 @@ import XCTest final class TargetTests: XCTestCase { func test_toJSON() { - let subject = Target( + let subject: Target = .target( name: "name", - platform: .iOS, + destinations: [.iPhone, .iPad], product: .app, productName: "product_name", bundleId: "bundle_id", - deploymentTarget: .iOS(targetVersion: "13.1", devices: [.iphone, .ipad]), + deploymentTargets: .iOS("13.1"), infoPlist: "info.plist", sources: "sources/*", resources: "resources/*", @@ -36,16 +36,16 @@ final class TargetTests: XCTestCase { debug: ["a": .string("b")], release: ["a": .string("b")] ), - coreDataModels: [CoreDataModel("pat", currentVersion: "version")], + coreDataModels: [.coreDataModel("pat", currentVersion: "version")], environmentVariables: ["a": "b"] ) XCTAssertCodable(subject) } func test_toJSON_withFileList() { - let subject = Target( + let subject: Target = .target( name: "name", - platform: .iOS, + destinations: [.iPhone, .iPad, .macWithiPadDesign], product: .app, productName: "product_name", bundleId: "bundle_id", @@ -85,7 +85,7 @@ final class TargetTests: XCTestCase { .debug(name: .release, settings: ["a": .string("release")], xcconfig: "debug.xcconfig"), ] ), - coreDataModels: [CoreDataModel("pat", currentVersion: "version")], + coreDataModels: [.coreDataModel("pat", currentVersion: "version")], environmentVariables: ["a": "b"] ) XCTAssertCodable(subject) diff --git a/Tests/TuistAcceptanceTests/EditAcceptanceTests.swift b/Tests/TuistAcceptanceTests/EditAcceptanceTests.swift deleted file mode 100644 index 72feb61a1d4..00000000000 --- a/Tests/TuistAcceptanceTests/EditAcceptanceTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -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/InitAcceptanceTests.swift b/Tests/TuistAcceptanceTests/InitAcceptanceTests.swift deleted file mode 100644 index 8379f30d69b..00000000000 --- a/Tests/TuistAcceptanceTests/InitAcceptanceTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -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/RunAcceptanceTests.swift b/Tests/TuistAcceptanceTests/RunAcceptanceTests.swift deleted file mode 100644 index 9348443ba3a..00000000000 --- a/Tests/TuistAcceptanceTests/RunAcceptanceTests.swift +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index d39e7eaa154..00000000000 --- a/Tests/TuistAcceptanceTests/ScaffoldAcceptanceTests.swift +++ /dev/null @@ -1,144 +0,0 @@ -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/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift b/Tests/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift index 9c8f3aa5419..1d620be4085 100644 --- a/Tests/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift +++ b/Tests/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest @@ -21,7 +21,7 @@ final class AsyncQueuePersistorTests: TuistUnitTestCase { super.tearDown() } - func test_write() throws { + func test_write() async throws { // Given let event = AnyAsyncQueueEvent(dispatcherId: "dispatcher") @@ -29,7 +29,7 @@ final class AsyncQueuePersistorTests: TuistUnitTestCase { try subject.write(event: event) // Then - let got = try subject.readAll() + let got = try await subject.readAll() let gotEvent = try XCTUnwrap(got.first) XCTAssertEqual(gotEvent.dispatcherId, "dispatcher") XCTAssertEqual(gotEvent.id, event.id) @@ -37,7 +37,7 @@ final class AsyncQueuePersistorTests: TuistUnitTestCase { XCTAssertEqual(gotEvent.date, normalizedDate) } - func test_write_whenDirectoryDoesntExist_itCreatesDirectory() throws { + func test_write_whenDirectoryDoesntExist_itCreatesDirectory() async throws { let temporaryDirectory = try! temporaryPath() subject = AsyncQueuePersistor(directory: temporaryDirectory.appending(try RelativePath(validating: "test/"))) @@ -48,7 +48,7 @@ final class AsyncQueuePersistorTests: TuistUnitTestCase { try subject.write(event: event) // Then - let got = try subject.readAll() + let got = try await subject.readAll() let gotEvent = try XCTUnwrap(got.first) XCTAssertEqual(gotEvent.dispatcherId, "dispatcher") XCTAssertEqual(gotEvent.id, event.id) @@ -56,18 +56,18 @@ final class AsyncQueuePersistorTests: TuistUnitTestCase { XCTAssertEqual(gotEvent.date, normalizedDate) } - func test_delete() throws { + func test_delete() async throws { // Given let event = AnyAsyncQueueEvent(dispatcherId: "dispatcher") try subject.write(event: event) - var persistedEvents = try subject.readAll() + var persistedEvents = try await subject.readAll() XCTAssertEqual(persistedEvents.count, 1) // When - try subject.delete(event: event) + try await subject.delete(event: event) // Then - persistedEvents = try subject.readAll() + persistedEvents = try await subject.readAll() XCTAssertEqual(persistedEvents.count, 0) } } diff --git a/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift b/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift index 52f5bbb2fe5..9ec89372f89 100644 --- a/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift +++ b/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift @@ -1,4 +1,5 @@ import Foundation +import MockableTest import Queuer import TuistCore import TuistSupport @@ -9,20 +10,20 @@ import XCTest @testable import TuistSupportTesting final class AsyncQueueTests: TuistUnitTestCase { - var subject: AsyncQueue! + private var subject: AsyncQueue! - let dispatcher1ID = "Dispatcher1" - let dispatcher2ID = "Dispatcher2" + private let dispatcher1ID = "Dispatcher1" + private let dispatcher2ID = "Dispatcher2" - var mockAsyncQueueDispatcher1: MockAsyncQueueDispatcher! - var mockAsyncQueueDispatcher2: MockAsyncQueueDispatcher! + private var mockAsyncQueueDispatcher1: MockAsyncQueueDispatcher! + private var mockAsyncQueueDispatcher2: MockAsyncQueueDispatcher! - var mockCIChecker: MockCIChecker! + private var ciChecker: MockCIChecking! - var mockPersistor: MockAsyncQueuePersistor! - var mockQueuer: MockQueuer! + private var mockPersistor: MockAsyncQueuePersistor! + private var mockQueuer: MockQueuer! - let timeout = 3.0 + private let timeout = 3.0 override func setUp() { super.setUp() @@ -32,7 +33,7 @@ final class AsyncQueueTests: TuistUnitTestCase { mockAsyncQueueDispatcher2 = MockAsyncQueueDispatcher() mockAsyncQueueDispatcher2.stubbedIdentifier = dispatcher2ID - mockCIChecker = MockCIChecker() + ciChecker = .init() mockPersistor = MockAsyncQueuePersistor() mockQueuer = MockQueuer() } @@ -40,7 +41,7 @@ final class AsyncQueueTests: TuistUnitTestCase { override func tearDown() { mockAsyncQueueDispatcher1 = nil mockAsyncQueueDispatcher2 = nil - mockCIChecker = nil + ciChecker = nil mockPersistor = nil mockQueuer = nil subject = nil @@ -50,12 +51,11 @@ final class AsyncQueueTests: TuistUnitTestCase { func makeSubject( queue: Queuing? = nil, ciChecker: CIChecking? = nil, - persistor: AsyncQueuePersisting? = nil, - dispatchers _: [AsyncQueueDispatching]? = nil + persistor: AsyncQueuePersisting? = nil ) -> AsyncQueue { let asyncQueue = AsyncQueue( queue: queue ?? mockQueuer, - ciChecker: ciChecker ?? mockCIChecker, + ciChecker: ciChecker ?? self.ciChecker, persistor: persistor ?? mockPersistor ) asyncQueue.register(dispatcher: mockAsyncQueueDispatcher1) @@ -197,15 +197,17 @@ final class AsyncQueueTests: TuistUnitTestCase { XCTAssertEqual(mockPersistor.invokedDeleteEventCount, 0) } - func test_waits_for_queue_to_finish_when_CI() throws { + func test_waits_for_queue_to_finish_when_CI() async throws { // Given let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: 1) mockPersistor.stubbedReadAllResult = [eventTuple1] - mockCIChecker.isCIStub = true + given(ciChecker) + .isCI() + .willReturn(true) // When subject = makeSubject(queue: Queuer.shared) - subject.start() + await subject.start() // Then XCTAssertEqual(Queuer.shared.operationCount, 0) @@ -226,8 +228,11 @@ final class AsyncQueueTests: TuistUnitTestCase { // XCTAssertEqual(Queuer.shared.operationCount, 1) // } - func test_start_readsPersistedEventsInitialization() throws { + func test_start_readsPersistedEventsInitialization() async throws { // Given + given(ciChecker) + .isCI() + .willReturn(false) let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: 1) let eventTuple2: AsyncQueueEventTuple = makeEventTuple(id: 2) let eventTuple3: AsyncQueueEventTuple = makeEventTuple(id: 3) @@ -235,7 +240,7 @@ final class AsyncQueueTests: TuistUnitTestCase { // When subject = makeSubject() - subject.start() + await subject.start() // Then let numberOfOperationsQueued = mockQueuer.invokedAddOperationCount @@ -260,8 +265,11 @@ final class AsyncQueueTests: TuistUnitTestCase { XCTAssertEqual(queuedOperation3.name, eventTuple3.id.uuidString) } - func test_start_persistedEventIsDispatchedByTheRightDispatcher() throws { + func test_start_persistedEventIsDispatchedByTheRightDispatcher() async throws { // Given + given(ciChecker) + .isCI() + .willReturn(false) let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: 1) mockPersistor.stubbedReadAllResult = [eventTuple1] @@ -272,7 +280,7 @@ final class AsyncQueueTests: TuistUnitTestCase { // When subject = makeSubject(queue: Queuer.shared) - subject.start() + await subject.start() // Then wait(for: [expectation], timeout: timeout) @@ -285,8 +293,11 @@ final class AsyncQueueTests: TuistUnitTestCase { XCTAssertEqual(mockAsyncQueueDispatcher2.invokedDispatchPersistedCount, 0) } - func test_start_sentPersistedEventIsThenDeleted() throws { + func test_start_sentPersistedEventIsThenDeleted() async throws { // Given + given(ciChecker) + .isCI() + .willReturn(false) let id: UInt = 1 let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: id) mockPersistor.stubbedReadAllResult = [eventTuple1] @@ -298,7 +309,7 @@ final class AsyncQueueTests: TuistUnitTestCase { // When subject = makeSubject(queue: Queuer.shared) - subject.start() + await subject.start() // Then wait(for: [expectation], timeout: timeout) diff --git a/Tests/TuistAutomationAcceptanceTests/BuildAcceptanceTests.swift b/Tests/TuistAutomationAcceptanceTests/BuildAcceptanceTests.swift new file mode 100644 index 00000000000..1bf664bdff8 --- /dev/null +++ b/Tests/TuistAutomationAcceptanceTests/BuildAcceptanceTests.swift @@ -0,0 +1,196 @@ +import Path +import TuistAcceptanceTesting +import TuistSupport +import XCTest + +@testable import TuistKit + +/// Build projects using Tuist build +final class BuildAcceptanceTestWithTemplates: TuistAcceptanceTestCase { + func test_with_templates() async throws { + try await run(InitCommand.self, "--platform", "ios", "--name", "MyApp") + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + try await run(BuildCommand.self, "MyApp") + try await run(BuildCommand.self, "MyApp", "--configuration", "Debug") + try await run(BuildCommand.self, "MyApp", "--", "-parallelizeTargets", "-enableAddressSanitizer", "YES") + } +} + +final class BuildAcceptanceTestInvalidArguments: TuistAcceptanceTestCase { + func test_with_invalid_arguments() async throws { + try await run(InitCommand.self, "--platform", "ios", "--name", "MyApp") + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + await XCTAssertThrowsSpecific( + try await run(BuildCommand.self, "MyApp", "--", "-scheme", "MyApp"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-scheme") + ) + await XCTAssertThrowsSpecific( + try await run(BuildCommand.self, "MyApp", "--", "-project", "MyApp"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-project") + ) + await XCTAssertThrowsSpecific( + try await run(BuildCommand.self, "MyApp", "--", "-workspace", "MyApp"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-workspace") + ) + // SystemError is verbose and would lead to flakyness + // xcodebuild: error: The flag -addressSanitizerEnabled must be supplied with an argument YES or NO + await XCTAssertThrows( + try await run(BuildCommand.self, "MyApp", "--", "-parallelizeTargets", "YES", "-enableAddressSanitizer") + ) + // xcodebuild: error: option '-configuration' may only be provided once + // Usage: xcodebuild [-project ] ... + await XCTAssertThrows( + try await run(BuildCommand.self, "MyApp", "--configuration", "Debug", "--", "-configuration", "Debug") + ) + } +} + +final class BuildAcceptanceTestAppWithPreviews: TuistAcceptanceTestCase { + func test_with_previews() async throws { + try await setUpFixture(.appWithPreviews) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class BuildAcceptanceTestAppWithFrameworkAndTests: TuistAcceptanceTestCase { + func test_with_framework_and_tests() async throws { + try await setUpFixture(.appWithFrameworkAndTests) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + try await run(BuildCommand.self, "App") + try await run(BuildCommand.self, "AppCustomScheme") + try await run(BuildCommand.self, "App-Workspace") + } +} + +// TODO: Fix -> This currently doesn't build because of a misconfig in Github actions where the tvOS platform is not available +// final class BuildAcceptanceTestAppWithTests: TuistAcceptanceTestCase { +// func test() async throws { +// try await setUpFixture("app_with_tests") +// try await run(GenerateCommand.self) +// try await run(BuildCommand.self) +// try await run(BuildCommand.self, "App") +// try await run(BuildCommand.self, "App-Workspace-iOS") +// try await run(BuildCommand.self, "App-Workspace-macOS") +// try await run(BuildCommand.self, "App-Workspace-tvOS") +// } +// } + +final class BuildAcceptanceTestiOSAppWithCustomConfigurationAndBuildToCustomDirectory: TuistAcceptanceTestCase { + func test_ios_app_with_custom_and_build_to_custom_directory() async throws { + try await setUpFixture(.iosAppWithCustomConfiguration) + try await run(GenerateCommand.self) + try await run( + BuildCommand.self, + "App", + "--configuration", + "debug", + "--build-output-path", + fixturePath.appending(component: "Builds").pathString + ) + let debugPath = fixturePath.appending( + try RelativePath(validating: "Builds/debug-iphonesimulator") + ) + try XCTAssertDirectoryContentEqual(debugPath, ["App.app", "App.swiftmodule", "FrameworkA.framework"]) + try await run( + BuildCommand.self, + "App", + "--configuration", + "release", + "--build-output-path", + fixturePath.appending(component: "Builds").pathString + ) + try XCTAssertDirectoryContentEqual(debugPath, ["App.app", "App.swiftmodule", "FrameworkA.framework"]) + let releasePath = fixturePath.appending( + try RelativePath(validating: "Builds/release-iphonesimulator") + ) + try XCTAssertDirectoryContentEqual( + releasePath, + [ + "App.app", + "App.app.dSYM", + "App.swiftmodule", + "FrameworkA.framework", + "FrameworkA.framework.dSYM", + ] + ) + } +} + +final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithStandardMethod: TuistAcceptanceTestCase { + func test_framework_with_swift_macro_integrated_with_standard_method() async throws { + try await setUpFixture(.frameworkWithSwiftMacro) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "Framework") + } +} + +final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithXcodeProjPrimitives: TuistAcceptanceTestCase { + func test_framework_with_swift_macro_integrated_with_xcode_proj_primitives() async throws { + try await setUpFixture(.frameworkWithNativeSwiftMacro) + try await run(InstallCommand.self) + try await run(GenerateCommand.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 await setUpFixture(.multiplatformAppWithExtension) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} + +final class BuildAcceptanceTestMultiplatformAppWithSDK: TuistAcceptanceTestCase { + func test() async throws { + try await setUpFixture(.multiplatformAppWithSdk) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "macos") + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} + +final class BuildAcceptanceTestMultiplatformµFeatureUnitTestsWithExplicitDependencies: TuistAcceptanceTestCase { + func test() async throws { + try await setUpFixture(.multiplatformµFeatureUnitTestsWithExplicitDependencies) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "ExampleApp", "--platform", "ios") + try await run(TestCommand.self, "ModuleA", "--platform", "ios") + } +} + +final class BuildAcceptanceTestAppWithSPMDependencies: TuistAcceptanceTestCase { + func test() async throws { + try await setUpFixture(.appWithSpmDependencies) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} + +final class BuildAcceptanceTestMultiplatformAppWithMacrosAndEmbeddedWatchOSApp: TuistAcceptanceTestCase { + func test() async throws { + try await setUpFixture(.multiplatformAppWithMacrosAndEmbeddedWatchOSApp) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} + +final class BuildAcceptanceTestIosAppWithSPMDependencies: TuistAcceptanceTestCase { + func test() async throws { + try await setUpFixture(.iosAppWithSpmDependencies) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} diff --git a/Tests/TuistAutomationAcceptanceTests/TestAcceptanceTests.swift b/Tests/TuistAutomationAcceptanceTests/TestAcceptanceTests.swift new file mode 100644 index 00000000000..084f402bcc0 --- /dev/null +++ b/Tests/TuistAutomationAcceptanceTests/TestAcceptanceTests.swift @@ -0,0 +1,119 @@ +import Path +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistKit + +/// Test projects using tuist test +final class TestAcceptanceTests: TuistAcceptanceTestCase { + func test_with_app_with_framework_and_tests() async throws { + try await setUpFixture(.appWithFrameworkAndTests) + try await run(TestCommand.self) + try await run(TestCommand.self, "App") + try await run(TestCommand.self, "--test-targets", "FrameworkTests/FrameworkTests") + try await run(TestCommand.self, "App", "--", "-testLanguage", "en") + } + + func test_with_app_with_test_plan() async throws { + try await setUpFixture(.appWithTestPlan) + try await run(TestCommand.self) + try await run(TestCommand.self, "App", "--test-plan", "All") + } + + func test_with_invalid_arguments() async throws { + try await setUpFixture(.appWithFrameworkAndTests) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-scheme", "App"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-scheme") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-project", "App"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-project") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-workspace", "App"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-workspace") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-testPlan", "TestPlan"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-testPlan") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-skip-test-configuration", "TestPlan"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-skip-test-configuration") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-only-test-configuration", "TestPlan"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-only-test-configuration") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-only-testing", "AppTests"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-only-testing") + ) + await XCTAssertThrowsSpecific( + try await run(TestCommand.self, "App", "--", "-skip-testing", "AppTests"), + XcodeBuildPassthroughArgumentError.alreadyHandled("-skip-testing") + ) + // SystemError is verbose and would lead to flakyness + // xcodebuild: error: The flag -addressSanitizerEnabled must be supplied with an argument YES or NO + await XCTAssertThrows( + try await run(TestCommand.self, "App", "--", "-parallelizeTargets", "YES", "-enableAddressSanitizer") + ) + // xcodebuild: error: option '-configuration' may only be provided once + // Usage: xcodebuild [-project ] ... + await XCTAssertThrows( + try await run(TestCommand.self, "App", "--configuration", "Debug", "--", "-configuration", "Debug") + ) + } +} + +// Feature: Tests projects using Tuist test +// # TODO: Fix +// # Scenario: The project is an application with tests (app_with_tests) +// # Given that tuist is available +// # And I have a working directory +// # Then I copy the fixture app_with_tests into the working directory +// # Then tuist generates the project +// # Then tuist tests the project +// # Then tuist tests the scheme App-Workspace-iOS from the project +// # Then tuist tests the scheme App-Workspace-macOS from the project +// # Then tuist tests the scheme App-Workspace-tvOS from the project +// # Then tuist tests the scheme App from the project +// # Then tuist tests the scheme MacFramework from the project +// # Then tuist tests the scheme App and configuration Debug from the project +// +// # TODO: Fix +// # Scenario: The project is an application with tests (app_with_tests) +// # Given that tuist is available +// # And I have a working directory +// # Then I copy the fixture app_with_tests into the working directory +// # Then tuist tests the project +// # Then App-Workspace-iOS scheme has something to test +// # Then generated project is deleted +// # Then tuist tests the project +// # Then App-Workspace-iOS scheme has nothing to test +// # Then generated project is deleted +// # Then I add an empty line at the end of the file Targets/App/Sources/AppDelegate.swift +// # Then tuist tests the project +// # Then App-Workspace-iOS scheme has something to test + +// Then(/^generated project is deleted/) do +// FileUtils.rm_rf(@workspace_path) +// FileUtils.rm_rf(@xcodeproj_path) +// end + +// Then(/^([a-zA-Z-]+) scheme has nothing to test/) do |scheme_name| +// scheme_file = File.join(Xcodeproj::Workspace.new_from_xcworkspace(@workspace_path).schemes[scheme_name], +// 'xcshareddata', 'xcschemes', "#{scheme_name}.xcscheme") +// scheme = Xcodeproj::XCScheme.new(scheme_file) +// flunk("Project #{scheme_name} scheme has nothing to test") unless scheme.test_action.testables.empty? +// end + +// Then(/^([a-zA-Z-]+) scheme has something to test/) do |scheme_name| +// scheme_file = File.join(Xcodeproj::Workspace.new_from_xcworkspace(@workspace_path).schemes[scheme_name], +// 'xcshareddata', 'xcschemes', "#{scheme_name}.xcscheme") +// scheme = Xcodeproj::XCScheme.new(scheme_file) +// flunk("Project #{scheme_name} scheme has nothing to test") if scheme.test_action.testables.empty? +// end diff --git a/Tests/TuistAutomationIntegrationTests/Simulator/SimulatorControllerIntegrationTests.swift b/Tests/TuistAutomationIntegrationTests/Simulator/SimulatorControllerIntegrationTests.swift index 2e012b8bb4e..af05c2297ff 100644 --- a/Tests/TuistAutomationIntegrationTests/Simulator/SimulatorControllerIntegrationTests.swift +++ b/Tests/TuistAutomationIntegrationTests/Simulator/SimulatorControllerIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift b/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift index 8e35143e554..6d3bb7aa84c 100644 --- a/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift +++ b/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistAutomationTests/ProjectMappers/SkipUITestsProjectMapperTests.swift b/Tests/TuistAutomationTests/ProjectMappers/SkipUITestsProjectMapperTests.swift index 6f42b611394..d8e8187c8f1 100644 --- a/Tests/TuistAutomationTests/ProjectMappers/SkipUITestsProjectMapperTests.swift +++ b/Tests/TuistAutomationTests/ProjectMappers/SkipUITestsProjectMapperTests.swift @@ -1,8 +1,8 @@ import Foundation import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistAutomation diff --git a/Tests/TuistAutomationTests/ProjectMappers/SourceRootPathProjectMapperTests.swift b/Tests/TuistAutomationTests/ProjectMappers/SourceRootPathProjectMapperTests.swift index 5449f955b76..b7aba86328f 100644 --- a/Tests/TuistAutomationTests/ProjectMappers/SourceRootPathProjectMapperTests.swift +++ b/Tests/TuistAutomationTests/ProjectMappers/SourceRootPathProjectMapperTests.swift @@ -1,8 +1,8 @@ import Foundation import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistAutomation diff --git a/Tests/TuistAutomationTests/Utilities/AppBundleLoaderTests.swift b/Tests/TuistAutomationTests/Utilities/AppBundleLoaderTests.swift new file mode 100644 index 00000000000..1a83b53c38c --- /dev/null +++ b/Tests/TuistAutomationTests/Utilities/AppBundleLoaderTests.swift @@ -0,0 +1,72 @@ +import FileSystem +import Foundation +import MockableTest +import Path +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistAutomation + +final class AppBundleLoaderTests: TuistUnitTestCase { + private var subject: AppBundleLoader! + + override func setUp() { + super.setUp() + + subject = AppBundleLoader(fileSystem: FileSystem()) + } + + override func tearDown() { + subject = nil + + super.tearDown() + } + + func test_load_app_bundle() async throws { + // Given + let appBundlePath = fixturePath(path: try RelativePath(validating: "App.app")) + + // When + let appBundle = try await subject.load(appBundlePath) + + // Then + XCTAssertEqual( + appBundle, + AppBundle( + path: appBundlePath, + infoPlist: AppBundle.InfoPlist( + version: Version("1.0"), + name: "App", + bundleId: "io.tuist.App", + minimumOSVersion: Version("17.4"), + supportedPlatforms: [.simulator(.iOS)] + ) + ) + ) + } + + func test_load_app_bundle_when_info_plist_is_missing_does_not_exist() async throws { + // Given + let appBundlePath = try temporaryPath() + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.load(appBundlePath), + AppBundleLoaderError.missingInfoPlist(appBundlePath.appending(component: "Info.plist")) + ) + } + + func test_load_app_bundle_when_decoding_info_plist_failed() async throws { + // Given + let appBundlePath = try temporaryPath() + let infoPlistPath = appBundlePath.appending(component: "Info.plist") + try fileHandler.write("{}", path: infoPlistPath, atomically: true) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.load(appBundlePath), + AppBundleLoaderError.failedDecodingInfoPlist(infoPlistPath, "The data couldn’t be read because it is missing.") + ) + } +} diff --git a/Tests/TuistAutomationTests/Utilities/AppRunnerTests.swift b/Tests/TuistAutomationTests/Utilities/AppRunnerTests.swift new file mode 100644 index 00000000000..22639d33bd4 --- /dev/null +++ b/Tests/TuistAutomationTests/Utilities/AppRunnerTests.swift @@ -0,0 +1,447 @@ +import Foundation +import MockableTest +import struct TSCUtility.Version +import TuistCore +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistAutomation + +final class AppRunnerTests: TuistUnitTestCase { + private var subject: AppRunner! + private var simulatorController: MockSimulatorControlling! + private var userInputReader: MockUserInputReading! + + override func setUp() { + super.setUp() + + simulatorController = .init() + userInputReader = .init() + subject = AppRunner( + simulatorController: simulatorController, + userInputReader: userInputReader + ) + + given(simulatorController) + .launchApp( + bundleId: .any, + device: .any, + arguments: .any + ) + .willReturn() + + given(simulatorController) + .booted(device: .any) + .willProduce { $0 } + + Matcher.register([SimulatorDeviceAndRuntime].self) + } + + override func tearDown() { + subject = nil + userInputReader = nil + simulatorController = nil + + Matcher.reset() + + super.tearDown() + } + + func test_run_single_app_bundle_with_one_currently_booted_device() async throws { + // Given + let appBundle: AppBundle = .test() + let simulatorDeviceAndRuntime: SimulatorDeviceAndRuntime = .test( + device: .test(state: "Booted") + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.iOS), + version: .value(nil), + minVersion: .value(Version("17.4.0")), + deviceName: .value(nil) + ) + .willReturn( + [ + simulatorDeviceAndRuntime, + ] + ) + + given(simulatorController) + .installApp( + at: .value(appBundle.path), + device: .value(simulatorDeviceAndRuntime.device) + ) + .willReturn() + + // When + try await subject.runApp( + [appBundle], + version: nil, + device: nil + ) + + // Then + verify(simulatorController) + .launchApp( + bundleId: .value(appBundle.infoPlist.bundleId), + device: .value(simulatorDeviceAndRuntime.device), + arguments: .value([]) + ) + .called(1) + } + + func test_run_single_app_bundle_with_specific_device() async throws { + // Given + let appBundle: AppBundle = .test() + let simulatorDeviceAndRuntime: SimulatorDeviceAndRuntime = .test( + device: .test(name: "iPhone 15 Pro") + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.iOS), + version: .value(nil), + minVersion: .value(Version("17.4.0")), + deviceName: .value("iPhone 15 Pro") + ) + .willReturn( + [ + simulatorDeviceAndRuntime, + ] + ) + + given(simulatorController) + .installApp( + at: .value(appBundle.path), + device: .value(simulatorDeviceAndRuntime.device) + ) + .willReturn() + + given(userInputReader) + .readValue( + asking: .any, + values: .value([simulatorDeviceAndRuntime]), + valueDescription: .any + ) + .willReturn(simulatorDeviceAndRuntime) + + // When + try await subject.runApp( + [appBundle], + version: nil, + device: "iPhone 15 Pro" + ) + + // Then + verify(simulatorController) + .launchApp( + bundleId: .value(appBundle.infoPlist.bundleId), + device: .value(simulatorDeviceAndRuntime.device), + arguments: .value([]) + ) + .called(1) + } + + func test_run_single_app_bundle_with_specific_version() async throws { + // Given + let appBundle: AppBundle = .test() + let simulatorDeviceAndRuntime: SimulatorDeviceAndRuntime = .test( + device: .test( + name: "iPhone 15 Pro" + ) + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.iOS), + version: .value(Version("18.0.0")), + minVersion: .value(Version("17.4.0")), + deviceName: .value(nil) + ) + .willReturn( + [ + simulatorDeviceAndRuntime, + ] + ) + + given(simulatorController) + .installApp( + at: .value(appBundle.path), + device: .value(simulatorDeviceAndRuntime.device) + ) + .willReturn() + + given(userInputReader) + .readValue( + asking: .any, + values: .value([simulatorDeviceAndRuntime]), + valueDescription: .any + ) + .willReturn(simulatorDeviceAndRuntime) + + // When + try await subject.runApp( + [appBundle], + version: Version("18.0.0"), + device: nil + ) + + // Then + verify(simulatorController) + .launchApp( + bundleId: .value(appBundle.infoPlist.bundleId), + device: .value(simulatorDeviceAndRuntime.device), + arguments: .value([]) + ) + .called(1) + } + + func test_run_single_app_bundle_with_multiple_booted_devices() async throws { + // Given + let appBundle: AppBundle = .test() + let simulatorDeviceAndRuntimeOne: SimulatorDeviceAndRuntime = .test( + device: .test( + state: "Booted", + name: "iPhone 15" + ) + ) + let simulatorDeviceAndRuntimeTwo: SimulatorDeviceAndRuntime = .test( + device: .test( + state: "Booted", + name: "iPhone 15 Pro" + ) + ) + let simulatorDeviceAndRuntimeThree: SimulatorDeviceAndRuntime = .test( + device: .test( + state: "Shutdown", + name: "iPhone 16 Pro" + ) + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.iOS), + version: .value(nil), + minVersion: .value(Version("17.4.0")), + deviceName: .value(nil) + ) + .willReturn( + [ + simulatorDeviceAndRuntimeOne, + simulatorDeviceAndRuntimeTwo, + simulatorDeviceAndRuntimeThree, + ] + ) + + given(userInputReader) + .readValue( + asking: .any, + values: .value([simulatorDeviceAndRuntimeOne, simulatorDeviceAndRuntimeTwo, simulatorDeviceAndRuntimeThree]), + valueDescription: .any + ) + .willReturn(simulatorDeviceAndRuntimeTwo) + + given(simulatorController) + .installApp( + at: .value(appBundle.path), + device: .value(simulatorDeviceAndRuntimeTwo.device) + ) + .willReturn() + + // When + try await subject.runApp( + [appBundle], + version: nil, + device: nil + ) + + // Then + verify(simulatorController) + .launchApp( + bundleId: .value(appBundle.infoPlist.bundleId), + device: .value(simulatorDeviceAndRuntimeTwo.device), + arguments: .value([]) + ) + .called(1) + } + + func test_run_mutliple_app_bundles_with_one_currently_booted_device() async throws { + // Given + let appBundleOne: AppBundle = .test( + infoPlist: .test( + supportedPlatforms: [ + .simulator(.iOS), + ] + ) + ) + let appBundleTwo: AppBundle = .test( + infoPlist: .test( + supportedPlatforms: [ + .simulator(.visionOS), + ] + ) + ) + let appBundleThree: AppBundle = .test( + infoPlist: .test( + supportedPlatforms: [ + .device(.visionOS), + ] + ) + ) + let simulatorDeviceAndRuntime: SimulatorDeviceAndRuntime = .test( + device: .test( + state: "Booted" + ), + runtime: .test( + name: "visionOS 2.0" + ) + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.visionOS), + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn( + [ + simulatorDeviceAndRuntime, + ] + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.iOS), + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn([.test()]) + + given(simulatorController) + .installApp( + at: .value(appBundleTwo.path), + device: .value(simulatorDeviceAndRuntime.device) + ) + .willReturn() + + // When + try await subject.runApp( + [ + appBundleOne, + appBundleTwo, + appBundleThree, + ], + version: nil, + device: nil + ) + + // Then + verify(simulatorController) + .launchApp( + bundleId: .value(appBundleTwo.infoPlist.bundleId), + device: .value(simulatorDeviceAndRuntime.device), + arguments: .value([]) + ) + .called(1) + } + + func test_run_mutliple_app_bundles_with_no_booted_device() async throws { + // Given + let appBundleOne: AppBundle = .test( + infoPlist: .test( + supportedPlatforms: [ + .simulator(.iOS), + ] + ) + ) + let appBundleTwo: AppBundle = .test( + infoPlist: .test( + supportedPlatforms: [ + .simulator(.visionOS), + ] + ) + ) + let appBundleThree: AppBundle = .test( + infoPlist: .test( + supportedPlatforms: [ + .device(.visionOS), + ] + ) + ) + let visionOSSimulatorDeviceAndRuntime: SimulatorDeviceAndRuntime = .test( + runtime: .test( + name: "visionOS 2.0" + ) + ) + + let iOSSimulatorDeviceAndRuntime: SimulatorDeviceAndRuntime = .test( + runtime: .test( + name: "iOS 18.0" + ) + ) + + given(simulatorController) + .findAvailableDevices( + platform: .value(.visionOS), + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn( + [ + visionOSSimulatorDeviceAndRuntime, + ] + ) + given(simulatorController) + .findAvailableDevices( + platform: .value(.iOS), + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn( + [ + iOSSimulatorDeviceAndRuntime, + ] + ) + + given(userInputReader) + .readValue( + asking: .any, + values: .value([iOSSimulatorDeviceAndRuntime, visionOSSimulatorDeviceAndRuntime]), + valueDescription: .any + ) + .willReturn(iOSSimulatorDeviceAndRuntime) + + given(simulatorController) + .installApp( + at: .value(appBundleTwo.path), + device: .value(iOSSimulatorDeviceAndRuntime.device) + ) + .willReturn() + + // When + try await subject.runApp( + [ + appBundleOne, + appBundleTwo, + appBundleThree, + ], + version: nil, + device: nil + ) + + // Then + verify(simulatorController) + .launchApp( + bundleId: .value(appBundleTwo.infoPlist.bundleId), + device: .value(iOSSimulatorDeviceAndRuntime.device), + arguments: .value([]) + ) + .called(1) + } +} diff --git a/Tests/TuistAutomationTests/Utilities/BuildGraphInspectorTests.swift b/Tests/TuistAutomationTests/Utilities/BuildGraphInspectorTests.swift index df1586f99bf..34a4a05a05a 100644 --- a/Tests/TuistAutomationTests/Utilities/BuildGraphInspectorTests.swift +++ b/Tests/TuistAutomationTests/Utilities/BuildGraphInspectorTests.swift @@ -1,8 +1,8 @@ import Foundation import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistAutomation @@ -56,13 +56,16 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { ) ) - let project = Project.test(path: projectPath, schemes: [scheme1, scheme2]) + let project = Project.test( + path: projectPath, + targets: [ + target1, + target2, + ], + schemes: [scheme1, scheme2] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [ - target1.name: target1, - target2.name: target2, - ]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -136,10 +139,14 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { let projectPath = path.appending(component: "Project.xcodeproj") let scheme = Scheme.test(buildAction: .test(targets: [.init(projectPath: projectPath, name: "Core")])) let target = Target.test(name: "Core") - let project = Project.test(path: projectPath) + let project = Project.test( + path: projectPath, + targets: [ + target, + ] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [target.name: target]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -186,10 +193,12 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { targets: [TestableTarget(target: targetReference)] ) ) - let project = Project.test(path: projectPath) + let project = Project.test( + path: projectPath, + targets: [target] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [target.name: target]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -229,13 +238,15 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { testPlans: [testPlan] ) ) - let project = Project.test(path: projectPath) + let project = Project.test( + path: projectPath, + targets: [ + target1, + target2, + ] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [ - target1.name: target1, - target2.name: target2, - ]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -275,13 +286,15 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { testPlans: [testPlan] ) ) - let project = Project.test(path: projectPath) + let project = Project.test( + path: projectPath, + targets: [ + target1, + target2, + ] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [ - target1.name: target1, - target2.name: target2, - ]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -321,13 +334,15 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { testPlans: [testPlan] ) ) - let project = Project.test(path: projectPath) + let project = Project.test( + path: projectPath, + targets: [ + target1, + target2, + ] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [ - target1.name: target1, - target2.name: target2, - ]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -367,13 +382,15 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { testPlans: [testPlan] ) ) - let project = Project.test(path: projectPath) + let project = Project.test( + path: projectPath, + targets: [ + target1, + target2, + ] + ) let graph = Graph.test( - projects: [projectPath: project], - targets: [projectPath: [ - target1.name: target1, - target2.name: target2, - ]] + projects: [projectPath: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -465,55 +482,37 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { ) ) let coreTarget = Target.test(name: "Core") - let coreProject = Project.test( - path: coreProjectPath, - schemes: [coreScheme, coreTestsScheme] - ) - let coreGraphTarget = GraphTarget.test( - target: coreTarget, - project: coreProject - ) let coreTestsTarget = Target.test( name: "CoreTests", product: .unitTests, dependencies: [.target(name: "Core")] ) - let coreTestsGraphTarget = GraphTarget.test( - target: coreTestsTarget, - project: coreProject + let coreProject = Project.test( + path: coreProjectPath, + targets: [ + coreTarget, + coreTestsTarget, + ], + schemes: [coreScheme, coreTestsScheme] ) let kitTarget = Target.test(name: "Kit", dependencies: [.target(name: "Core")]) - let kitProject = Project.test( - path: projectPath, - schemes: [kitScheme, kitTestsScheme] - ) - let kitGraphTarget = GraphTarget.test( - target: kitTarget, - project: kitProject - ) let kitTestsTarget = Target.test( name: "KitTests", product: .unitTests, dependencies: [.target(name: "Kit")] ) - let kitTestsGraphTarget = GraphTarget.test( - target: kitTestsTarget, - project: kitProject + let kitProject = Project.test( + path: projectPath, + targets: [ + kitTarget, + kitTestsTarget, + ], + schemes: [kitScheme, kitTestsScheme] ) let graph = Graph.test( projects: [ kitProject.path: kitProject, coreProject.path: coreProject, - ], - targets: [ - projectPath: [ - kitGraphTarget.target.name: kitGraphTarget.target, - kitTestsGraphTarget.target.name: kitTestsGraphTarget.target, - ], - coreProjectPath: [ - coreGraphTarget.target.name: coreGraphTarget.target, - coreTestsGraphTarget.target.name: coreTestsGraphTarget.target, - ], ] ) let graphTraverser = GraphTraverser(graph: graph) @@ -571,20 +570,14 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { ) let coreProject = Project.test( path: coreProjectPath, + targets: [ + coreTarget, + ], schemes: [coreScheme, coreTestsScheme, coreTestPlanScheme, coreTestPlanTestsScheme] ) - let coreGraphTarget = GraphTarget.test( - target: coreTarget, - project: coreProject - ) let graph = Graph.test( projects: [ coreProject.path: coreProject, - ], - targets: [ - coreProject.path: [ - coreGraphTarget.target.name: coreGraphTarget.target, - ], ] ) let graphTraverser = GraphTraverser(graph: graph) @@ -610,21 +603,28 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { let projectAPath = path.appending(component: "ProjectA.xcodeproj") let schemeA = Scheme.test(buildAction: .test(targets: [.init(projectPath: projectAPath, name: "A")])) - let projectA = Project.test(path: projectAPath, schemes: [schemeA]) let targetA = Target.test(name: "A") + let projectA = Project.test( + path: projectAPath, + targets: [targetA], + schemes: [schemeA] + ) let projectBPath = path.appending(component: "ProjectB.xcodeproj") let schemeB = Scheme.test(buildAction: .test(targets: [.init(projectPath: projectBPath, name: "B")])) - let projectB = Project.test(path: projectBPath, schemes: [schemeB]) let targetB = Target.test(name: "B") + let projectB = Project.test( + path: projectBPath, + targets: [targetB], + schemes: [schemeB] + ) let graph = Graph.test( workspace: Workspace.test(projects: [projectA.path]), projects: [ projectA.path: projectA, projectB.path: projectB, - ], - targets: [projectAPath: [targetA.name: targetA], projectBPath: [targetB.name: targetB]] + ] ) let graphTraverser = GraphTraverser(graph: graph) @@ -667,7 +667,7 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { let path = try temporaryPath() let nonTuistWorkspacePath = path.appending(components: "SPM.xcworkspace") try FileHandler.shared.createFolder(nonTuistWorkspacePath) - let workspacePath = path.appending(component: "TuistApp.xcworkspace") + let workspacePath = path.appending(component: "TuistServer.xcworkspace") try FileHandler.shared.createFolder(workspacePath) try FileHandler.shared.touch(workspacePath.appending(component: Constants.tuistGeneratedFileName)) @@ -685,8 +685,7 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { name: "WorkspaceName", schemes: [ .test(name: "WorkspaceName"), - .test(name: "WorkspaceName-Workspace-iOS"), - .test(name: "WorkspaceName-Workspace-macOS"), + .test(name: "WorkspaceName-Workspace"), ] ) ) @@ -699,8 +698,7 @@ final class BuildGraphInspectorTests: TuistUnitTestCase { XCTAssertEqual( got, [ - .test(name: "WorkspaceName-Workspace-iOS"), - .test(name: "WorkspaceName-Workspace-macOS"), + .test(name: "WorkspaceName-Workspace"), ] ) } diff --git a/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift b/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift index 8ed3206b38d..276d470bcda 100644 --- a/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift +++ b/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift @@ -1,13 +1,14 @@ -import TSCBasic +import MockableTest +import Path +import TuistAutomationTesting import TuistCore -import TuistGraph import TuistSupport +import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistAutomation -@testable import TuistAutomationTesting @testable import TuistCoreTesting -@testable import TuistSupportTesting final class TargetBuilderErrorTests: XCTestCase { func test_errorDescription() { @@ -28,24 +29,37 @@ final class TargetBuilderErrorTests: XCTestCase { } final class TargetBuilderTests: TuistUnitTestCase { - private var buildGraphInspector: MockBuildGraphInspector! - private var xcodeBuildController: MockXcodeBuildController! - private var xcodeProjectBuildDirectoryLocator: MockXcodeProjectBuildDirectoryLocator! - private var simulatorController: MockSimulatorController! + private var buildGraphInspector: MockBuildGraphInspecting! + private var xcodeBuildController: MockXcodeBuildControlling! + private var xcodeProjectBuildDirectoryLocator: MockXcodeProjectBuildDirectoryLocating! + private var simulatorController: MockSimulatorControlling! private var subject: TargetBuilder! override func setUp() { super.setUp() - buildGraphInspector = MockBuildGraphInspector() - xcodeBuildController = MockXcodeBuildController() - xcodeProjectBuildDirectoryLocator = MockXcodeProjectBuildDirectoryLocator() - simulatorController = MockSimulatorController() + buildGraphInspector = .init() + xcodeBuildController = .init() + xcodeProjectBuildDirectoryLocator = .init() + simulatorController = .init() subject = TargetBuilder( buildGraphInspector: buildGraphInspector, xcodeBuildController: xcodeBuildController, xcodeProjectBuildDirectoryLocator: xcodeProjectBuildDirectoryLocator, simulatorController: simulatorController ) + + given(xcodeBuildController) + .build( + .any, + scheme: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + clean: .any, + arguments: .any, + passthroughXcodeBuildArguments: .any + ) + .willReturn() } override func tearDown() { @@ -68,30 +82,28 @@ final class TargetBuilderTests: TuistUnitTestCase { .sdk("iphoneos"), ] let destination = XcodeBuildDestination.device("this_is_a_udid") - let version = "15.2".version() + let version = Version("15.2") let rosetta = true let device = "iPhone 13 Pro" - simulatorController.findAvailableDeviceStub = { _, _version, _, _deviceName in - XCTAssertEqual(_version, version) - XCTAssertEqual(_deviceName, device) - - return .test(device: SimulatorDevice.test(udid: "this_is_a_udid")) - } - buildGraphInspector.buildArgumentsStub = { _, _, _, _ in - 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"))] - } + given(simulatorController) + .findAvailableDevice( + platform: .any, + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn( + .test(device: SimulatorDevice.test(udid: "this_is_a_udid")) + ) + given(buildGraphInspector) + .buildArguments( + project: .any, + target: .any, + configuration: .any, + skipSigning: .any + ) + .willReturn(buildArguments) // When try await subject.buildTarget( @@ -106,8 +118,32 @@ final class TargetBuilderTests: TuistUnitTestCase { device: device, osVersion: version, rosetta: rosetta, - graphTraverser: MockGraphTraverser() + graphTraverser: MockGraphTraversing(), + passthroughXcodeBuildArguments: [] ) + + // Then + verify(simulatorController) + .findAvailableDevice( + platform: .any, + version: .value("15.2".version()), + minVersion: .any, + deviceName: .value(device) + ) + .called(1) + + verify(xcodeBuildController) + .build( + .any, + scheme: .value(scheme.name), + destination: .value(destination), + rosetta: .value(rosetta), + derivedDataPath: .value(nil), + clean: .value(clean), + arguments: .value(buildArguments), + passthroughXcodeBuildArguments: .any + ) + .called(1) } func test_copiesBuildProducts_to_outputPath_defaultConfiguration() async throws { @@ -116,19 +152,40 @@ final class TargetBuilderTests: TuistUnitTestCase { let buildOutputPath = path.appending(component: ".build") let scheme = Scheme.test(name: "A") let workspacePath = try AbsolutePath(validating: "/path/to/project.xcworkspace") - let graphTraverser = MockGraphTraverser() - - xcodeBuildController.buildStub = { _, _, _, _, _, _, _ in - [.standardOutput(.init(raw: "success"))] - } + let graphTraverser = MockGraphTraversing() let xcodeBuildPath = path.appending(components: "Xcode", "DerivedData", "MyProject-hash", "Debug") - xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _ in xcodeBuildPath } + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(xcodeBuildPath) try createFiles([ "Xcode/DerivedData/MyProject-hash/Debug/App.app", "Xcode/DerivedData/MyProject-hash/Debug/App.swiftmodule", ]) + given(buildGraphInspector) + .buildArguments( + project: .any, + target: .any, + configuration: .any, + skipSigning: .any + ) + .willReturn([]) + + given(simulatorController) + .findAvailableDevice( + platform: .any, + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn(.test()) + // When try await subject.buildTarget( .test(), @@ -142,7 +199,8 @@ final class TargetBuilderTests: TuistUnitTestCase { device: nil, osVersion: nil, rosetta: false, - graphTraverser: graphTraverser + graphTraverser: graphTraverser, + passthroughXcodeBuildArguments: [] ) // Then @@ -167,14 +225,35 @@ final class TargetBuilderTests: TuistUnitTestCase { let scheme = Scheme.test(name: "A") let configuration = "TestRelease" let workspacePath = try AbsolutePath(validating: "/path/to/project.xcworkspace") - let graphTraverser = MockGraphTraverser() + let graphTraverser = MockGraphTraversing() + + given(buildGraphInspector) + .buildArguments( + project: .any, + target: .any, + configuration: .any, + skipSigning: .any + ) + .willReturn([]) - xcodeBuildController.buildStub = { _, _, _, _, _, _, _ in - [.standardOutput(.init(raw: "success"))] - } + given(simulatorController) + .findAvailableDevice( + platform: .any, + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn(.test()) let xcodeBuildPath = path.appending(components: "Xcode", "DerivedData", "MyProject-hash", configuration) - xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _ in xcodeBuildPath } + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(xcodeBuildPath) try createFiles([ "Xcode/DerivedData/MyProject-hash/\(configuration)/App.app", "Xcode/DerivedData/MyProject-hash/\(configuration)/App.swiftmodule", @@ -193,7 +272,8 @@ final class TargetBuilderTests: TuistUnitTestCase { device: nil, osVersion: nil, rosetta: false, - graphTraverser: graphTraverser + graphTraverser: graphTraverser, + passthroughXcodeBuildArguments: [] ) // Then diff --git a/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift b/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift index 6355e435b4e..ca828d8e9c3 100644 --- a/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift +++ b/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift @@ -1,14 +1,15 @@ -import TSCBasic +import MockableTest +import Path import struct TSCUtility.Version +import TuistAutomationTesting import TuistCore -import TuistGraph +import TuistCoreTesting import TuistSupport +import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistAutomation -@testable import TuistAutomationTesting -@testable import TuistCoreTesting -@testable import TuistSupportTesting final class TargetRunnerErrorTests: XCTestCase { func test_description() { @@ -29,16 +30,16 @@ final class TargetRunnerErrorTests: XCTestCase { } final class TargetRunnerTests: TuistUnitTestCase { - private var xcodeBuildController: MockXcodeBuildController! - private var xcodeProjectBuildDirectoryLocator: MockXcodeProjectBuildDirectoryLocator! - private var simulatorController: MockSimulatorController! + private var xcodeBuildController: MockXcodeBuildControlling! + private var xcodeProjectBuildDirectoryLocator: MockXcodeProjectBuildDirectoryLocating! + private var simulatorController: MockSimulatorControlling! private var subject: TargetRunner! override func setUp() { super.setUp() - xcodeBuildController = MockXcodeBuildController() - xcodeProjectBuildDirectoryLocator = MockXcodeProjectBuildDirectoryLocator() - simulatorController = MockSimulatorController() + xcodeBuildController = .init() + xcodeProjectBuildDirectoryLocator = .init() + simulatorController = .init() subject = TargetRunner( xcodeBuildController: xcodeBuildController, xcodeProjectBuildDirectoryLocator: xcodeProjectBuildDirectoryLocator, @@ -61,7 +62,14 @@ final class TargetRunnerTests: TuistUnitTestCase { let workspacePath = path.appending(component: "App.xcworkspace") let outputPath = path.appending(component: ".build") let productPath = outputPath.appending(component: "Target.app") - xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _ in outputPath } + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(outputPath) fileHandler.stubExists = { _ in false } // When / Then @@ -88,15 +96,16 @@ final class TargetRunnerTests: TuistUnitTestCase { fileHandler.stubExists = { _ in true } system.succeedCommand(["/path/to/proj.xcworkspace/Target"]) - let expectation = expectation(description: "locates with default configuration") - xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _configuration in - // THEN - XCTAssertEqual(_configuration, BuildConfiguration.debug.name) - expectation.fulfill() - return try AbsolutePath(validating: "/path/to/proj.xcworkspace") - } + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(try AbsolutePath(validating: "/path/to/proj.xcworkspace")) - // WHEN + // When try await subject.runTarget( .test(target: .test(platform: .macOS, product: .commandLineTool)), platform: .macOS, @@ -109,7 +118,15 @@ final class TargetRunnerTests: TuistUnitTestCase { arguments: [] ) - await fulfillment(of: [expectation], timeout: 1) + // Then + verify(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .value(BuildConfiguration.debug.name) + ) + .called(1) } func test_runsExecutable_when_platform_is_macOS_and_product_is_commandLineTool() async throws { @@ -122,7 +139,14 @@ final class TargetRunnerTests: TuistUnitTestCase { let arguments = ["Argument", "--option1", "AnotherArgument", "--option2=true", "-opt3"] fileHandler.stubExists = { _ in true } - xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _ in outputPath } + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(outputPath) system.succeedCommand([executablePath.pathString] + arguments) // THEN @@ -157,44 +181,88 @@ final class TargetRunnerTests: TuistUnitTestCase { let bundleId = "com.tuist.bundleid" fileHandler.stubExists = { _ in true } - xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _ in outputPath } - xcodeBuildController.showBuildSettingsStub = { _, _, _, _ in - let settings = ["PRODUCT_BUNDLE_IDENTIFIER": bundleId] - return [ - graphTarget.target - .name: XcodeBuildSettings(settings, target: graphTarget.target.name, configuration: "Debug"), - ] - } - simulatorController.findAvailableDeviceStub = { _platform, _version, _minVersion, _deviceName in - XCTAssertEqual(_platform, .iOS) - XCTAssertEqual(_version, version) - XCTAssertEqual(_minVersion, minVersion) - XCTAssertEqual(_deviceName, deviceName) - return .test(device: .test(), runtime: .test()) - } - simulatorController.installAppStub = { _appPath, _ in - XCTAssertEqual(_appPath, appPath) - } - simulatorController.launchAppStub = { _bundleId, _, _arguments in - XCTAssertEqual(_bundleId, bundleId) - XCTAssertEqual(_arguments, arguments) - } + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .any, + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(outputPath) + given(xcodeBuildController) + .showBuildSettings( + .any, + scheme: .any, + configuration: .any, + derivedDataPath: .any + ) + .willReturn( + [ + graphTarget.target + .name: XcodeBuildSettings( + ["PRODUCT_BUNDLE_IDENTIFIER": bundleId], + target: graphTarget.target.name, configuration: "Debug" + ), + ] + ) + given(simulatorController) + .launchApp( + bundleId: .any, + device: .any, + arguments: .any + ) + .willReturn() + given(simulatorController) + .askForAvailableDevice( + platform: .any, + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn(.test()) - // THEN - do { - try await subject.runTarget( - graphTarget, - platform: .iOS, - workspacePath: workspacePath, - schemeName: "MyScheme", - configuration: nil, - minVersion: minVersion, - version: version, - deviceName: deviceName, - arguments: arguments + given(simulatorController) + .installApp( + at: .any, + device: .any ) - } catch { - XCTFail("Should not throw") - } + .willReturn() + + // Then + try await subject.runTarget( + graphTarget, + platform: .iOS, + workspacePath: workspacePath, + schemeName: "MyScheme", + configuration: nil, + minVersion: minVersion, + version: version, + deviceName: deviceName, + arguments: arguments + ) + + verify(simulatorController) + .askForAvailableDevice( + platform: .value(.iOS), + version: .value(version), + minVersion: .value(minVersion), + deviceName: .value(deviceName) + ) + .called(1) + + verify(simulatorController) + .installApp( + at: .value(appPath), + device: .any + ) + .called(1) + + verify(simulatorController) + .launchApp( + bundleId: .value(bundleId), + device: .any, + arguments: .value(arguments) + ) + .called(1) } } diff --git a/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift b/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift index 60bddb92740..df167f69053 100644 --- a/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift +++ b/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift @@ -43,18 +43,16 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { system.succeedCommand(command, output: "output") // When - let events = try subject.build( + try await subject.build( target, scheme: scheme, destination: nil, rosetta: false, derivedDataPath: nil, clean: true, - arguments: [] + arguments: [], + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_build_without_device_id_but_arch() async throws { @@ -71,18 +69,16 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { system.succeedCommand(command, output: "output") // When - let events = try subject.build( + try await subject.build( target, scheme: scheme, destination: nil, rosetta: true, derivedDataPath: nil, clean: true, - arguments: [] + arguments: [], + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_build_with_device_id() async throws { @@ -100,18 +96,16 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { system.succeedCommand(command, output: "output") // When - let events = try subject.build( + try await subject.build( target, scheme: scheme, destination: .device("this_is_a_udid"), rosetta: false, derivedDataPath: nil, clean: true, - arguments: [] + arguments: [], + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_build_with_device_id_and_arch() async throws { @@ -129,18 +123,16 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { system.succeedCommand(command, output: "output") // When - let events = try subject.build( + try await subject.build( target, scheme: scheme, destination: .device("this_is_a_udid"), rosetta: true, derivedDataPath: nil, clean: true, - arguments: [] + arguments: [], + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_test_when_device() async throws { @@ -165,7 +157,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { system.succeedCommand(command, output: "output") // When - let events = try subject.test( + try await subject.test( target, scheme: scheme, clean: true, @@ -177,11 +169,9 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil + testPlanConfiguration: nil, + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_test_when_device_arch() async throws { @@ -206,7 +196,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { system.succeedCommand(command, output: "output") // When - let events = try subject.test( + try await subject.test( target, scheme: scheme, clean: true, @@ -218,11 +208,9 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil + testPlanConfiguration: nil, + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_test_when_mac() async throws { @@ -249,7 +237,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { developerEnvironment.stubbedArchitecture = .x8664 // When - let events = try subject.test( + try await subject.test( target, scheme: scheme, clean: true, @@ -261,11 +249,9 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil + testPlanConfiguration: nil, + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_test_with_derived_data() async throws { @@ -292,7 +278,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { developerEnvironment.stubbedArchitecture = .x8664 // When - let events = try subject.test( + try await subject.test( target, scheme: scheme, clean: true, @@ -304,11 +290,9 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil + testPlanConfiguration: nil, + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } func test_test_with_result_bundle_path() async throws { @@ -335,7 +319,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { developerEnvironment.stubbedArchitecture = .x8664 // When - let events = try subject.test( + try await subject.test( target, scheme: scheme, clean: true, @@ -347,11 +331,9 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil + testPlanConfiguration: nil, + passthroughXcodeBuildArguments: [] ) - - let result = try await events.toArray() - XCTAssertEqual(result, [.standardOutput(XcodeBuildOutput(raw: "output"))]) } } diff --git a/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift b/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift deleted file mode 100644 index 2bd23483c37..00000000000 --- a/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift +++ /dev/null @@ -1,122 +0,0 @@ -import TSCBasic -import TuistAcceptanceTesting -import TuistSupport -import XCTest - -/// Build projects using Tuist build -final class BuildAcceptanceTestWithTemplates: TuistAcceptanceTestCase { - func test_with_templates() async throws { - try run(InitCommand.self, "--platform", "ios", "--name", "MyApp") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - try await run(BuildCommand.self, "MyApp") - try await run(BuildCommand.self, "MyApp", "--configuration", "Debug") - } -} - -final class BuildAcceptanceTestAppWithFrameworkAndTests: TuistAcceptanceTestCase { - func test_with_framework_and_tests() async throws { - try setUpFixture(.appWithFrameworkAndTests) - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - try await run(BuildCommand.self, "App") - try await run(BuildCommand.self, "AppCustomScheme") - try await run(BuildCommand.self, "App-Workspace") - } -} - -// TODO: Fix -> This currently doesn't build because of a misconfig in Github actions where the tvOS platform is not available -// final class BuildAcceptanceTestAppWithTests: TuistAcceptanceTestCase { -// func test() async throws { -// try setUpFixture("app_with_tests") -// try await run(GenerateCommand.self) -// try await run(BuildCommand.self) -// try await run(BuildCommand.self, "App") -// try await run(BuildCommand.self, "App-Workspace-iOS") -// try await run(BuildCommand.self, "App-Workspace-macOS") -// try await run(BuildCommand.self, "App-Workspace-tvOS") -// } -// } - -final class BuildAcceptanceTestiOSAppWithCustomConfigurationAndBuildToCustomDirectory: TuistAcceptanceTestCase { - func test_ios_app_with_custom_and_build_to_custom_directory() async throws { - try setUpFixture(.iosAppWithCustomConfiguration) - try await run(GenerateCommand.self) - try await run( - BuildCommand.self, - "App", - "--configuration", - "debug", - "--build-output-path", - fixturePath.appending(component: "Builds").pathString - ) - let debugPath = fixturePath.appending( - try RelativePath(validating: "Builds/debug-iphonesimulator") - ) - try XCTAssertDirectoryContentEqual(debugPath, ["App.app", "App.swiftmodule", "FrameworkA.framework"]) - try await run( - BuildCommand.self, - "App", - "--configuration", - "release", - "--build-output-path", - fixturePath.appending(component: "Builds").pathString - ) - try XCTAssertDirectoryContentEqual(debugPath, ["App.app", "App.swiftmodule", "FrameworkA.framework"]) - let releasePath = fixturePath.appending( - try RelativePath(validating: "Builds/release-iphonesimulator") - ) - try XCTAssertDirectoryContentEqual( - releasePath, - [ - "App.app", - "App.app.dSYM", - "App.swiftmodule", - "FrameworkA.framework", - "FrameworkA.framework.dSYM", - ] - ) - } -} - -final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithStandardMethod: TuistAcceptanceTestCase { - func test_framework_with_swift_macro_integrated_with_standard_method() async throws { - try setUpFixture(.frameworkWithSwiftMacro) - try await run(GenerateCommand.self) - try await run(BuildCommand.self, "Framework") - } -} - -final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithXcodeProjPrimitives: TuistAcceptanceTestCase { - func test_framework_with_swift_macro_integrated_with_xcode_proj_primitives() async throws { - 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(.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/TuistCacheIntegrationTests/CacheGraphContentHasherIntegrationTests.swift b/Tests/TuistCacheIntegrationTests/CacheGraphContentHasherIntegrationTests.swift new file mode 100644 index 00000000000..1ae669c6e3d --- /dev/null +++ b/Tests/TuistCacheIntegrationTests/CacheGraphContentHasherIntegrationTests.swift @@ -0,0 +1,511 @@ +import Foundation +import MockableTest +import Path +import struct TSCUtility.Version +import TuistCore +import TuistHasher +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistCache + +final class ContentHashingIntegrationTests: TuistUnitTestCase { + var subject: CacheGraphContentHasher! + var temporaryDirectoryPath: String! + var source1: SourceFile! + var source2: SourceFile! + var source3: SourceFile! + var source4: SourceFile! + var resourceFile1: ResourceFileElement! + var resourceFile2: ResourceFileElement! + var resourceFolderReference1: ResourceFileElement! + var resourceFolderReference2: ResourceFileElement! + var coreDataModel1: CoreDataModel! + var coreDataModel2: CoreDataModel! + + override func setUp() { + super.setUp() + do { + let temporaryDirectoryPath = try temporaryPath() + source1 = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "1", content: "1") + source2 = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "2", content: "2") + source3 = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "3", content: "3") + source4 = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "4", content: "4") + resourceFile1 = try createTemporaryResourceFile(on: temporaryDirectoryPath, name: "r1", content: "r1") + resourceFile2 = try createTemporaryResourceFile(on: temporaryDirectoryPath, name: "r2", content: "r2") + resourceFolderReference1 = try createTemporaryResourceFolderReference( + on: temporaryDirectoryPath, + name: "rf1", + content: "rf1" + ) + resourceFolderReference2 = try createTemporaryResourceFolderReference( + on: temporaryDirectoryPath, + name: "rf2", + content: "rf2" + ) + _ = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "CoreDataModel1", content: "cd1") + _ = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "CoreDataModel2", content: "cd2") + _ = try createTemporarySourceFile(on: temporaryDirectoryPath, name: "Info.plist", content: "plist") + coreDataModel1 = CoreDataModel( + path: temporaryDirectoryPath.appending(component: "CoreDataModel1"), + versions: [], + currentVersion: "1" + ) + coreDataModel2 = CoreDataModel( + path: temporaryDirectoryPath.appending(component: "CoreDataModel2"), + versions: [], + currentVersion: "2" + ) + } catch { + XCTFail("Error while creating files for stub project") + } + given(swiftVersionProvider).swiftlangVersion().willReturn("5.4.0") + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 3, 0)) + subject = CacheGraphContentHasher(contentHasher: CachedContentHasher()) + } + + override func tearDown() { + subject = nil + source1 = nil + source2 = nil + source3 = nil + source4 = nil + resourceFile1 = nil + resourceFile2 = nil + resourceFolderReference1 = nil + resourceFolderReference2 = nil + coreDataModel1 = nil + coreDataModel2 = nil + super.tearDown() + } + + // MARK: - Sources + + func test_contentHashes_frameworksWithSameSources() throws { + // Given + let framework1Target = makeFramework(sources: [source1, source2]) + let framework2Target = makeFramework(sources: [source2, source1]) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertEqual(contentHash[framework1], contentHash[framework2]) + } + + func test_contentHashes_frameworksWithDifferentSources() throws { + // Given + let framework1Target = makeFramework(sources: [source1, source2]) + let framework2Target = makeFramework(sources: [source3, source4]) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertNotEqual(contentHash[framework1], contentHash[framework2]) + } + + func test_contentHashes_hashIsConsistent() throws { + // Given + let framework1Target = makeFramework(sources: [source1, source2]) + let framework2Target = makeFramework(sources: [source3, source4]) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertEqual(contentHash[framework1], "ed446c23f3327663c85968146cf9bc8b") + XCTAssertEqual(contentHash[framework2], "856dc2d5a7df61c597f53147ab8fa5f8") + } + + // MARK: - Resources + + func test_contentHashes_differentResourceFiles() throws { + // Given + let framework1Target = makeFramework(resources: .init([resourceFile1])) + let framework2Target = makeFramework(resources: .init([resourceFile2])) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertNotEqual(contentHash[framework1], contentHash[framework2]) + } + + func test_contentHashes_differentResourcesFolderReferences() throws { + // Given + let framework1Target = makeFramework(resources: .init([resourceFolderReference1])) + let framework2Target = makeFramework(resources: .init([resourceFolderReference2])) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertNotEqual(contentHash[framework1], contentHash[framework2]) + } + + func test_contentHashes_sameResources() throws { + // Given + let resources: ResourceFileElements = .init([resourceFile1, resourceFolderReference1]) + let framework1Target = makeFramework(resources: resources) + let framework2Target = makeFramework(resources: resources) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertEqual(contentHash[framework1], contentHash[framework2]) + } + + // MARK: - Core Data Models + + func test_contentHashes_differentCoreDataModels() throws { + // Given + let framework1Target = makeFramework(coreDataModels: [coreDataModel1]) + let framework2Target = makeFramework(coreDataModels: [coreDataModel2]) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertNotEqual(contentHash[framework1], contentHash[framework2]) + } + + func test_contentHashes_sameCoreDataModels() throws { + // Given + let framework1Target = makeFramework(coreDataModels: [coreDataModel1]) + let framework2Target = makeFramework(coreDataModels: [coreDataModel1]) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + XCTAssertEqual(contentHash[framework1], contentHash[framework2]) + } + + // MARK: - Target Actions + + // MARK: - Platform + + func test_contentHashes_differentPlatform() throws { + // Given + let framework1Target = makeFramework(platform: .iOS) + let framework2Target = makeFramework(platform: .macOS) + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + XCTAssertNotEqual(contentHash[framework1], contentHash[framework2]) + } + + // MARK: - ProductName + + func test_contentHashes_differentProductName() throws { + // Given + let framework1Target = makeFramework(productName: "1") + let framework2Target = makeFramework(productName: "2") + let project1 = Project.test( + path: try temporaryPath().appending(component: "f1"), + settings: .default, + targets: [framework1Target] + ) + let project2 = Project.test( + path: try temporaryPath().appending(component: "f2"), + settings: .default, + targets: [framework2Target] + ) + let framework1 = GraphTarget(path: project1.path, target: framework1Target, project: project1) + let framework2 = GraphTarget(path: project2.path, target: framework2Target, project: project2) + + let graph = Graph.test( + projects: [ + project1.path: project1, + project2.path: project2, + ] + ) + + // When + let contentHash = try subject.contentHashes( + for: graph, + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + XCTAssertNotEqual(contentHash[framework1], contentHash[framework2]) + } + + // MARK: - Private helpers + + private func createTemporarySourceFile( + on temporaryDirectoryPath: AbsolutePath, + name: String, + content: String + ) throws -> SourceFile { + let filePath = temporaryDirectoryPath.appending(component: name) + try FileHandler.shared.touch(filePath) + try FileHandler.shared.write(content, path: filePath, atomically: true) + return SourceFile(path: filePath, compilerFlags: nil) + } + + private func createTemporaryResourceFile( + on temporaryDirectoryPath: AbsolutePath, + name: String, + content: String + ) throws -> ResourceFileElement { + let filePath = temporaryDirectoryPath.appending(component: name) + try FileHandler.shared.touch(filePath) + try FileHandler.shared.write(content, path: filePath, atomically: true) + return ResourceFileElement.file(path: filePath) + } + + private func createTemporaryResourceFolderReference( + on temporaryDirectoryPath: AbsolutePath, + name: String, + content: String + ) throws -> ResourceFileElement { + let filePath = temporaryDirectoryPath.appending(component: name) + try FileHandler.shared.touch(filePath) + try FileHandler.shared.write(content, path: filePath, atomically: true) + return ResourceFileElement.folderReference(path: filePath) + } + + private func makeFramework( + platform: Platform = .iOS, + productName: String? = nil, + sources: [SourceFile] = [], + resources: ResourceFileElements = .init([]), + coreDataModels: [CoreDataModel] = [], + targetScripts: [TargetScript] = [] + ) -> Target { + .test( + platform: platform, + product: .framework, + productName: productName, + sources: sources, + resources: resources, + coreDataModels: coreDataModels, + scripts: targetScripts + ) + } +} diff --git a/Tests/TuistCacheTests/CacheGraphContentHasherTests.swift b/Tests/TuistCacheTests/CacheGraphContentHasherTests.swift new file mode 100644 index 00000000000..e60efec8691 --- /dev/null +++ b/Tests/TuistCacheTests/CacheGraphContentHasherTests.swift @@ -0,0 +1,183 @@ +import Foundation +import Mockable +import MockableTest +import Path +import struct TSCUtility.Version +import TuistCore +import TuistHasher +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistCache + +final class CacheGraphContentHasherTests: TuistUnitTestCase { + private var graphContentHasher: MockGraphContentHashing! + private var contentHasher: MockContentHashing! + private var defaultConfigurationFetcher: MockDefaultConfigurationFetching! + private var subject: CacheGraphContentHasher! + + override func setUp() { + super.setUp() + + graphContentHasher = .init() + contentHasher = .init() + defaultConfigurationFetcher = MockDefaultConfigurationFetching() + + subject = CacheGraphContentHasher( + graphContentHasher: graphContentHasher, + contentHasher: contentHasher, + versionFetcher: CacheVersionFetcher(), + defaultConfigurationFetcher: defaultConfigurationFetcher, + xcodeController: xcodeController, + swiftVersionProvider: swiftVersionProvider + ) + + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + } + + override func tearDown() { + graphContentHasher = nil + contentHasher = nil + defaultConfigurationFetcher = nil + subject = nil + super.tearDown() + } + + func test_contentHashes_when_no_excluded_targets_all_hashes_are_computed() throws { + // Given + let includedTarget = GraphTarget( + path: "/Project/Path", + target: Target.test(name: "Included", product: .framework), + project: Project.test() + ) + given(graphContentHasher) + .contentHashes( + for: .any, + include: .any, + additionalStrings: .any + ) + .willReturn([:]) + given(defaultConfigurationFetcher) + .fetch(configuration: .any, config: .any, graph: .any) + .willReturn("Debug") + given(swiftVersionProvider).swiftlangVersion().willReturn("5.10.0") + + // When + _ = try subject.contentHashes( + for: Graph.test(), + configuration: "Debug", + config: .test(), + excludedTargets: [] + ) + + // Then + verify(graphContentHasher) + .contentHashes( + for: .any, + include: .matching { filter in + filter(includedTarget) + }, + additionalStrings: .any + ) + .called(1) + } + + func test_contentHashes_when_excluded_targets_excluded_hashes_are_not_computed() throws { + // Given + let excludedTarget = GraphTarget( + path: "/Project/Path", + target: Target.test(name: "Excluded", product: .framework), + project: Project.test() + ) + let includedTarget = GraphTarget( + path: "/Project/Path", + target: Target.test(name: "Included", product: .framework), + project: Project.test() + ) + given(graphContentHasher) + .contentHashes( + for: .any, + include: .any, + additionalStrings: .any + ) + .willReturn([:]) + given(defaultConfigurationFetcher) + .fetch(configuration: .any, config: .any, graph: .any) + .willReturn("Debug") + given(swiftVersionProvider).swiftlangVersion().willReturn("5.10.0") + + // When + _ = try subject.contentHashes( + for: Graph.test(), + configuration: "Debug", + config: .test(), + excludedTargets: ["Excluded"] + ) + + // Then + verify(graphContentHasher) + .contentHashes( + for: .any, + include: .matching { filter in + filter(includedTarget) && !filter(excludedTarget) + }, + additionalStrings: .any + ) + .called(1) + } + + func test_contentHashes_when_excluded_targets_resources_hashes_are_not_computed() throws { + // Given + let project = Project.test() + + let excludedTarget = GraphTarget( + path: "/Project/Path", + target: Target.test(name: "Excluded", product: .framework), + project: project + ) + let excludedTargetResource = GraphTarget( + path: "/Project/Path", + target: Target.test(name: "\(project.name)_Excluded", product: .bundle), + project: project + ) + let includedTarget = GraphTarget( + path: "/Project/Path", + target: Target.test(name: "Included", product: .framework), + project: Project.test() + ) + given(graphContentHasher) + .contentHashes( + for: .any, + include: .any, + additionalStrings: .any + ) + .willReturn([:]) + given(defaultConfigurationFetcher) + .fetch(configuration: .any, config: .any, graph: .any) + .willReturn("Debug") + given(swiftVersionProvider).swiftlangVersion().willReturn("5.10.0") + + // When + _ = try subject.contentHashes( + for: Graph.test(), + configuration: "Debug", + config: .test(), + excludedTargets: ["Excluded"] + ) + + // Then + verify(graphContentHasher) + .contentHashes( + for: .any, + include: .matching { filter in + filter(includedTarget) && !filter(excludedTarget) && !filter(excludedTargetResource) + }, + additionalStrings: .any + ) + .called(1) + } +} diff --git a/Tests/TuistCacheTests/VersionFetcherTests.swift b/Tests/TuistCacheTests/VersionFetcherTests.swift new file mode 100644 index 00000000000..a5efe14a338 --- /dev/null +++ b/Tests/TuistCacheTests/VersionFetcherTests.swift @@ -0,0 +1,20 @@ +import Foundation +import Path +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistCache + +final class CacheVersionFetcherTests: TuistUnitTestCase { + func test_return_the_right_version() { + // Given + let subject = CacheVersionFetcher() + + // When + let got = subject.version() + + // Then + XCTAssertEqual(got, .version2) + } +} diff --git a/Tests/TuistCoreIntegrationTests/MetadataProviders/FrameworkMetadataProviderIntegrationTests.swift b/Tests/TuistCoreIntegrationTests/MetadataProviders/FrameworkMetadataProviderIntegrationTests.swift index dff6f85343e..5df880ae7de 100644 --- a/Tests/TuistCoreIntegrationTests/MetadataProviders/FrameworkMetadataProviderIntegrationTests.swift +++ b/Tests/TuistCoreIntegrationTests/MetadataProviders/FrameworkMetadataProviderIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest @@ -21,28 +21,28 @@ final class FrameworkMetadataProviderIntegrationTests: TuistTestCase { func test_bcsymbolmapPaths() throws { // Given - let carthagePath = try temporaryFixture("Carthage/") - let frameworkPath = FileHandler.shared.glob(carthagePath, glob: "*.framework").first! + let testPath = try temporaryFixture("PrebuiltFramework/") + let frameworkPath = FileHandler.shared.glob(testPath, glob: "*.framework").first! // When let got = try subject.bcsymbolmapPaths(frameworkPath: frameworkPath).sorted() // Then XCTAssertEqual(got, [ - carthagePath.appending(component: "2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap"), - carthagePath.appending(component: "773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap"), + testPath.appending(component: "2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap"), + testPath.appending(component: "773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap"), ]) } func test_dsymPath() throws { // Given - let carthagePath = try temporaryFixture("Carthage/") - let frameworkPath = FileHandler.shared.glob(carthagePath, glob: "*.framework").first! + let testPath = try temporaryFixture("PrebuiltFramework/") + let frameworkPath = FileHandler.shared.glob(testPath, glob: "*.framework").first! // When let got = try subject.dsymPath(frameworkPath: frameworkPath) // Then - XCTAssertTrue(got == carthagePath.appending(component: "\(frameworkPath.basename).dSYM")) + XCTAssertEqual(got, testPath.appending(component: "\(frameworkPath.basename).dSYM")) } } diff --git a/Tests/TuistCoreIntegrationTests/MetadataProviders/PrecompiledMetadataProviderIntegrationTests.swift b/Tests/TuistCoreIntegrationTests/MetadataProviders/PrecompiledMetadataProviderIntegrationTests.swift index ed1477183dd..cb9b445f0c7 100644 --- a/Tests/TuistCoreIntegrationTests/MetadataProviders/PrecompiledMetadataProviderIntegrationTests.swift +++ b/Tests/TuistCoreIntegrationTests/MetadataProviders/PrecompiledMetadataProviderIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistCoreIntegrationTests/RootDirectoryLocatorIntegrationTests.swift b/Tests/TuistCoreIntegrationTests/RootDirectoryLocatorIntegrationTests.swift index 6369723913d..203cd995e4b 100644 --- a/Tests/TuistCoreIntegrationTests/RootDirectoryLocatorIntegrationTests.swift +++ b/Tests/TuistCoreIntegrationTests/RootDirectoryLocatorIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistCoreTests/Automation/XcodeBuildArgumentTests.swift b/Tests/TuistCoreTests/Automation/XcodeBuildArgumentTests.swift index 4401b0b13c7..48196dd18c5 100644 --- a/Tests/TuistCoreTests/Automation/XcodeBuildArgumentTests.swift +++ b/Tests/TuistCoreTests/Automation/XcodeBuildArgumentTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistCore @testable import TuistSupportTesting diff --git a/Tests/TuistCoreTests/Automation/XcodeBuildControllingTests.swift b/Tests/TuistCoreTests/Automation/XcodeBuildControllingTests.swift deleted file mode 100644 index 5cf87cd3417..00000000000 --- a/Tests/TuistCoreTests/Automation/XcodeBuildControllingTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -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/Automation/XcodeBuildTargetTests.swift b/Tests/TuistCoreTests/Automation/XcodeBuildTargetTests.swift index 3a986521093..b977acfa4ac 100644 --- a/Tests/TuistCoreTests/Automation/XcodeBuildTargetTests.swift +++ b/Tests/TuistCoreTests/Automation/XcodeBuildTargetTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift b/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift index bab699cc2a5..c09a03bb0c4 100644 --- a/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift +++ b/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport import XCTest @testable import TuistCore diff --git a/Tests/TuistCoreTests/Graph/CircularDependencyLinterTests.swift b/Tests/TuistCoreTests/Graph/CircularDependencyLinterTests.swift index e2f35346d8f..4a83019248b 100644 --- a/Tests/TuistCoreTests/Graph/CircularDependencyLinterTests.swift +++ b/Tests/TuistCoreTests/Graph/CircularDependencyLinterTests.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore diff --git a/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift b/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift index 9638abf7b83..8fd067b6382 100644 --- a/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistCoreTests/Graph/GraphDependencyTests.swift b/Tests/TuistCoreTests/Graph/GraphDependencyTests.swift index 640433868f9..0a05d977d42 100644 --- a/Tests/TuistCoreTests/Graph/GraphDependencyTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphDependencyTests.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistSupportTesting diff --git a/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift b/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift index 437a16fc912..51886be5011 100644 --- a/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore @@ -45,7 +45,7 @@ final class GraphLoaderTests: TuistUnitTestCase { XCTAssertEqual(graph.projects, [ "/A": projectA, ]) - XCTAssertTrue(graph.targets.isEmpty) + XCTAssertTrue(graph.projects.values.flatMap(\.targets).isEmpty) XCTAssertTrue(graph.dependencies.isEmpty) } @@ -71,7 +71,7 @@ final class GraphLoaderTests: TuistUnitTestCase { "/A": projectA, "/B": projectB, ]) - XCTAssertTrue(graph.targets.isEmpty) + XCTAssertTrue(graph.projects.values.flatMap(\.targets).isEmpty) XCTAssertTrue(graph.dependencies.isEmpty) } @@ -99,10 +99,6 @@ final class GraphLoaderTests: TuistUnitTestCase { "/A": projectA, "/B": projectB, ]) - XCTAssertEqual(graph.targets, [ - "/A": ["A": targetA], - "/B": ["B": targetB], - ]) XCTAssertEqual(graph.dependencies, [ .target(name: "A", path: "/A"): Set([ .target(name: "B", path: "/B"), @@ -132,7 +128,7 @@ final class GraphLoaderTests: TuistUnitTestCase { XCTAssertEqual(graph.projects, [ "/A": projectA, ]) - XCTAssertTrue(graph.targets.isEmpty) + XCTAssertTrue(graph.projects.values.flatMap(\.targets).isEmpty) XCTAssertTrue(graph.dependencies.isEmpty) } @@ -159,10 +155,6 @@ final class GraphLoaderTests: TuistUnitTestCase { "/A": projectA, "/B": projectB, ]) - XCTAssertEqual(graph.targets, [ - "/A": ["A": targetA], - "/B": ["B": targetB], - ]) XCTAssertEqual(graph.dependencies, [ .target(name: "A", path: "/A"): Set([ .target(name: "B", path: "/B"), @@ -216,7 +208,6 @@ final class GraphLoaderTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .dynamic, architectures: [.arm64], - isCarthage: false, status: .required ), ]), @@ -228,7 +219,6 @@ final class GraphLoaderTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .static, architectures: [.x8664], - isCarthage: false, status: .required ), ]), @@ -270,7 +260,6 @@ final class GraphLoaderTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .dynamic, architectures: [.arm64], - isCarthage: false, status: .required ) XCTAssertEqual(graph.dependencies, [ @@ -696,7 +685,6 @@ final class GraphLoaderTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: metadata.linkage, architectures: metadata.architectures, - isCarthage: false, status: .required ) } diff --git a/Tests/TuistCoreTests/Graph/GraphLoadingErrorTests.swift b/Tests/TuistCoreTests/Graph/GraphLoadingErrorTests.swift index efbab6e12bc..52731dfc1e4 100644 --- a/Tests/TuistCoreTests/Graph/GraphLoadingErrorTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphLoadingErrorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistCore diff --git a/Tests/TuistCoreTests/Graph/GraphTargetTests.swift b/Tests/TuistCoreTests/Graph/GraphTargetTests.swift index 8357714e126..5c0a59f9256 100644 --- a/Tests/TuistCoreTests/Graph/GraphTargetTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphTargetTests.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistCoreTesting diff --git a/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift b/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift index 6fda03f8eca..a21857d5529 100644 --- a/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift @@ -1,32 +1,23 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistCoreTesting -@testable import TuistGraphTesting @testable import TuistSupportTesting final class GraphTraverserTests: TuistUnitTestCase { func test_dependsOnXCTest_when_is_framework() { // Given - let project = Project.test() - let frameworkTarget = GraphTarget.test( - path: project.path, - target: Target.test( - name: "Framework", - product: .framework - ) + let target = Target.test( + name: "Framework", + product: .framework ) + let project = Project.test(targets: [target]) let graph = Graph.test( projects: [ project.path: project, - ], - targets: [ - project.path: [ - frameworkTarget.target.name: frameworkTarget.target, - ], ] ) let subject = GraphTraverser(graph: graph) @@ -40,22 +31,14 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_dependsOnXCTest_when_is_tests_bundle() { // Given - let project = Project.test() - let unitTestsTarget = GraphTarget.test( - path: project.path, - target: Target.test( - name: "UnitTests", - product: .unitTests - ) + let target = Target.test( + name: "UnitTests", + product: .unitTests ) + let project = Project.test(targets: [target]) let graph = Graph.test( projects: [ project.path: project, - ], - targets: [ - project.path: [ - unitTestsTarget.target.name: unitTestsTarget.target, - ], ] ) let subject = GraphTraverser(graph: graph) @@ -69,26 +52,22 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_dependsOnXCTest_when_direct_dependency_is_XCTest_SDK() { // Given - let project = Project.test() + let target = Target.test( + name: "Framework", + product: .framework + ) + let project = Project.test(targets: [target]) let frameworkTarget = GraphTarget.test( path: project.path, - target: Target.test( - name: "Framework", - product: .framework - ) + target: target ) let graph = Graph.test( projects: [ project.path: project, ], - targets: [ - project.path: [ - frameworkTarget.target.name: frameworkTarget.target, - ], - ], dependencies: [ .target(name: frameworkTarget.target.name, path: project.path): [ - .testSDK(name: "XCTest"), + .testSDK(name: "XCTest.framework"), ], ] ) @@ -103,25 +82,17 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_dependsOnXCTest_when_settings_enables_search_paths() { // Given - let project = Project.test() - let frameworkTarget = GraphTarget.test( - path: project.path, - target: Target.test( - name: "Framework", - product: .framework, - settings: .test(base: [ - "ENABLE_TESTING_SEARCH_PATHS": "YES", - ]) - ) + let target = Target.test( + name: "Framework", + product: .framework, + settings: .test(base: [ + "ENABLE_TESTING_SEARCH_PATHS": "YES", + ]) ) + let project = Project.test(targets: [target]) let graph = Graph.test( projects: [ project.path: project, - ], - targets: [ - project.path: [ - frameworkTarget.target.name: frameworkTarget.target, - ], ] ) let subject = GraphTraverser(graph: graph) @@ -138,15 +109,12 @@ final class GraphTraverserTests: TuistUnitTestCase { let path = AbsolutePath.root let app = Target.test(name: "App", product: .app) let framework = Target.test(name: "Framework", product: .framework) - let project = Project.test(path: path) + let project = Project.test(path: path, targets: [app, framework]) // Given: Value Graph let graph = Graph.test( path: path, - projects: [path: project], - targets: [ - "/": ["App": app, "Framework": framework], - ] + projects: [path: project] ) let subject = GraphTraverser(graph: graph) @@ -161,16 +129,13 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let path = AbsolutePath.root let app = Target.test(name: "App", product: .app) - let project = Project.test(path: path) let framework = Target.test(name: "Framework", product: .framework) + let project = Project.test(path: path, targets: [app, framework]) // When: Value Graph let graph = Graph.test( path: path, - projects: [path: project], - targets: [ - path: ["App": app, "Framework": framework], - ] + projects: [path: project] ) let subject = GraphTraverser(graph: graph) @@ -183,17 +148,10 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_directStaticDependencies() { // Given - let project = Project.test() let path = AbsolutePath.root let framework = Target.test(name: "Framework", product: .framework) let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary) - let targets: [AbsolutePath: [String: Target]] = [ - path: [ - framework.name: framework, - staticLibrary.name: staticLibrary, - ], - ] - + let project = Project.test(targets: [framework, staticLibrary]) let dependencies: [GraphDependency: Set] = [ .target(name: framework.name, path: path): Set([.target(name: staticLibrary.name, path: path)]), ] @@ -202,7 +160,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: targets, dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -220,24 +177,19 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_directLocalTargetDependencies() { // Given // A -> B -> C - let project = Project.test() let a = Target.test(name: "A") let b = Target.test(name: "B") let c = Target.test(name: "C") + let project = Project.test(targets: [a, b, c]) + let dependencies: [GraphDependency: Set] = [ .target(name: a.name, path: project.path): Set([.target(name: b.name, path: project.path)]), .target(name: b.name, path: project.path): Set([.target(name: c.name, path: project.path)]), ] - let targets: [AbsolutePath: [String: Target]] = [project.path: [ - a.name: a, - b.name: b, - c.name: c, - ]] // Given: Value Graph let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: targets, dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -254,31 +206,21 @@ final class GraphTraverserTests: TuistUnitTestCase { // Project A: A1 -> A2 // -> (Project B) B1 // Project B: B1 - let projectA = Project.test(path: "/ProjectA", name: "ProjectA") - let projectB = Project.test(path: "/ProjectB", name: "ProjectB") let a1 = Target.test(name: "A1") let a2 = Target.test(name: "A2") let b1 = Target.test(name: "B1") + let projectA = Project.test(path: "/ProjectA", name: "ProjectA", targets: [a1, a2]) + let projectB = Project.test(path: "/ProjectB", name: "ProjectB", targets: [b1]) let dependencies: [GraphDependency: Set] = [ .target(name: a1.name, path: projectA.path): Set([ .target(name: a2.name, path: projectA.path), .target(name: b1.name, path: projectB.path), ]), ] - let targets: [AbsolutePath: [String: Target]] = [ - projectA.path: [ - a1.name: a1, - a2.name: a2, - ], - projectB.path: [ - b1.name: b1, - ], - ] // Given: Value Graph let graph = Graph.test( path: projectA.path, projects: [projectA.path: projectA, projectB.path: projectB], - targets: targets, dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -295,33 +237,23 @@ final class GraphTraverserTests: TuistUnitTestCase { // Project A: A1 -> A2 // -> (Project B) B1 // Project B: B1 - let projectA = Project.test(path: "/ProjectA", name: "ProjectA") - let projectB = Project.test(path: "/ProjectB", name: "ProjectB") let a1 = Target.test(name: "A1") let a2 = Target.test(name: "A2") let b1 = Target.test( name: "B1" ) + let projectA = Project.test(path: "/ProjectA", name: "ProjectA", targets: [a1, a2]) + let projectB = Project.test(path: "/ProjectB", name: "ProjectB", targets: [b1]) let dependencies: [GraphDependency: Set] = [ .target(name: a1.name, path: projectA.path): Set([ .target(name: a2.name, path: projectA.path), .target(name: b1.name, path: projectB.path), ]), ] - let targets: [AbsolutePath: [String: Target]] = [ - projectA.path: [ - a1.name: a1, - a2.name: a2, - ], - projectB.path: [ - b1.name: b1, - ], - ] // Given: Value Graph let graph = Graph.test( path: projectA.path, projects: [projectA.path: projectA, projectB.path: projectB], - targets: targets, dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -342,10 +274,10 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_resourceBundleDependencies_returns_an_empty_list_when_a_dependency_can_host_resources() { // Given // App -> WatchApp -> Bundle - let project = Project.test() let app = Target.test(name: "App", platform: .iOS, product: .app) let watchApp = Target.test(name: "WatchApp", platform: .iOS, product: .watch2App) let bundle = Target.test(name: "Bundle", platform: .iOS, product: .bundle) + let project = Project.test(targets: [app, watchApp, bundle]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set([.target(name: watchApp.name, path: project.path)]), @@ -357,11 +289,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - watchApp.name: watchApp, - bundle.name: bundle, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -376,10 +303,10 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_resourceBundleDependencies() { // Given // App -> StaticLibrary -> Bundle - let project = Project.test() let app = Target.test(name: "App", product: .app) let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary) let bundle = Target.test(name: "Bundle", product: .bundle) + let project = Project.test(targets: [app, staticLibrary, bundle]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set([.target(name: staticLibrary.name, path: project.path)]), @@ -391,11 +318,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticLibrary.name: staticLibrary, - bundle.name: bundle, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -409,12 +331,110 @@ final class GraphTraverserTests: TuistUnitTestCase { ]) } + func test_resourceBundleDependencies_when_app_depends_on_external_static_framework_with_resources_via_dynamic_framework() { + // Given + // App -> DynamicFramework -> StaticLibrary (External) -> Bundle + let app = Target.test(name: "App", product: .app) + let dynamicFramework = Target.test(name: "DynamicFramework", product: .framework) + let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary) + let bundle = Target.test(name: "Bundle", product: .bundle) + let project = Project.test(targets: [app, dynamicFramework]) + let externalProject = Project.test( + path: try! AbsolutePath(validating: "/ExternalProject"), + targets: [staticLibrary, bundle], + isExternal: true + ) + + let dependencies: [GraphDependency: Set] = [ + .target(name: app.name, path: project.path): Set([.target(name: dynamicFramework.name, path: project.path)]), + .target(name: dynamicFramework.name, path: project.path): Set([.target( + name: staticLibrary.name, + path: externalProject.path + )]), + .target(name: staticLibrary.name, path: externalProject.path): Set([.target( + name: bundle.name, + path: externalProject.path + )]), + .target(name: bundle.name, path: externalProject.path): Set([]), + ] + + // Given: Value Graph + let graph = Graph.test( + path: project.path, + projects: [ + project.path: project, + externalProject.path: externalProject, + ], + dependencies: dependencies + ) + let subject = GraphTraverser(graph: graph) + + // When + let appBundleDependencies = subject.resourceBundleDependencies(path: project.path, name: app.name).sorted() + let dynamicFrameworkBundleDependencies = subject.resourceBundleDependencies( + path: project.path, + name: dynamicFramework.name + ).sorted() + + // Then + XCTAssertEqual(appBundleDependencies, [ + .product(target: bundle.name, productName: bundle.productNameWithExtension), + ]) + XCTAssertEqual(dynamicFrameworkBundleDependencies, []) + } + + func test_resourceBundleDependencies_when_app_extension_depends_on_external_static_framework_with_resources() { + // Given + // AppExtension -> StaticLibrary (External) -> Bundle + let appExtension = Target.test(name: "AppExtension", product: .appExtension) + let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary) + let bundle = Target.test(name: "Bundle", product: .bundle) + let project = Project.test(targets: [appExtension]) + let externalProject = Project.test( + path: try! AbsolutePath(validating: "/ExternalProject"), + targets: [staticLibrary, bundle], + isExternal: true + ) + + let dependencies: [GraphDependency: Set] = [ + .target(name: appExtension.name, path: project.path): Set([.target( + name: staticLibrary.name, + path: externalProject.path + )]), + .target(name: staticLibrary.name, path: externalProject.path): Set([.target( + name: bundle.name, + path: externalProject.path + )]), + .target(name: bundle.name, path: externalProject.path): Set([]), + ] + + // Given: Value Graph + let graph = Graph.test( + path: project.path, + projects: [ + project.path: project, + externalProject.path: externalProject, + ], + dependencies: dependencies + ) + let subject = GraphTraverser(graph: graph) + + // When + let appExtensionBundleDependencies = subject.resourceBundleDependencies(path: project.path, name: appExtension.name) + .sorted() + + // Then + XCTAssertEqual(appExtensionBundleDependencies, [ + .product(target: bundle.name, productName: bundle.productNameWithExtension), + ]) + } + func test_resourceBundleDependencies_when_the_target_doesnt_support_resources() { // Given // StaticLibrary -> Bundle - let project = Project.test() let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary) let bundle = Target.test(name: "Bundle", product: .bundle) + let project = Project.test(targets: [staticLibrary, bundle]) let dependencies: [GraphDependency: Set] = [ .target(name: staticLibrary.name, path: project.path): Set([.target(name: bundle.name, path: project.path)]), @@ -425,10 +445,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - staticLibrary.name: staticLibrary, - bundle.name: bundle, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -444,7 +460,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let bundle = Target.test(name: "Bundle1", product: .bundle) let app = Target.test(name: "App", product: .bundle) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [bundle, app]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set([.target(name: bundle.name, path: project.path)]), @@ -455,10 +471,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - bundle.name: bundle, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -475,10 +487,10 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_resourceBundleDependencies_fromProjectDependency() { // Given let bundle = Target.test(name: "Bundle1", product: .bundle) - let projectA = Project.test(path: "/path/a") + let projectA = Project.test(path: "/path/a", targets: [bundle]) let app = Target.test(name: "App", product: .app) - let projectB = Project.test(path: "/path/b") + let projectB = Project.test(path: "/path/b", targets: [app]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: projectB.path): Set([.target(name: bundle.name, path: projectA.path)]), @@ -492,10 +504,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projectA.path: projectA, projectB.path: projectB, ], - targets: [ - projectA.path: [bundle.name: bundle], - projectB.path: [app.name: app], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -531,13 +539,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projectA.path: projectA, projectB.path: projectB, ], - targets: [ - projectA.path: [ - bundle.name: bundle, - staticFramework.name: staticFramework, - ], - projectB.path: [app.name: app], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -583,15 +584,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projectA.path: projectA, projectB.path: projectB, ], - targets: [ - projectA.path: [ - bundle1.name: bundle1, - bundle2.name: bundle2, - staticFramework1.name: staticFramework1, - staticFramework2.name: staticFramework2, - ], - projectB.path: [app.name: app], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -641,15 +633,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projectA.path: projectA, projectB.path: projectB, ], - targets: [ - projectA.path: [ - bundle.name: bundle, - staticFramework1.name: staticFramework1, - staticFramework2.name: staticFramework2, - dynamicFramework.name: dynamicFramework, - ], - projectB.path: [app.name: app], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -703,14 +686,6 @@ final class GraphTraverserTests: TuistUnitTestCase { staticFrameworkProject.path: staticFrameworkProject, appProject.path: appProject, ], - targets: [ - staticFrameworkProject.path: [ - staticFramework.name: staticFramework, - ], - appProject.path: [ - appTests.name: appTests, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -763,14 +738,6 @@ final class GraphTraverserTests: TuistUnitTestCase { staticFrameworkProject.path: staticFrameworkProject, appProject.path: appProject, ], - targets: [ - staticFrameworkProject.path: [ - staticFramework.name: staticFramework, - ], - appProject.path: [ - app.name: app, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -823,14 +790,6 @@ final class GraphTraverserTests: TuistUnitTestCase { dynamicFrameworkProject.path: dynamicFrameworkProject, appProject.path: appProject, ], - targets: [ - dynamicFrameworkProject.path: [ - dynamicFramework.name: dynamicFramework, - ], - appProject.path: [ - app.name: app, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -854,14 +813,13 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_target_from_dependency() { // Given - let project = Project.test() let app = Target.test(name: "App", product: .app) + let project = Project.test(targets: [app]) // Given: Value Graph let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [app.name: app]], dependencies: [.target(name: app.name, path: project.path): Set()] ) let subject = GraphTraverser(graph: graph) @@ -876,10 +834,10 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_allDependencies() throws { // Given // App -> StaticLibrary -> Bundle - let project = Project.test() let app = Target.test(name: "App", product: .app) let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary, productName: "StaticLibrary") let bundle = Target.test(name: "Bundle", product: .bundle) + let project = Project.test(targets: [app, staticLibrary, bundle]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set([.target(name: staticLibrary.name, path: project.path)]), @@ -891,11 +849,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticLibrary.name: staticLibrary, - bundle.name: bundle, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -918,12 +871,12 @@ final class GraphTraverserTests: TuistUnitTestCase { // App -> StaticLibrary -> Bundle // | // -> FrameworkA -> FrameworkC - let project = Project.test() let app = Target.test(name: "App", product: .app) let staticLibrary = Target.test(name: "StaticLibrary", product: .staticLibrary) let frameworkA = Target.test(name: "FrameworkA", product: .framework) let frameworkB = Target.test(name: "FrameworkB", product: .framework) let bundle = Target.test(name: "Bundle", product: .bundle) + let project = Project.test(targets: [app, staticLibrary, frameworkA, frameworkB, bundle]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set([ @@ -939,13 +892,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticLibrary.name: staticLibrary, - bundle.name: bundle, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -975,7 +921,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let target = Target.test(name: "Main") let dependency = Target.test(name: "AppExtension", product: .appExtension) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependency]) let dependencies: [GraphDependency: Set] = [ .target(name: target.name, path: project.path): Set([.target(name: dependency.name, path: project.path)]), .target(name: dependency.name, path: project.path): Set([]), @@ -985,10 +931,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - target.name: target, - dependency.name: dependency, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1004,7 +946,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // When let target = Target.test(name: "Main") let dependency = Target.test(name: "StickerPackExtension", product: .stickerPackExtension) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependency]) let dependencies: [GraphDependency: Set] = [ .target(name: target.name, path: project.path): Set([.target(name: dependency.name, path: project.path)]), .target(name: dependency.name, path: project.path): Set([]), @@ -1014,10 +956,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - target.name: target, - dependency.name: dependency, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1043,10 +981,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - messageExtension.name: messageExtension, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1062,14 +996,13 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_appClipDependencies() throws { // Given - let project = Project.test() let app = Target.test(name: "app", product: .app) let appClip = Target.test(name: "clip", product: .appClip) + let project = Project.test(targets: [app, appClip]) // Given: Value graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [app.name: app, appClip.name: appClip]], dependencies: [.target( name: app.name, path: project.path @@ -1087,15 +1020,14 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_buildsForMacCatalyst_returns_false_when_someDependenciesCantBuildForMacCatalyst() { // Given - let project = Project.test() let app = Target.test(name: "app", destinations: [.macCatalyst], product: .app) let library = Target.test(name: "library-a", destinations: [.iPhone], product: .dynamicLibrary) let transitiveLibrary = Target.test(name: "library-b", destinations: [.iPhone], product: .dynamicLibrary) + let project = Project.test(targets: [app, library, transitiveLibrary]) // Given: Value graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [app.name: app, library.name: library, transitiveLibrary.name: transitiveLibrary]], dependencies: [ .target( name: app.name, @@ -1118,15 +1050,14 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_buildsForMacCatalyst_returns_false_when_aTargetDoesntSupportCatalystRegardlessOfItsDependencies() { // Given - let project = Project.test() let app = Target.test(name: "app", destinations: [.iPhone], product: .app) let library = Target.test(name: "library-a", destinations: [.macCatalyst], product: .dynamicLibrary) let transitiveLibrary = Target.test(name: "library-b", destinations: [.macCatalyst], product: .dynamicLibrary) + let project = Project.test(targets: [app, library, transitiveLibrary]) // Given: Value graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [app.name: app, library.name: library, transitiveLibrary.name: transitiveLibrary]], dependencies: [ .target( name: app.name, @@ -1149,15 +1080,14 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_buildsForMacCatalyst_returns_true_when_aTargetAndItsDependenciesSupportCatalyst() { // Given - let project = Project.test() let app = Target.test(name: "app", destinations: [.macCatalyst], product: .app) let library = Target.test(name: "library-a", destinations: [.macCatalyst], product: .dynamicLibrary) let transitiveLibrary = Target.test(name: "library-b", destinations: [.macCatalyst], product: .dynamicLibrary) + let project = Project.test(targets: [app, library, transitiveLibrary]) // Given: Value graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [app.name: app, library.name: library, transitiveLibrary.name: transitiveLibrary]], dependencies: [ .target( name: app.name, @@ -1192,12 +1122,10 @@ final class GraphTraverserTests: TuistUnitTestCase { 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, @@ -1220,12 +1148,11 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let target = Target.test(name: "Main", product: .framework) let dependency = Target.test(name: "Dependency", product: .framework) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependency]) // Given: Value Graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [dependency.name: dependency, target.name: target]], dependencies: [ .target( name: target.name, @@ -1248,12 +1175,11 @@ final class GraphTraverserTests: TuistUnitTestCase { let target = Target.test(name: "Main") let dependencyA = Target.test(name: "DependencyA", product: .framework) let dependencyB = Target.test(name: "DependencyB", product: .framework, settings: mergeableSettings) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependencyA, dependencyB]) // Given: Value Graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [dependencyA.name: dependencyA, dependencyB.name: dependencyB, target.name: target]], dependencies: [ .target( name: target.name, @@ -1283,12 +1209,11 @@ final class GraphTraverserTests: TuistUnitTestCase { let target = Target.test(name: "Main", mergedBinaryType: .automatic) let dependencyA = Target.test(name: "DependencyA", product: .framework) let dependencyB = Target.test(name: "DependencyB", product: .framework, mergeable: true) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependencyA, dependencyB]) // Given: Value Graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [dependencyA.name: dependencyA, dependencyB.name: dependencyB, target.name: target]], dependencies: [ .target( name: target.name, @@ -1321,12 +1246,10 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: [ .target(name: target.name, path: project.path): Set(arrayLiteral: frameworkDependency), ] @@ -1400,7 +1323,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [app.name: app]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1481,7 +1403,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [app.name: app]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1498,7 +1419,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let target = Target.test(name: "Main") let dependency = Target.test(name: "Dependency", product: .framework) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependency]) // Given: Value Graph let frameworkDependency = GraphDependency.testFramework( @@ -1507,8 +1428,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ .target(name: target.name, path: project.path): Set(arrayLiteral: .target(name: dependency.name, path: project.path)), @@ -1516,7 +1436,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target, dependency.name: dependency]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1544,13 +1463,11 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .static, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] )), ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1583,11 +1500,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - watchExtension.name: watchExtension, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1623,11 +1535,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - xpc.name: xpc, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1651,7 +1558,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let app = Target.test(name: "App", product: .app) let tests = Target.test(name: "AppTests", product: .unitTests) - let project = Project.test(path: "/path/") + let project = Project.test(path: "/path/", targets: [app, tests, framework]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -1661,11 +1568,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - tests.name: tests, - framework.name: framework, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1680,8 +1582,8 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_embeddableDependencies_when_nonHostedTestTarget_dynamic_dependencies() throws { // Given let unitTests = Target.test(name: "AppUnitTests", product: .unitTests) - let project = Project.test(path: "/path/a") let target = Target.test(name: "LocallyBuiltFramework", product: .framework) + let project = Project.test(path: "/path/a", targets: [unitTests, target]) // Given: Value Graph let precompiledDependency = GraphDependency.testFramework( @@ -1690,8 +1592,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ .target(name: target.name, path: project.path): Set(), @@ -1702,10 +1603,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - unitTests.name: unitTests, - target.name: target, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1731,7 +1628,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let app = Target.test(name: "App", product: .app) let tests = Target.test(name: "AppTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [framework, staticFramework, app, tests]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -1751,12 +1648,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - framework.name: framework, - staticFramework.name: staticFramework, - app.name: app, - tests.name: tests, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1772,7 +1663,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let app = Target.test(name: "App", product: .app) let uiTests = Target.test(name: "AppUITests", product: .uiTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, uiTests]) // Given: Value Graph let precompiledDependency = GraphDependency.testFramework( @@ -1781,8 +1672,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set(arrayLiteral: precompiledDependency), @@ -1791,10 +1681,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - uiTests.name: uiTests, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1824,7 +1710,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1856,7 +1741,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1890,7 +1774,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1902,27 +1785,31 @@ final class GraphTraverserTests: TuistUnitTestCase { XCTAssertEqual(got.first, GraphDependencyReference(macroXCFramework)) } - func test_linkableDependencies_whenPrecompiled() throws { + func test_linkableDependencies_doesntReturnTransitiveStaticPrecompiledBinaries_when_thereAreIntermediatePrecompiledBinariesThatCanLink( + ) throws { // Given let target = Target.test(name: "Main") let project = Project.test(targets: [target]) // Given: Value Graph - let precompiledDependency = GraphDependency.testFramework( + let precompiledDynamicFramework = GraphDependency.testFramework( path: "/test/test.framework", binaryPath: "/test/test.framework/test", dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] + ) + let precompiledTransitiveXCFramework = GraphDependency.testXCFramework( + path: "/test/b.xcframework", + linking: .static ) let dependencies: [GraphDependency: Set] = [ - .target(name: target.name, path: project.path): Set(arrayLiteral: precompiledDependency), + .target(name: target.name, path: project.path): Set(arrayLiteral: precompiledDynamicFramework), + precompiledDynamicFramework: [precompiledTransitiveXCFramework], ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -1931,7 +1818,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let got = try subject.linkableDependencies(path: project.path, name: target.name).sorted() // Then - XCTAssertEqual(got.first, GraphDependencyReference(precompiledDependency)) + XCTAssertEqual(got, [GraphDependencyReference(precompiledDynamicFramework)]) } func test_linkableAndEmbeddableDependencies_when_appDependensOnPrecompiledStaticBinaryWithPrecompiledStaticBinaryDependency( @@ -1949,8 +1836,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .static, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencyPrecompiledStaticBinaryA = GraphDependency.testFramework( path: "/test/StaticFrameworkA.framework", @@ -1958,8 +1844,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .static, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ @@ -1969,7 +1854,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2005,8 +1889,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencyPrecompiledDynamicBinaryA = GraphDependency.testFramework( path: "/test/DynamicFrameworkA.framework", @@ -2014,18 +1897,17 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) + let dependencyPrecompiledXCFramework = GraphDependency.testXCFramework(linking: .static) let dependencies: [GraphDependency: Set] = [ .target(name: target.name, path: project.path): Set(arrayLiteral: dependencyPrecompiledDynamicBinaryA), dependencyPrecompiledDynamicBinaryA: - Set(arrayLiteral: dependencyPrecompiledDynamicBinaryB), + Set([dependencyPrecompiledDynamicBinaryB, dependencyPrecompiledXCFramework]), ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2049,6 +1931,115 @@ final class GraphTraverserTests: TuistUnitTestCase { ]) } + func test_linkableAndEmbeddableDependencies_when_appDependensOnPrecompiledDynamicXCFrameworkWithStaticXCFrameworkDependency( + ) throws { + // App ---(depends on)---> Dynamic XCFramework ----> Static XCFramework (A) ----> Static XCFramework (B) + + // Given + let target = Target.test(name: "Main") + let project = Project.test(targets: [target]) + + // Given: Value Graph + let dependencyDynamicXCFramework = GraphDependency.testXCFramework( + path: "/test/DynamicFramework.xcframework", + linking: .dynamic + ) + let dependencyStaticXCFrameworkA = GraphDependency.testXCFramework( + path: "/test/StaticFrameworkA.xcframework", + linking: .static + ) + let dependencyStaticXCFrameworkB = GraphDependency.testXCFramework( + path: "/test/StaticFrameworkB.xcframework", + linking: .static + ) + + let dependencies: [GraphDependency: Set] = [ + .target(name: target.name, path: project.path): Set(arrayLiteral: dependencyDynamicXCFramework), + dependencyDynamicXCFramework: Set(arrayLiteral: dependencyStaticXCFrameworkA), + dependencyStaticXCFrameworkA: Set(arrayLiteral: dependencyStaticXCFrameworkB), + ] + let graph = Graph.test( + projects: [project.path: project], + dependencies: dependencies + ) + let subject = GraphTraverser(graph: graph) + + // When + let got = try subject.linkableDependencies(path: project.path, name: target.name).sorted() + + // Then + XCTAssertEqual(got, [ + GraphDependencyReference(dependencyDynamicXCFramework), + GraphDependencyReference(dependencyStaticXCFrameworkA), + GraphDependencyReference(dependencyStaticXCFrameworkB), + ]) + + // When + let embeddable = subject.embeddableFrameworks(path: project.path, name: target.name) + + // Then + XCTAssertBetterEqual(embeddable, [ + GraphDependencyReference(dependencyDynamicXCFramework), + ]) + } + + func test_linkableAndEmbeddableDependencies_when_appDependensOnPrecompiledDynamicXCFrameworkWithStaticXCFrameworkDependencyWithALinkedSystemLibrary( + ) throws { + // App ---(depends on)---> Dynamic XCFramework ----> Static XCFramework (A) ----> libc++.tbd + + // Given + let target = Target.test(name: "Main") + let project = Project.test(targets: [target]) + + // Given: Value Graph + let dependencyDynamicXCFramework = GraphDependency.testXCFramework( + path: "/test/DynamicFramework.xcframework", + linking: .dynamic + ) + let dependencyStaticXCFrameworkA = GraphDependency.testXCFramework( + path: "/test/StaticFrameworkA.xcframework", + linking: .static + ) + let dependencyLibCpp = GraphDependency.testSDK( + name: "libc++.tbd", + path: try AbsolutePath(validating: "/libc++.tbd") + ) + + let dependencies: [GraphDependency: Set] = [ + .target(name: target.name, path: project.path): Set(arrayLiteral: dependencyDynamicXCFramework), + dependencyDynamicXCFramework: Set(arrayLiteral: dependencyStaticXCFrameworkA), + dependencyStaticXCFrameworkA: Set(arrayLiteral: dependencyLibCpp), + ] + let graph = Graph.test( + projects: [project.path: project], + dependencies: dependencies + ) + let subject = GraphTraverser(graph: graph) + + // When + let got = try subject.linkableDependencies(path: project.path, name: target.name).sorted() + + // Then + XCTAssertBetterEqual(got, [ + .sdk( + path: try AbsolutePath(validating: "/libc++.tbd"), + status: .required, + source: .system, + condition: nil + ), + GraphDependencyReference(dependencyDynamicXCFramework), + GraphDependencyReference(dependencyStaticXCFrameworkA), + ]) + + // When + let embeddable = subject.embeddableFrameworks(path: project.path, name: target.name) + + // Then + XCTAssertBetterEqual(embeddable, [ + GraphDependencyReference(dependencyDynamicXCFramework), + ]) + } + func test_linkableAndEmbeddableDependencies_when_appDependensOnPrecompiledStaticBinaryWithPrecompiledDynamicBinaryDependency( ) throws { // App ---(depends on)---> Precompiled static binary (A) ----> Precompiled dynamic binary (B) @@ -2064,8 +2055,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencyPrecompiledStaticBinaryA = GraphDependency.testFramework( path: "/test/StaticFrameworkA.framework", @@ -2073,8 +2063,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .static, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ @@ -2084,7 +2073,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2122,8 +2110,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .static, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencyPrecompiledDynamicBinaryA = GraphDependency.testFramework( path: "/test/DynamicFrameworkA.framework", @@ -2131,8 +2118,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ @@ -2142,7 +2128,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2153,7 +2138,6 @@ final class GraphTraverserTests: TuistUnitTestCase { // Then XCTAssertEqual(got, [ GraphDependencyReference(dependencyPrecompiledDynamicBinaryA), - GraphDependencyReference(dependencyPrecompiledStaticBinaryB), ]) // When @@ -2200,12 +2184,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ project.path: project, ], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2228,7 +2206,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let target = Target.test(name: "Main") let dependency = Target.test(name: "Dependency", product: .staticLibrary) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependency]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2237,7 +2215,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target, dependency.name: dependency]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2254,7 +2231,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let target = Target.test(name: "Main") let dependency = Target.test(name: "Dependency", product: .framework) let staticDependency = Target.test(name: "StaticDependency", product: .staticLibrary) - let project = Project.test(targets: [target]) + let project = Project.test(targets: [target, dependency, staticDependency]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2267,11 +2244,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - target.name: target, - dependency.name: dependency, - staticDependency.name: staticDependency, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2308,7 +2280,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dependencies: [] ) let app = Target.test(name: "App", product: .app) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, staticFramework, dynamicFramework]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2324,11 +2296,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticFramework.name: staticFramework, - dynamicFramework.name: dynamicFramework, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2373,7 +2340,10 @@ final class GraphTraverserTests: TuistUnitTestCase { dependencies: [] ) let app = Target.test(name: "App", product: .app) - let project = Project.test(path: "/path/a") + let project = Project.test( + path: "/path/a", + targets: [app, dynamicFramework1, dynamicFramework2, staticFramework1, staticFramework2] + ) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2397,13 +2367,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - dynamicFramework1.name: dynamicFramework1, - dynamicFramework2.name: dynamicFramework2, - staticFramework1.name: staticFramework1, - staticFramework2.name: staticFramework2, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2465,7 +2428,10 @@ final class GraphTraverserTests: TuistUnitTestCase { let app = Target.test(name: "App", product: .app) - let project = Project.test(path: "/path/a") + let project = Project.test( + path: "/path/a", + targets: [app, dynamicFramework1, dynamicFramework2, dynamicFramework3, staticFramework1, staticFramework2] + ) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2493,14 +2459,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - dynamicFramework1.name: dynamicFramework1, - dynamicFramework2.name: dynamicFramework2, - staticFramework1.name: staticFramework1, - staticFramework2.name: staticFramework2, - dynamicFramework3.name: dynamicFramework3, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2531,7 +2489,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dependencies: [] ) let app = Target.test(name: "App", product: .app) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, staticFrameworkA, staticFrameworkB]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2552,11 +2510,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkA.name: staticFrameworkA, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2583,7 +2536,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dependencies: [] ) let app = Target.test(name: "App", product: .app) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, staticFramework, dynamicFramework]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2604,11 +2557,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticFramework.name: staticFramework, - dynamicFramework.name: dynamicFramework, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2638,7 +2586,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dependencies: [.sdk(name: "some.framework", status: .optional)] ) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, staticFramework]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2655,10 +2603,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2683,7 +2627,7 @@ final class GraphTraverserTests: TuistUnitTestCase { ] ) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [staticFramework]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2704,7 +2648,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [staticFramework.name: staticFramework]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2735,7 +2678,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dependencies: [.sdk(name: "ThingTwo.framework", status: .optional)] ) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [staticFrameworkA, staticFrameworkB]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -2757,10 +2700,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2787,18 +2726,11 @@ final class GraphTraverserTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .dynamic, architectures: [.arm64], - isCarthage: false, status: .required ) let project = Project.test(path: "/path/project", targets: [app, staticFramework]) let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ .target(name: app.name, path: project.path): Set([ .target(name: staticFramework.name, path: project.path), @@ -2819,7 +2751,6 @@ final class GraphTraverserTests: TuistUnitTestCase { .framework( path: "/path/to/frameworks/precompiled.framework", binaryPath: "/path/to/frameworks/precompiled.framework/precompiled", - isCarthage: false, dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, @@ -2842,18 +2773,11 @@ final class GraphTraverserTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .dynamic, architectures: [.arm64], - isCarthage: false, status: .required ) let project = Project.test(path: "/path/project", targets: [app, framework]) let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - framework.name: framework, - ], - ], dependencies: [ .target(name: app.name, path: project.path): Set([ .target(name: framework.name, path: project.path), @@ -2887,19 +2811,11 @@ final class GraphTraverserTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .dynamic, architectures: [.arm64], - isCarthage: false, status: .required ) let project = Project.test(path: "/path/project", targets: [app, staticFramework, framework]) let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - framework.name: framework, - ], - ], dependencies: [ .target(name: app.name, path: project.path): Set([ .target(name: staticFramework.name, path: project.path), @@ -2945,11 +2861,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - watchExtension.name: watchExtension, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -2984,11 +2895,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - watchExtension.name: watchExtension, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3012,7 +2918,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let app = Target.test(name: "App", product: .app) let tests = Target.test(name: "AppTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, staticFramework, tests]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -3027,11 +2933,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticFramework.name: staticFramework, - tests.name: tests, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3054,7 +2955,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let appClip = Target.test(name: "AppClip", product: .appClip) let tests = Target.test(name: "AppClipTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [appClip, staticFramework, tests]) let dependencies: [GraphDependency: Set] = [ .target(name: appClip.name, path: project.path): [ .target(name: staticFramework.name, path: project.path), @@ -3067,11 +2968,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - appClip.name: appClip, - staticFramework.name: staticFramework, - tests.name: tests, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3094,7 +2990,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let app = Target.test(name: "App", product: .app) let tests = Target.test(name: "AppTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, framework, tests]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -3107,11 +3003,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - framework.name: framework, - tests.name: tests, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3134,7 +3025,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let app = Target.test(name: "App", product: .app) let tests = Target.test(name: "AppTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, framework, tests]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -3144,11 +3035,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - framework.name: framework, - tests.name: tests, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3163,7 +3049,7 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_linkableDependencies_when_appClipSDKNode() throws { // Given let target = Target.test(name: "AppClip", product: .appClip) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [target]) // Given: Value Graph let sdkDependency: GraphDependency = .sdk( @@ -3183,7 +3069,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies, dependencyConditions: dependencyConditions ) @@ -3212,7 +3097,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let appClipTarget = Target.test(name: "AppClip", product: .appClip) let frameworkTarget = Target.test(name: "MyFramework", product: .framework) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [appClipTarget, frameworkTarget]) // Given: Value Graph let dependencies: [GraphDependency: Set] = [ @@ -3224,12 +3109,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ project.path: project, ], - targets: [ - project.path: [ - appClipTarget.name: appClipTarget, - frameworkTarget.name: frameworkTarget, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3259,12 +3138,10 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: [ .target(name: target.name, path: project.path): Set(arrayLiteral: frameworkDependency), ] @@ -3292,8 +3169,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .static, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let dependencies: [GraphDependency: Set] = [ .target(name: target.name, path: project.path): [ @@ -3302,7 +3178,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3331,12 +3206,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ project.path: project, ], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: staticFramework.name, path: project.path), @@ -3400,12 +3269,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ project.path: project, ], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: staticFramework.name, path: project.path), @@ -3470,12 +3333,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ project.path: project, ], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: staticFramework.name, path: project.path), @@ -3559,12 +3416,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - unitTests.name: unitTests, - staticFramework.name: staticFramework, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3606,13 +3457,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - unitTests.name: unitTests, - dynamicFramework.name: dynamicFramework, - staticFramework.name: staticFramework, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3658,12 +3502,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - hostApp.name: hostApp, - unitTests.name: unitTests, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3685,7 +3523,7 @@ final class GraphTraverserTests: TuistUnitTestCase { binaryPath: "/test/PrecompiledStaticFramework.framework/PrecompiledStaticFramework", linking: .static ) - let project = Project.test(targets: [hostApp, unitTests]) + let project = Project.test(targets: [hostApp, unitTests, staticFramework]) let dependencies: [GraphDependency: Set] = [ .target(name: hostApp.name, path: project.path): [ precompiledStaticFramework, @@ -3700,13 +3538,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - hostApp.name: hostApp, - unitTests.name: unitTests, - staticFramework.name: staticFramework, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3733,7 +3564,7 @@ final class GraphTraverserTests: TuistUnitTestCase { binaryPath: "/test/PrecompiledStaticFramework.framework/PrecompiledStaticFramework", linking: .static ) - let project = Project.test(targets: [hostApp, unitTests]) + let project = Project.test(targets: [hostApp, unitTests, staticFramework]) let dependencies: [GraphDependency: Set] = [ .target(name: hostApp.name, path: project.path): [ precompiledStaticFramework, @@ -3748,13 +3579,6 @@ final class GraphTraverserTests: TuistUnitTestCase { ] let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - hostApp.name: hostApp, - unitTests.name: unitTests, - staticFramework.name: staticFramework, - ], - ], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -3780,7 +3604,6 @@ final class GraphTraverserTests: TuistUnitTestCase { // 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([ .testLibrary(path: "/test/test.a", swiftModuleMap: "/test/modules/test.swiftmodulemap"), @@ -3802,7 +3625,7 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_runPathSearchPaths() throws { // Given let unitTests = Target.test(name: "AppUnitTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [unitTests]) // Given: Value Graph let precompiledDependency = GraphDependency.testFramework( @@ -3811,8 +3634,7 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let precompiledBDependency = GraphDependency.testFramework( path: "/test/testb.famework", @@ -3820,12 +3642,10 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [unitTests.name: unitTests]], dependencies: [ .target(name: unitTests.name, path: project.path): Set([precompiledDependency, precompiledBDependency]), precompiledDependency: Set(), @@ -3848,7 +3668,7 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let app = Target.test(name: "App", product: .app) let unitTests = Target.test(name: "AppUnitTests", product: .unitTests) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [unitTests, app]) // Given: Value Graph let precompiledDependency = GraphDependency.testFramework( @@ -3857,15 +3677,10 @@ final class GraphTraverserTests: TuistUnitTestCase { dsymPath: nil, bcsymbolmapPaths: [], linking: .dynamic, - architectures: [.arm64], - isCarthage: false + architectures: [.arm64] ) let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - unitTests.name: unitTests, - app.name: app, - ]], dependencies: [ .target( name: unitTests.name, @@ -3888,15 +3703,11 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let app = Target.test(name: "App", platform: .iOS, product: .app) let watchApp = Target.test(name: "WatchApp", platform: .watchOS, product: .watch2App) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [app, watchApp]) // Given: Value Graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - app.name: app, - watchApp.name: watchApp, - ]], dependencies: [ .target(name: app.name, path: project.path): Set([.target(name: watchApp.name, path: project.path)]), .target(name: watchApp.name, path: project.path): Set([]), @@ -3915,15 +3726,11 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given let watchApp = Target.test(name: "WatchApp", platform: .watchOS, product: .watch2App) let watchAppExtension = Target.test(name: "WatchAppExtension", platform: .watchOS, product: .watch2Extension) - let project = Project.test(path: "/path/a") + let project = Project.test(path: "/path/a", targets: [watchAppExtension, watchApp]) // Given: Value Graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - watchAppExtension.name: watchAppExtension, - watchApp.name: watchApp, - ]], dependencies: [ .target( name: watchApp.name, @@ -3946,16 +3753,11 @@ final class GraphTraverserTests: TuistUnitTestCase { let macosApp = Target.test(name: "MacOS", platform: .macOS, product: .app) let tvosApp = Target.test(name: "tvOS", platform: .tvOS, product: .app) let framework = Target.test(name: "Framework", platform: .iOS, product: .framework) - let project = Project.test(path: "/project") + let project = Project.test(path: "/project", targets: [macosApp, tvosApp, framework]) // Given: Value Graph let graph = Graph.test( projects: [project.path: project], - targets: [project.path: [ - macosApp.name: macosApp, - tvosApp.name: tvosApp, - framework.name: framework, - ]], dependencies: [ .target(name: macosApp.name, path: project.path): Set(), .target(name: tvosApp.name, path: project.path): Set(), @@ -3976,19 +3778,16 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_allTargets_returns_all_the_targets() { // Given let firstPath = try! AbsolutePath(validating: "/first") - let firstProject = Project.test(path: firstPath) let secondPath = try! AbsolutePath(validating: "/second") - let secondProject = Project.test(path: secondPath) let firstTarget = Target.test(name: "first") + let firstProject = Project.test(path: firstPath, targets: [firstTarget]) let secondTarget = Target.test(name: "second") + let secondProject = Project.test(path: secondPath, targets: [secondTarget]) + let graph = Graph.test( projects: [ firstPath: firstProject, secondPath: secondProject, - ], - targets: [ - firstPath: [firstTarget.name: firstTarget], - secondPath: [secondTarget.name: secondTarget], ] ) let graphTraverser = GraphTraverser(graph: graph) @@ -4041,10 +3840,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - extensionKitExtension.name: extensionKitExtension, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4091,7 +3886,6 @@ final class GraphTraverserTests: TuistUnitTestCase { bcsymbolmapPaths: [], linking: .static, architectures: [.arm64], - isCarthage: false, status: .required ) let directFrameworkTarget = GraphDependency.target(name: staticFramework.name, path: project.path) @@ -4156,9 +3950,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - staticLibrary.name: staticLibrary, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4207,8 +3998,8 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_copyProductDependencies_when_targetHasDirectStaticDependencies() throws { // Given let staticLibrary = Target.test(name: "StaticLibrary", destinations: [.iPhone], product: .staticLibrary) - let project = Project.test(targets: [staticLibrary]) let aDependency = Target.test(name: "StaticDependency", destinations: [.iPhone], product: .staticLibrary) + let project = Project.test(targets: [staticLibrary, aDependency]) let dependencies: [GraphDependency: Set] = [ .target(name: staticLibrary.name, path: project.path): Set([.target(name: aDependency.name, path: project.path)]), @@ -4218,10 +4009,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - staticLibrary.name: staticLibrary, - aDependency.name: aDependency, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4238,8 +4025,8 @@ final class GraphTraverserTests: TuistUnitTestCase { func test_copyProductDependencies_when_targetHasBundleDependencies() throws { // Given let app = Target.test(name: "App", destinations: [.iPhone], product: .app) - let project = Project.test(targets: [app]) let bundle = Target.test(name: "Bundle", destinations: [.iPhone], product: .bundle) + let project = Project.test(targets: [app, bundle]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set([.target(name: bundle.name, path: project.path)]), @@ -4249,10 +4036,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - bundle.name: bundle, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4284,12 +4067,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ appkGraphDependency: [ staticFrameworkGraphDependency, @@ -4299,7 +4076,7 @@ final class GraphTraverserTests: TuistUnitTestCase { ], ], dependencyConditions: [ - GraphEdge(from: appkGraphDependency, to: staticFrameworkGraphDependency): try .test([.ios]), + GraphEdge(from: appkGraphDependency, to: staticFrameworkGraphDependency): try XCTUnwrap(.test([.ios])), ] ) let subject = GraphTraverser(graph: graph) @@ -4331,12 +4108,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ appkGraphDependency: [ staticFrameworkGraphDependency, @@ -4346,8 +4117,8 @@ final class GraphTraverserTests: TuistUnitTestCase { ], ], dependencyConditions: [ - GraphEdge(from: appkGraphDependency, to: staticFrameworkGraphDependency): try .test([.macos]), - GraphEdge(from: staticFrameworkGraphDependency, to: sdkGraphDependency): try .test([.ios]), + GraphEdge(from: appkGraphDependency, to: staticFrameworkGraphDependency): try XCTUnwrap(.test([.macos])), + GraphEdge(from: staticFrameworkGraphDependency, to: sdkGraphDependency): try XCTUnwrap(.test([.ios])), ] ) let subject = GraphTraverser(graph: graph) @@ -4377,12 +4148,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFramework.name: staticFramework, - ], - ], dependencies: [ appkGraphDependency: [ staticFrameworkGraphDependency, @@ -4392,7 +4157,7 @@ final class GraphTraverserTests: TuistUnitTestCase { ], ], dependencyConditions: [ - GraphEdge(from: staticFrameworkGraphDependency, to: sdkGraphDependency): try .test([.ios]), + GraphEdge(from: staticFrameworkGraphDependency, to: sdkGraphDependency): try XCTUnwrap(.test([.ios])), ] ) let subject = GraphTraverser(graph: graph) @@ -4436,14 +4201,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - ], - ], dependencies: [ appkGraphDependency: [ staticFrameworkAGraphDependency, @@ -4457,7 +4214,7 @@ final class GraphTraverserTests: TuistUnitTestCase { ], ], dependencyConditions: [ - GraphEdge(from: staticFrameworkAGraphDependency, to: sdkGraphDependency): try .test([.ios]), + GraphEdge(from: staticFrameworkAGraphDependency, to: sdkGraphDependency): try XCTUnwrap(.test([.ios])), ] ) let subject = GraphTraverser(graph: graph) @@ -4514,14 +4271,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - ], - ], dependencies: [ appkGraphDependency: [ staticFrameworkAGraphDependency, @@ -4535,7 +4284,7 @@ final class GraphTraverserTests: TuistUnitTestCase { ], ], dependencyConditions: [ - GraphEdge(from: appkGraphDependency, to: staticFrameworkBGraphDependency): try .test([.macos]), + GraphEdge(from: appkGraphDependency, to: staticFrameworkBGraphDependency): try XCTUnwrap(.test([.macos])), ] ) let subject = GraphTraverser(graph: graph) @@ -4560,10 +4309,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - framework.name: framework, - macro.name: macro, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4589,9 +4334,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - framework.name: framework, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4637,14 +4379,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - staticFrameworkMacro.name: staticFrameworkMacro, - dynamicFrameworkMacro.name: dynamicFrameworkMacro, - staticLibraryMacro.name: staticLibraryMacro, - dynamicLibraryMacro.name: dynamicLibraryMacro, - macro.name: macro, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4688,10 +4422,6 @@ final class GraphTraverserTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - app.name: app, - macroFramework.name: macroFramework, - ]], dependencies: dependencies ) let subject = GraphTraverser(graph: graph) @@ -4715,7 +4445,7 @@ final class GraphTraverserTests: TuistUnitTestCase { ) let transitiveMacro = Target.test(name: "TransitiveMacro", destinations: [.mac], product: .macro) - let project = Project.test(targets: [app, directMacroFramework, directMacro]) + let project = Project.test(targets: [app, directMacroFramework, directMacro, transitiveMacroLibrary, transitiveMacro]) 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([ @@ -4732,25 +4462,23 @@ final class GraphTraverserTests: TuistUnitTestCase { 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) + let gotDirectMacroFramework = subject.allSwiftMacroTargets(path: project.path, name: directMacroFramework.name) // Then XCTAssertEqual(got.sorted(), [ GraphTarget(path: project.path, target: directMacroFramework, project: project), GraphTarget(path: project.path, target: transitiveMacroLibrary, project: project), ]) + XCTAssertEqual(gotDirectMacroFramework.sorted(), [ + GraphTarget(path: project.path, target: directMacroFramework, project: project), + GraphTarget(path: project.path, target: transitiveMacroLibrary, project: project), + ]) } func test_directTargetDependenciesWithConditions() throws { @@ -4764,16 +4492,12 @@ final class GraphTraverserTests: TuistUnitTestCase { appDependency: Set([frameworkDependency]), frameworkDependency: Set([]), ] - let platformCondition = try PlatformCondition.test([.ios]) + let platformCondition = try XCTUnwrap(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, @@ -4816,19 +4540,12 @@ final class GraphTraverserTests: TuistUnitTestCase { frameworkBDependency: Set([frameworkCDependency]), frameworkCDependency: Set([frameworkDDependency]), ] - let platformCondition = try PlatformCondition.test([.ios]) + let platformCondition = try XCTUnwrap(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, @@ -4878,13 +4595,6 @@ final class GraphTraverserTests: TuistUnitTestCase { 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]), @@ -4918,12 +4628,6 @@ final class GraphTraverserTests: TuistUnitTestCase { 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]), @@ -4957,12 +4661,6 @@ final class GraphTraverserTests: TuistUnitTestCase { 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]), @@ -4987,28 +4685,34 @@ final class GraphTraverserTests: TuistUnitTestCase { destinations: [.iPad, .iPhone, .appleWatch, .appleTv, .mac], product: .framework ) + let externalPackageTargetB = Target.test( + name: "PackageB", + 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 externalProject = Project.test( + path: packagesDirectory, + targets: [externalPackage, externalPackageTargetB], + isExternal: true + ) let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) let externalPackageDependency = GraphDependency.target(name: externalPackage.name, path: externalProject.path) + let externalPackageBDependency = GraphDependency.target(name: externalPackageTargetB.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]), + externalPackageDependency: Set([externalPackageBDependency]), + ], + dependencyConditions: [ + GraphEdge(from: externalPackageDependency, to: externalPackageBDependency): .when([.ios, .macos])!, ] ) @@ -5021,6 +4725,10 @@ final class GraphTraverserTests: TuistUnitTestCase { got[GraphTarget(path: externalProject.path, target: externalPackage, project: externalProject)], Set([.iOS]) ) + XCTAssertEqual( + got[GraphTarget(path: externalProject.path, target: externalPackageTargetB, project: externalProject)], + Set([.iOS]) + ) } func test_test_externalTargetSupportedPlatforms_when_external_transitive_dependency_without_platform_filter() async throws { @@ -5059,15 +4767,6 @@ final class GraphTraverserTests: TuistUnitTestCase { 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]), @@ -5124,15 +4823,6 @@ final class GraphTraverserTests: TuistUnitTestCase { 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]), @@ -5184,14 +4874,6 @@ final class GraphTraverserTests: TuistUnitTestCase { directory: project, packagesDirectory: externalProject, ], - targets: [ - project.path: [ - appTarget.name: appTarget, - ], - externalProject.path: [ - externalFramework.name: externalFramework, - ], - ], dependencies: [ appTargetDependency: Set([externalFrameworkDependency]), ] @@ -5224,11 +4906,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ directory: project, ], - targets: [ - project.path: [ - appTarget.name: appTarget, - ], - ], dependencies: [ appTargetDependency: Set([precompiledMacroXCFramework]), precompiledMacroXCFramework: Set([precompiledMacroExecutable]), @@ -5261,7 +4938,16 @@ final class GraphTraverserTests: TuistUnitTestCase { ) let transitiveMacroMacroTarget = Target.test(name: "TransitiveMacro", destinations: [.appleWatch], product: .macro) - let project = Project.test(path: directory, targets: [appTarget]) + let project = Project.test( + path: directory, + targets: [ + appTarget, + directMacroStaticFrameworkTarget, + directMacroMacroTarget, + transitiveMacroMacroTarget, + transitiveMacroStaticFrameworkTarget, + ] + ) let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) let directMacroStaticFrameworkTargetDependency = GraphDependency.target( name: directMacroStaticFrameworkTarget.name, @@ -5281,15 +4967,6 @@ final class GraphTraverserTests: TuistUnitTestCase { 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([ @@ -5322,7 +4999,7 @@ final class GraphTraverserTests: TuistUnitTestCase { let precompiledMacroPath: AbsolutePath = .root.appending(component: "macro.macro") let directMacroMacroPrecompiledExecutable = GraphDependency.macro(path: precompiledMacroPath) - let project = Project.test(path: directory, targets: [appTarget]) + let project = Project.test(path: directory, targets: [appTarget, directMacroStaticFrameworkTarget]) let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) let directMacroStaticFrameworkTargetDependency = GraphDependency.target( name: directMacroStaticFrameworkTarget.name, @@ -5333,12 +5010,6 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [ directory: project, ], - targets: [ - project.path: [ - appTarget.name: appTarget, - directMacroStaticFrameworkTarget.name: directMacroStaticFrameworkTarget, - ], - ], dependencies: [ appTargetDependency: Set([directMacroStaticFrameworkTargetDependency]), directMacroStaticFrameworkTargetDependency: Set([ diff --git a/Tests/TuistCoreTests/Graph/Mappers/GraphMapperTests.swift b/Tests/TuistCoreTests/Graph/Mappers/GraphMapperTests.swift index 96f6a241aea..0b60da406b2 100644 --- a/Tests/TuistCoreTests/Graph/Mappers/GraphMapperTests.swift +++ b/Tests/TuistCoreTests/Graph/Mappers/GraphMapperTests.swift @@ -1,8 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistCoreTesting -import TuistGraph +import XcodeGraph import XCTest @testable import TuistSupportTesting @@ -14,11 +13,11 @@ final class AnyGraphMapperTests: TuistUnitTestCase { let output = Graph.test(name: "output") let subject = AnyGraphMapper(mapper: { graph in XCTAssertEqual(graph.name, input.name) - return (output, []) + return (output, [], MapperEnvironment()) }) // When - let (got, _) = try subject.map(graph: input) + let (got, _, _) = try subject.map(graph: input, environment: MapperEnvironment()) // Then XCTAssertEqual(got.name, output.name) @@ -32,17 +31,17 @@ final class SequentialGraphMapperTests: TuistUnitTestCase { let input = Graph.test(name: "0") let first = AnyGraphMapper(mapper: { graph in XCTAssertEqual(graph.name, "0") - return (Graph.test(name: "1"), [firstSideEffect]) + return (Graph.test(name: "1"), [firstSideEffect], MapperEnvironment()) }) let secondSideEffect = SideEffectDescriptor.file(.init(path: "/second")) let second = AnyGraphMapper(mapper: { graph in XCTAssertEqual(graph.name, "1") - return (Graph.test(name: "2"), [secondSideEffect]) + return (Graph.test(name: "2"), [secondSideEffect], MapperEnvironment()) }) let subject = SequentialGraphMapper([first, second]) // When - let (got, sideEffects) = try await subject.map(graph: input) + let (got, sideEffects, _) = try await subject.map(graph: input, environment: MapperEnvironment()) // Then XCTAssertEqual(got.name, "2") diff --git a/Tests/TuistCoreTests/Graph/Mappers/ProjectMapperTests.swift b/Tests/TuistCoreTests/Graph/Mappers/ProjectMapperTests.swift index cd86ec2a13f..89e4be55f77 100644 --- a/Tests/TuistCoreTests/Graph/Mappers/ProjectMapperTests.swift +++ b/Tests/TuistCoreTests/Graph/Mappers/ProjectMapperTests.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph +import XcodeGraph import XCTest @testable import TuistSupportTesting @@ -24,7 +24,7 @@ final class TargetProjectMapperTests: XCTestCase { let (updatedProject, sideEffects) = try subject.map(project: project) // Then - XCTAssertEqual(updatedProject.targets.map(\.name), [ + XCTAssertEqual(updatedProject.targets.values.map(\.name).sorted(), [ "Updated_A", "Updated_B", ]) @@ -47,7 +47,7 @@ final class TargetProjectMapperTests: XCTestCase { let (_, sideEffects) = try subject.map(project: project) // Then - XCTAssertEqual(sideEffects, [ + XCTAssertEqual(sideEffects.sorted(by: { $0.description < $1.description }), [ .file(.init(path: try AbsolutePath(validating: "/Targets/A.swift"))), .file(.init(path: try AbsolutePath(validating: "/Targets/B.swift"))), ]) diff --git a/Tests/TuistCoreTests/Graph/Mappers/WorkspaceMapperTests.swift b/Tests/TuistCoreTests/Graph/Mappers/WorkspaceMapperTests.swift index 555c0061364..dbf6d0f7761 100644 --- a/Tests/TuistCoreTests/Graph/Mappers/WorkspaceMapperTests.swift +++ b/Tests/TuistCoreTests/Graph/Mappers/WorkspaceMapperTests.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph +import XcodeGraph import XCTest @testable import TuistSupportTesting diff --git a/Tests/TuistCoreTests/MetadataProviders/FrameworkMetadataProviderTests.swift b/Tests/TuistCoreTests/MetadataProviders/FrameworkMetadataProviderTests.swift index e63da4db0bc..d64edcc65b4 100644 --- a/Tests/TuistCoreTests/MetadataProviders/FrameworkMetadataProviderTests.swift +++ b/Tests/TuistCoreTests/MetadataProviders/FrameworkMetadataProviderTests.swift @@ -1,5 +1,5 @@ -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistSupportTesting @@ -34,7 +34,6 @@ final class FrameworkMetadataProviderTests: XCTestCase { bcsymbolmapPaths: [], linking: .dynamic, architectures: [.x8664, .arm64], - isCarthage: false, status: .required )) } diff --git a/Tests/TuistCoreTests/MetadataProviders/LibraryMetadataProviderTests.swift b/Tests/TuistCoreTests/MetadataProviders/LibraryMetadataProviderTests.swift index 0e258a14c63..188e91160b7 100644 --- a/Tests/TuistCoreTests/MetadataProviders/LibraryMetadataProviderTests.swift +++ b/Tests/TuistCoreTests/MetadataProviders/LibraryMetadataProviderTests.swift @@ -1,5 +1,5 @@ -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistSupportTesting diff --git a/Tests/TuistCoreTests/MetadataProviders/PrecompiledMetadataProviderTests.swift b/Tests/TuistCoreTests/MetadataProviders/PrecompiledMetadataProviderTests.swift index 8f7ab7580ad..94139c98400 100644 --- a/Tests/TuistCoreTests/MetadataProviders/PrecompiledMetadataProviderTests.swift +++ b/Tests/TuistCoreTests/MetadataProviders/PrecompiledMetadataProviderTests.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore @@ -35,29 +35,6 @@ final class PrecompiledMetadataProviderTests: TuistUnitTestCase { XCTAssertEqual(uuids, Set()) } - func test_metadata_carthage() throws { - // Given - let binaryPath = fixturePath(path: try RelativePath(validating: "Carthage/RxBlocking.framework/RxBlocking")) - - // When - let architectures = try subject.architectures(binaryPath: binaryPath) - let linking = try subject.linking(binaryPath: binaryPath) - let uuids = try subject.uuids(binaryPath: binaryPath) - - // Then - XCTAssertEqual(architectures, [.i386, .x8664, .armv7, .arm64]) - XCTAssertEqual(linking, BinaryLinking.dynamic) - XCTAssertEqual( - uuids, - Set([ - UUID(uuidString: "2510FE01-4D40-3956-BB71-857D3B2D9E73"), - UUID(uuidString: "1C061BD7-371A-3039-8510-15CDF61531F6"), - UUID(uuidString: "1208EC2E-0B7C-3B13-B5E1-6341E6AE6859"), - UUID(uuidString: "773847A9-0D05-35AF-9865-94A9A670080B"), - ]) - ) - } - func test_metadata_framework() throws { // Given let binaryPath = fixturePath(path: try RelativePath(validating: "xpm.framework/xpm")) diff --git a/Tests/TuistCoreTests/MetadataProviders/SystemFrameworkMetadataProviderTests.swift b/Tests/TuistCoreTests/MetadataProviders/SystemFrameworkMetadataProviderTests.swift index 924e6e93d51..a280d7a2186 100644 --- a/Tests/TuistCoreTests/MetadataProviders/SystemFrameworkMetadataProviderTests.swift +++ b/Tests/TuistCoreTests/MetadataProviders/SystemFrameworkMetadataProviderTests.swift @@ -1,5 +1,5 @@ -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistSupportTesting @@ -49,6 +49,22 @@ final class SystemFrameworkMetadataProviderTests: XCTestCase { )) } + func test_loadMetadata_swiftLibrary() throws { + // Given + let sdkName = "libswiftObservation.tbd" + + // When + let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system) + + // Then + XCTAssertEqual(metadata, SystemFrameworkMetadata( + name: sdkName, + path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/libswiftObservation.tbd", + status: .required, + source: .system + )) + } + func test_loadMetadata_unsupportedType() throws { // Given let sdkName = "UIKit.xcframework" diff --git a/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift b/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift index 80343dce1dd..5bf577b2db1 100644 --- a/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift +++ b/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift @@ -1,8 +1,8 @@ -import TSCBasic +import Path import XCTest @testable import TuistCore -@testable import TuistGraph @testable import TuistSupportTesting +@testable import XcodeGraph final class XCFrameworkMetadataProviderTests: TuistTestCase { var subject: XCFrameworkMetadataProvider! @@ -28,12 +28,14 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { identifier: "ios-x86_64-simulator", path: try RelativePath(validating: "MyFramework.framework"), mergeable: false, + platform: .iOS, architectures: [.x8664] ), .init( identifier: "ios-arm64", path: try RelativePath(validating: "MyFramework.framework"), mergeable: false, + platform: .iOS, architectures: [.arm64] ), ]) @@ -68,6 +70,23 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { infoPlist.libraries.forEach { XCTAssertEqual($0.binaryName, "MyFramework") } } + func test_binaryPath_when_dylibIsPresent() throws { + // Given + let xcframeworkPath = fixturePath(path: try RelativePath(validating: "DylibXCFramework.xcframework")) + let infoPlist = try subject.infoPlist(xcframeworkPath: xcframeworkPath) + + // When + let binaryPath = try subject.binaryPath(xcframeworkPath: xcframeworkPath, libraries: infoPlist.libraries) + + // Then + XCTAssertEqual( + binaryPath, + xcframeworkPath.appending(try RelativePath(validating: "macos-arm64/libDylibXCFramework.dylib")) + ) + + infoPlist.libraries.forEach { XCTAssertEqual($0.binaryName, "libDylibXCFramework") } + } + func test_libraries_when_staticLibraryIsPresent() throws { // Given let frameworkPath = fixturePath(path: try RelativePath(validating: "MyStaticLibrary.xcframework")) @@ -79,12 +98,14 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { identifier: "ios-x86_64-simulator", path: try RelativePath(validating: "libMyStaticLibrary.a"), mergeable: false, + platform: .iOS, architectures: [.x8664] ), .init( identifier: "ios-arm64", path: try RelativePath(validating: "libMyStaticLibrary.a"), mergeable: false, + platform: .iOS, architectures: [.arm64] ), ]) @@ -117,12 +138,14 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { identifier: "ios-x86_64-simulator", path: try RelativePath(validating: "MyFramework.framework"), mergeable: false, + platform: .iOS, architectures: [.x8664] ), .init( identifier: "ios-arm64", path: try RelativePath(validating: "MyFramework.framework"), mergeable: false, + platform: .iOS, architectures: [.arm64] ), ]) @@ -153,12 +176,14 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { identifier: "ios-x86_64-simulator", path: try RelativePath(validating: "MyMergeableFramework.framework"), mergeable: true, + platform: .iOS, architectures: [.x8664] ), .init( identifier: "ios-arm64", path: try RelativePath(validating: "MyMergeableFramework.framework"), mergeable: true, + platform: .iOS, architectures: [.arm64] ), ]) @@ -189,12 +214,14 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { identifier: "ios-x86_64-simulator", path: try RelativePath(validating: "libMyStaticLibrary.a"), mergeable: false, + platform: .iOS, architectures: [.x8664] ), .init( identifier: "ios-arm64", path: try RelativePath(validating: "libMyStaticLibrary.a"), mergeable: false, + platform: .iOS, architectures: [.arm64] ), ]) @@ -224,12 +251,14 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { identifier: "ios-x86_64-simulator", // Not present on disk path: try RelativePath(validating: "MyFrameworkMissingArch.framework"), mergeable: false, + platform: .iOS, architectures: [.x8664] ), .init( identifier: "ios-arm64", path: try RelativePath(validating: "MyFrameworkMissingArch.framework"), mergeable: false, + platform: .iOS, architectures: [.arm64] ), ]) diff --git a/Tests/TuistCoreTests/NodeLoaders/FrameworkLoaderTests.swift b/Tests/TuistCoreTests/NodeLoaders/FrameworkLoaderTests.swift index b6c4438dab0..57730f95324 100644 --- a/Tests/TuistCoreTests/NodeLoaders/FrameworkLoaderTests.swift +++ b/Tests/TuistCoreTests/NodeLoaders/FrameworkLoaderTests.swift @@ -1,6 +1,6 @@ -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistCoreTesting @@ -80,7 +80,6 @@ final class FrameworkLoaderTests: TuistUnitTestCase { bcsymbolmapPaths: bcsymbolmapPaths, linking: linking, architectures: architectures, - isCarthage: false, status: .required ) } @@ -98,7 +97,6 @@ final class FrameworkLoaderTests: TuistUnitTestCase { bcsymbolmapPaths: bcsymbolmapPaths, linking: linking, architectures: architectures, - isCarthage: false, status: .required ) ) diff --git a/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift b/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift index 5bfa725f15d..2534f9d7c6e 100644 --- a/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift +++ b/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift @@ -1,6 +1,6 @@ -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore diff --git a/Tests/TuistCoreTests/Simulator/SimulatorControllerTests.swift b/Tests/TuistCoreTests/Simulator/SimulatorControllerTests.swift index 72bbd8dfdbb..aead8af0b52 100644 --- a/Tests/TuistCoreTests/Simulator/SimulatorControllerTests.swift +++ b/Tests/TuistCoreTests/Simulator/SimulatorControllerTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistCore @@ -11,7 +11,7 @@ final class SimulatorControllerTests: TuistUnitTestCase { override func setUp() { super.setUp() - subject = SimulatorController() + subject = SimulatorController(system: system) } override func tearDown() { diff --git a/Tests/TuistCoreTests/Utils/DefaultConfigurationFetcherTests.swift b/Tests/TuistCoreTests/Utils/DefaultConfigurationFetcherTests.swift new file mode 100644 index 00000000000..2a3be0e9b62 --- /dev/null +++ b/Tests/TuistCoreTests/Utils/DefaultConfigurationFetcherTests.swift @@ -0,0 +1,156 @@ +import Foundation +import Path +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistCore + +final class DefaultConfigurationFetcherTests: TuistUnitTestCase { + private var subject: DefaultConfigurationFetcher! + + override func setUp() { + super.setUp() + subject = DefaultConfigurationFetcher() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_fetch_throws_an_error_when_debug_configuration_not_found() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project.test(settings: Settings(configurations: [:])), + ]) + + // When/Then + XCTAssertThrowsSpecific( + try subject.fetch(configuration: nil, config: .test(), graph: graph), + DefaultConfigurationFetcherError.debugBuildConfigurationNotFound + ) + } + + func test_fetch_returns_the_first_debug_configuration_found() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project + .test(settings: Settings(configurations: [BuildConfiguration(name: "Development", variant: .debug): .test()])), + ]) + + // When + let got = try subject.fetch(configuration: nil, config: .test(), graph: graph) + + // Then + XCTAssertEqual(got, "Development") + } + + func test_fetch_returns_the_configuration_if_the_configuration_exists_in_the_project() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project + .test(settings: Settings(configurations: [BuildConfiguration(name: "Dev", variant: .debug): .test()])), + ]) + + // When + let got = try subject.fetch(configuration: "Dev", config: .test(), graph: graph) + + // Then + XCTAssertEqual(got, "Dev") + } + + func test_fetch_throws_an_error_when_the_configuration_passed_points_to_a_non_existing_configuration() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project + .test(settings: Settings(configurations: [BuildConfiguration(name: "Dev", variant: .debug): .test()])), + ]) + + // When + XCTAssertThrowsSpecific( + try subject.fetch(configuration: "Debug", config: .test(), graph: graph), + DefaultConfigurationFetcherError.configurationNotFound("Debug", available: ["Dev"]) + ) + } + + func test_fetch_returns_the_default_configuration_if_the_configuration_exists_in_the_project() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project + .test( + settings: Settings( + configurations: [ + BuildConfiguration(name: "Dev", variant: .debug): .test(), + BuildConfiguration(name: "Release", variant: .release): .test(), + ] + ) + ), + ]) + + // When + let got = try subject.fetch( + configuration: nil, + config: .test( + generationOptions: .test( + defaultConfiguration: "Release" + ) + ), + graph: graph + ) + + // Then + XCTAssertEqual(got, "Release") + } + + func test_fetch_returns_the_configuration_if_the_configuration_and_default_configuration_exist_in_the_project() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project + .test( + settings: Settings( + configurations: [ + BuildConfiguration(name: "Dev", variant: .debug): .test(), + BuildConfiguration(name: "Release", variant: .release): .test(), + ] + ) + ), + ]) + + // When + let got = try subject.fetch( + configuration: "Dev", + config: .test( + generationOptions: .test( + defaultConfiguration: "Release" + ) + ), + graph: graph + ) + + // Then + XCTAssertEqual(got, "Dev") + } + + func test_fetch_throws_an_error_when_the_default_configuration_passed_points_to_a_non_existing_configuration() throws { + // Given + let graph = Graph.test(projects: [ + try AbsolutePath(validating: "/project-a"): Project + .test(settings: Settings(configurations: [BuildConfiguration(name: "Dev", variant: .debug): .test()])), + ]) + + // When + XCTAssertThrowsSpecific( + try subject.fetch( + configuration: nil, + config: .test( + generationOptions: .test( + defaultConfiguration: "Debug" + ) + ), + graph: graph + ), + DefaultConfigurationFetcherError.defaultConfigurationNotFound("Debug", available: ["Dev"]) + ) + } +} diff --git a/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift b/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift index c10cf9207ba..83d82362fe0 100644 --- a/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift +++ b/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistCore diff --git a/Tests/TuistCoreTests/Utils/XcodeProjectBuildDirectoryLocatorTests.swift b/Tests/TuistCoreTests/Utils/XcodeProjectBuildDirectoryLocatorTests.swift index 794c9724f18..eef120a4d7d 100644 --- a/Tests/TuistCoreTests/Utils/XcodeProjectBuildDirectoryLocatorTests.swift +++ b/Tests/TuistCoreTests/Utils/XcodeProjectBuildDirectoryLocatorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupportTesting import XCTest diff --git a/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift b/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift index a06f854e4f6..3c34b08f8a7 100644 --- a/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift +++ b/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistAcceptanceTesting import TuistSupport import TuistSupportTesting @@ -7,9 +7,37 @@ import XCTest final class DependenciesAcceptanceTestAppWithSPMDependencies: TuistAcceptanceTestCase { func test_app_spm_dependencies() async throws { - try setUpFixture(.appWithSpmDependencies) - try await run(FetchCommand.self) + try await setUpFixture(.appWithSpmDependencies) + try await run(InstallCommand.self) try await run(GenerateCommand.self) try await run(BuildCommand.self, "App") + try await run(BuildCommand.self, "VisionOSApp") + try await run(TestCommand.self, "AppKit") + } +} + +final class DependenciesAcceptanceTestAppWithSPMDependenciesWithoutInstall: TuistAcceptanceTestCase { + func test() async throws { + try await setUpFixture(.appWithSpmDependencies) + do { + try await run(GenerateCommand.self) + } catch { + XCTAssertEqual( + (error as? FatalError)?.description, + "We could not find external dependencies. Run `tuist install` before you continue." + ) + return + } + XCTFail("Generate should have failed.") + } +} + +final class DependenciesAcceptanceTestIosAppWithSPMDependencies: TuistAcceptanceTestCase { + func test_ios_app_spm_dependencies() async throws { + try await setUpFixture(.iosAppWithSpmDependencies) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App") + try await run(TestCommand.self, "App") } } diff --git a/Tests/TuistDependenciesTests/Carthage/CarthageInteractorTests.swift b/Tests/TuistDependenciesTests/Carthage/CarthageInteractorTests.swift deleted file mode 100644 index 4c43ca024a7..00000000000 --- a/Tests/TuistDependenciesTests/Carthage/CarthageInteractorTests.swift +++ /dev/null @@ -1,335 +0,0 @@ -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class CarthageInteractorTests: TuistUnitTestCase { - private var subject: CarthageInteractor! - private var carthageController: MockCarthageController! - private var carthageGraphGenerator: MockCarthageGraphGenerator! - - override func setUp() { - super.setUp() - - carthageController = MockCarthageController() - carthageGraphGenerator = MockCarthageGraphGenerator() - subject = CarthageInteractor( - carthageController: carthageController, - carthageGraphGenerator: carthageGraphGenerator - ) - } - - override func tearDown() { - carthageController = nil - carthageGraphGenerator = nil - subject = nil - - super.tearDown() - } - - func test_install_when_shouldNotBeUpdated() throws { - // Given - carthageController.canUseSystemCarthageStub = { true } - - let rootPath = try TemporaryDirectory(removeTreeOnDeinit: true).path - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - let carthageDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.carthageDirectoryName) - let carthageBuildDirectory = carthageDirectory - .appending(component: "Build") - - let platforms: Set = [.iOS, .watchOS, .macOS, .tvOS] - let stubbedDependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - ] - ) - - carthageController.bootstrapStub = { arg0, arg1, arg2 in - XCTAssertEqual(arg0, dependenciesDirectory) - XCTAssertEqual(arg1, platforms) - XCTAssertTrue(arg2) - - try self.simulateCarthageOutput(at: arg0) - } - carthageGraphGenerator.generateStub = { arg0 in - XCTAssertEqual(arg0, carthageDirectory.appending(component: "Build")) - return .test() - } - - // When - let got = try subject.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: stubbedDependencies, - platforms: platforms, - shouldUpdate: false - ) - - // Then - XCTAssertEqual(got, .test()) - XCTAssertTrue(carthageGraphGenerator.invokedGenerate) - - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - Constants.DependenciesDirectory.carthageDirectoryName, - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - Constants.DependenciesDirectory.cartfileResolvedName, - ] - ) - try XCTAssertDirectoryContentEqual( - carthageDirectory, - [ - "Build", - "Cartfile", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory, - [ - "iOS", - "Mac", - "watchOS", - "tvOS", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "iOS"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "Mac"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "watchOS"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "tvOS"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - } - - func test_install_when_shouldBeUpdated() throws { - // Given - carthageController.canUseSystemCarthageStub = { true } - - let rootPath = try TemporaryDirectory(removeTreeOnDeinit: true).path - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - let carthageDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.carthageDirectoryName) - let carthageBuildDirectory = carthageDirectory - .appending(component: "Build") - - let platforms: Set = [.iOS, .watchOS, .macOS, .tvOS] - let stubbedDependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - ] - ) - - carthageController.updateStub = { arg0, arg1, arg2 in - XCTAssertEqual(arg0, dependenciesDirectory) - XCTAssertEqual(arg1, platforms) - XCTAssertTrue(arg2) - - try self.simulateCarthageOutput(at: arg0) - } - carthageGraphGenerator.generateStub = { arg0 in - XCTAssertEqual(arg0, carthageDirectory.appending(component: "Build")) - return .test() - } - - // When - let got = try subject.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: stubbedDependencies, - platforms: platforms, - shouldUpdate: true - ) - - // Then - XCTAssertEqual(got, .test()) - XCTAssertTrue(carthageGraphGenerator.invokedGenerate) - - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - Constants.DependenciesDirectory.carthageDirectoryName, - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - Constants.DependenciesDirectory.cartfileResolvedName, - ] - ) - try XCTAssertDirectoryContentEqual( - carthageDirectory, - [ - "Build", - "Cartfile", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory, - [ - "iOS", - "Mac", - "watchOS", - "tvOS", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "iOS"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "Mac"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "watchOS"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - try XCTAssertDirectoryContentEqual( - carthageBuildDirectory.appending(component: "tvOS"), - [ - "Moya.framework", - "ReactiveMoya.framework", - "RxMoya.framework", - ] - ) - } - - func test_install_throws_when_carthageUnavailableInEnvironment() throws { - // Given - carthageController.canUseSystemCarthageStub = { false } - - let rootPath = try TemporaryDirectory(removeTreeOnDeinit: true).path - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let dependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - ] - ) - let platforms: Set = [.iOS] - - // When / Then - XCTAssertThrowsSpecific( - try subject.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: dependencies, - platforms: platforms, - shouldUpdate: true - ), - CarthageInteractorError.carthageNotFound - ) - } - - func test_clean() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - - try createFiles([ - "Dependencies/Lockfiles/Cartfile.resolved", - "Dependencies/Lockfiles/OtherLockfile.lock", - "Dependencies/Carthage/Info.plist", - "Dependencies/OtherDependenciesManager/bar.bar", - ]) - - // When - try subject.clean(dependenciesDirectory: dependenciesDirectory) - - // Then - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - "OtherDependenciesManager", - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - "OtherLockfile.lock", - ] - ) - } -} - -// MARK: - Helpers - -extension CarthageInteractorTests { - private func simulateCarthageOutput(at path: AbsolutePath) throws { - try [ - "Cartfile.resolved", - "Carthage/Cartfile", - "Carthage/Build/iOS/Moya.framework/Info.plist", - "Carthage/Build/iOS/ReactiveMoya.framework/Info.plist", - "Carthage/Build/iOS/RxMoya.framework/Info.plist", - "Carthage/Build/Mac/Moya.framework/Info.plist", - "Carthage/Build/Mac/ReactiveMoya.framework/Info.plist", - "Carthage/Build/Mac/RxMoya.framework/Info.plist", - "Carthage/Build/watchOS/Moya.framework/Info.plist", - "Carthage/Build/watchOS/ReactiveMoya.framework/Info.plist", - "Carthage/Build/watchOS/RxMoya.framework/Info.plist", - "Carthage/Build/tvOS/Moya.framework/Info.plist", - "Carthage/Build/tvOS/ReactiveMoya.framework/Info.plist", - "Carthage/Build/tvOS/RxMoya.framework/Info.plist", - ].forEach { - try fileHandler.touch(path.appending(try RelativePath(validating: $0))) - } - } -} diff --git a/Tests/TuistDependenciesTests/Carthage/Models/CarthageVersionFileTests.swift b/Tests/TuistDependenciesTests/Carthage/Models/CarthageVersionFileTests.swift deleted file mode 100644 index a398bf2109a..00000000000 --- a/Tests/TuistDependenciesTests/Carthage/Models/CarthageVersionFileTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class CarthageVersionFileTests: TuistUnitTestCase { - func test_codable_alamofire() { - // Given - let json = CarthageVersionFile.testAlamofireJson - let expected = CarthageVersionFile.testAlamofire - - // When / Then - XCTAssertDecodableEqualToJson(json, expected) - } - - func test_codable_rxSwift() { - // Given - let json = CarthageVersionFile.testRxSwiftJson - let expected = CarthageVersionFile.testRxSwift - - // When / Then - XCTAssertDecodableEqualToJson(json, expected) - } - - func test_codable_realmSwift() { - // Given - let json = CarthageVersionFile.testRealmCocoaJson - let expected = CarthageVersionFile.testRealmCocoa - - // When / Then - XCTAssertDecodableEqualToJson(json, expected) - } - - func test_codable_ahoyRTC() { - // Given - let json = CarthageVersionFile.testAhoyRTCJson - let expected = CarthageVersionFile.testAhoyRTC - - // When / Then - XCTAssertDecodableEqualToJson(json, expected) - } - - func test_allProducts() { - // Given - let iOSProduct = CarthageVersionFile.Product.test(name: "iOS") - let macOSProduct = CarthageVersionFile.Product.test(name: "macOS") - let watchOSProduct = CarthageVersionFile.Product.test(name: "watchOS") - let tvOSProduct = CarthageVersionFile.Product.test(name: "tvOS") - - let subject = CarthageVersionFile.test( - iOS: [iOSProduct], - macOS: [macOSProduct], - watchOS: [watchOSProduct], - tvOS: [tvOSProduct] - ) - - // When - let got = subject.allProducts - - // Then - let expected: [CarthageVersionFile.Product] = [iOSProduct, macOSProduct, watchOSProduct, tvOSProduct] - XCTAssertEqual(got, expected) - } -} diff --git a/Tests/TuistDependenciesTests/Carthage/Utils/CarthageControllerTests.swift b/Tests/TuistDependenciesTests/Carthage/Utils/CarthageControllerTests.swift deleted file mode 100644 index fddc82655c0..00000000000 --- a/Tests/TuistDependenciesTests/Carthage/Utils/CarthageControllerTests.swift +++ /dev/null @@ -1,199 +0,0 @@ -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistSupportTesting - -final class CarthageControllerTests: TuistUnitTestCase { - private var subject: CarthageController! - - override func setUp() { - super.setUp() - - subject = CarthageController() - } - - override func tearDown() { - subject = nil - - super.tearDown() - } - - func test_canUseSystemCarthage_available() { - // Given - system.whichStub = { _ in "path" } - - // When / Then - XCTAssertTrue(subject.canUseSystemCarthage()) - } - - func test_canUseSystemCarthage_unavailable() { - // Given - system.whichStub = { _ in throw NSError.test() } - - // When / Then - XCTAssertFalse(subject.canUseSystemCarthage()) - } - - func test_carthageVersion_carthageNotFound() { - // Given - system.errorCommand(["/usr/bin/env", "carthage", "version"]) - - // When / Then - XCTAssertThrowsSpecific(try subject.carthageVersion(), CarthageControllerError.carthageNotFound) - } - - func test_carthageVersion_success() { - // Given - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: "0.37.0", exitstatus: 0) - - // When / Then - XCTAssertEqual(try subject.carthageVersion(), Version(0, 37, 0)) - } - - func test_bootstrap() throws { - // Given - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: "0.37.0", exitstatus: 0) - - let path = try temporaryPath() - system.succeedCommand([ - "carthage", - "bootstrap", - "--project-directory", - path.pathString, - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ]) - - // When / Then - XCTAssertNoThrow(try subject.bootstrap(at: path, platforms: [], printOutput: false)) - } - - func test_bootstrap_with_platforms() throws { - // Given - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: "0.37.0", exitstatus: 0) - - let path = try temporaryPath() - system.succeedCommand([ - "carthage", - "bootstrap", - "--project-directory", - path.pathString, - "--platform", - "iOS", - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ]) - - // When / Then - XCTAssertNoThrow(try subject.bootstrap(at: path, platforms: [.iOS], printOutput: false)) - } - - func test_bootstrap_with_platforms_throws_when_xcframeworkdProductionUnsupported() throws { - // Given - let carthageVersion = Version("0.36.0") - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: carthageVersion.description, exitstatus: 0) - - let path = try temporaryPath() - system.succeedCommand([ - "carthage", - "bootstrap", - "--project-directory", - path.pathString, - "--platform", - "iOS", - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ]) - - // When / Then - XCTAssertThrowsSpecific( - try subject.bootstrap(at: path, platforms: [.iOS], printOutput: false), - CarthageControllerError.xcframeworksProductionNotSupported(installedVersion: carthageVersion) - ) - } - - func test_update() throws { - // Given - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: "0.37.0", exitstatus: 0) - - let path = try temporaryPath() - system.succeedCommand([ - "carthage", - "update", - "--project-directory", - path.pathString, - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ]) - - // When / Then - XCTAssertNoThrow(try subject.update(at: path, platforms: [], printOutput: false)) - } - - func test_update_with_platforms() throws { - // Given - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: "0.37.0", exitstatus: 0) - - let path = try temporaryPath() - system.succeedCommand([ - "carthage", - "update", - "--project-directory", - path.pathString, - "--platform", - "iOS", - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ]) - - // When / Then - XCTAssertNoThrow(try subject.update(at: path, platforms: [.iOS], printOutput: false)) - } - - func test_update_with_platforms_throws_when_xcframeworkdProductionUnsupported() throws { - // Given - let carthageVersion = Version("0.36.0") - system.stubs["/usr/bin/env carthage version"] = (stderror: nil, stdout: carthageVersion.description, exitstatus: 0) - - let path = try temporaryPath() - system.succeedCommand([ - "carthage", - "update", - "--project-directory", - path.pathString, - "--platform", - "iOS", - "--use-xcframeworks", - "--no-use-binaries", - "--use-netrc", - "--cache-builds", - "--new-resolver", - ]) - - // When / Then - XCTAssertThrowsSpecific( - try subject.bootstrap(at: path, platforms: [.iOS], printOutput: false), - CarthageControllerError.xcframeworksProductionNotSupported(installedVersion: carthageVersion) - ) - } -} diff --git a/Tests/TuistDependenciesTests/Carthage/Utils/CarthageGraphGeneratorTests.swift b/Tests/TuistDependenciesTests/Carthage/Utils/CarthageGraphGeneratorTests.swift deleted file mode 100644 index aa977a3958d..00000000000 --- a/Tests/TuistDependenciesTests/Carthage/Utils/CarthageGraphGeneratorTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class CarthageGraphGeneratorTests: TuistUnitTestCase { - private var subject: CarthageGraphGenerator! - - override func setUp() { - super.setUp() - - subject = CarthageGraphGenerator() - } - - override func tearDown() { - subject = nil - super.tearDown() - } - - func test_generate() throws { - // Given - let path = try temporaryPath() - - let rxSwiftVersionFilePath = path.appending(component: ".RxSwift.version") - try fileHandler.touch(rxSwiftVersionFilePath) - try fileHandler.write(CarthageVersionFile.testRxSwiftJson, path: rxSwiftVersionFilePath, atomically: true) - - let alamofireVersionFilePath = path.appending(component: ".Alamofire.version") - try fileHandler.touch(alamofireVersionFilePath) - try fileHandler.write(CarthageVersionFile.testAlamofireJson, path: alamofireVersionFilePath, atomically: true) - - // When - let got = try subject.generate(at: path) - - // Then - let expected = DependenciesGraph( - externalDependencies: [ - "RxSwift": [.xcframework(path: .relativeToManifest("Tuist/Dependencies/Carthage/Build/RxSwift.xcframework"))], - "RxCocoa": [.xcframework(path: .relativeToManifest("Tuist/Dependencies/Carthage/Build/RxCocoa.xcframework"))], - "RxRelay": [.xcframework(path: .relativeToManifest("Tuist/Dependencies/Carthage/Build/RxRelay.xcframework"))], - "RxTest": [.xcframework(path: .relativeToManifest("Tuist/Dependencies/Carthage/Build/RxTest.xcframework"))], - "RxBlocking": [.xcframework(path: .relativeToManifest("Tuist/Dependencies/Carthage/Build/RxBlocking.xcframework"))], - "Alamofire": [.xcframework(path: .relativeToManifest("Tuist/Dependencies/Carthage/Build/Alamofire.xcframework"))], - ], - externalProjects: [:] - ) - - XCTAssertEqual(got, expected) - } -} diff --git a/Tests/TuistDependenciesTests/DependenciesControllerTests.swift b/Tests/TuistDependenciesTests/DependenciesControllerTests.swift deleted file mode 100644 index 42d36da91c3..00000000000 --- a/Tests/TuistDependenciesTests/DependenciesControllerTests.swift +++ /dev/null @@ -1,525 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class DependenciesControllerTests: TuistUnitTestCase { - private var subject: DependenciesController! - - private var carthageInteractor: MockCarthageInteractor! - private var swiftPackageManagerInteractor: MockSwiftPackageManagerInteractor! - private var dependenciesGraphController: MockDependenciesGraphController! - - override func setUp() { - super.setUp() - - carthageInteractor = MockCarthageInteractor() - swiftPackageManagerInteractor = MockSwiftPackageManagerInteractor() - dependenciesGraphController = MockDependenciesGraphController() - - subject = DependenciesController( - carthageInteractor: carthageInteractor, - swiftPackageManagerInteractor: swiftPackageManagerInteractor, - dependenciesGraphController: dependenciesGraphController - ) - } - - override func tearDown() { - subject = nil - - carthageInteractor = nil - swiftPackageManagerInteractor = nil - - super.tearDown() - } - - // MARK: - Fetch - - func test_fetch_carthage() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectoryPath = rootPath - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - - let platforms: Set = [.iOS] - let carthageDependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - .github(path: "RxSwift", requirement: .exact("2.0.0")), - ] - ) - let dependencies = Dependencies( - carthage: carthageDependencies, - swiftPackageManager: nil, - platforms: platforms - ) - - let expectedGraphManifest = TuistCore.DependenciesGraph.testXCFramework(name: "Name") - - carthageInteractor.installStub = { arg0, arg1, arg2, arg3 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, carthageDependencies) - XCTAssertEqual(arg2, platforms) - XCTAssertFalse(arg3) - - return expectedGraphManifest - } - - // When - let graphManifest = try subject.fetch(at: rootPath, dependencies: dependencies, swiftVersion: nil) - - // Then - XCTAssertEqual(graphManifest, expectedGraphManifest) - - XCTAssertFalse(carthageInteractor.invokedClean) - XCTAssertTrue(carthageInteractor.invokedInstall) - - XCTAssertTrue(swiftPackageManagerInteractor.invokedClean) - XCTAssertFalse(swiftPackageManagerInteractor.invokedInstall) - } - - func test_fetch_swiftPackageManger() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectoryPath = rootPath - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - - let platforms: Set = [.iOS] - let swiftPackageManagerDependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "Moya", requirement: .exact("2.3.4")), - .remote(url: "Alamofire", requirement: .upToNextMajor("5.0.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - let dependencies = Dependencies( - carthage: nil, - swiftPackageManager: swiftPackageManagerDependencies, - platforms: platforms - ) - let swiftVersion = TSCUtility.Version(5, 4, 0) - - swiftPackageManagerInteractor.installStub = { arg0, arg1, arg2, arg3, arg4 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, swiftPackageManagerDependencies) - XCTAssertEqual(arg2, [.iOS]) - XCTAssertFalse(arg3) - XCTAssertEqual(arg4, TSCUtility.Version(5, 4, 0)) - return .test() - } - - // When - let graphManifest = try subject.fetch(at: rootPath, dependencies: dependencies, swiftVersion: swiftVersion) - - // Then - XCTAssertEqual(graphManifest, .test()) - - XCTAssertFalse(swiftPackageManagerInteractor.invokedClean) - XCTAssertTrue(swiftPackageManagerInteractor.invokedInstall) - - XCTAssertTrue(carthageInteractor.invokedClean) - XCTAssertFalse(carthageInteractor.invokedInstall) - } - - func test_fetch_carthage_swiftPackageManger() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectoryPath = rootPath - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - - let platforms: Set = [.iOS] - let carthageDependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - .github(path: "RxSwift", requirement: .exact("2.0.0")), - ] - ) - let swiftPackageManagerDependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "Moya", requirement: .exact("2.3.4")), - .remote(url: "Alamofire", requirement: .upToNextMajor("5.0.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - let dependencies = Dependencies( - carthage: carthageDependencies, - swiftPackageManager: swiftPackageManagerDependencies, - platforms: platforms - ) - let swiftVersion = TSCUtility.Version(5, 4, 0) - let carthageGraph = TuistCore.DependenciesGraph.testXCFramework(name: "Carthage") - let spmGraph = TuistCore.DependenciesGraph.testXCFramework(name: "SPM") - - carthageInteractor.installStub = { arg0, arg1, arg2, arg3 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, carthageDependencies) - XCTAssertEqual(arg2, platforms) - XCTAssertFalse(arg3) - - return carthageGraph - } - swiftPackageManagerInteractor.installStub = { arg0, arg1, arg2, arg3, arg4 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, swiftPackageManagerDependencies) - XCTAssertEqual(arg2, [.iOS]) - XCTAssertFalse(arg3) - XCTAssertEqual(arg4, swiftVersion) - return spmGraph - } - - // When - let graphManifest = try subject.fetch(at: rootPath, dependencies: dependencies, swiftVersion: swiftVersion) - - // Then - XCTAssertEqual( - graphManifest, - .init( - externalDependencies: [ - "Carthage": TuistCore.DependenciesGraph.testXCFramework(name: "Carthage") - .externalDependencies.values.first!, - "SPM": TuistCore.DependenciesGraph.testXCFramework(name: "SPM") - .externalDependencies.values.first!, - ], - externalProjects: [:] - ) - ) - - XCTAssertTrue(carthageInteractor.invokedInstall) - XCTAssertFalse(carthageInteractor.invokedClean) - - XCTAssertTrue(swiftPackageManagerInteractor.invokedInstall) - XCTAssertFalse(swiftPackageManagerInteractor.invokedClean) - } - - func test_fetch_carthage_swiftPackageManger_throws_when_duplicatedDependency() throws { - // Given - let rootPath = try temporaryPath() - let dependencies = TuistGraph.Dependencies( - carthage: .init( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - ] - ), - swiftPackageManager: .init( - .packages([ - .remote(url: "Moya", requirement: .exact("2.3.4")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ), - platforms: [.iOS] - ) - let carthageGraph = TuistCore.DependenciesGraph.testXCFramework( - name: "Duplicated", - path: Path(rootPath.appending(component: "Carthage").pathString) - ) - let spmGraph = TuistCore.DependenciesGraph.testXCFramework( - name: "Duplicated", - path: Path(rootPath.appending(component: "SPM").pathString) - ) - - carthageInteractor.installStub = { _, _, _, _ in - carthageGraph - } - swiftPackageManagerInteractor.installStub = { _, _, _, _, _ in - spmGraph - } - - // When / Then - XCTAssertThrowsSpecific( - try subject.fetch(at: rootPath, dependencies: dependencies, swiftVersion: nil), - DependenciesControllerError.duplicatedDependency( - "Duplicated", - carthageGraph.externalDependencies.values.first!, - spmGraph.externalDependencies.values.first! - ) - ) - - // Then - XCTAssertTrue(carthageInteractor.invokedInstall) - XCTAssertFalse(carthageInteractor.invokedClean) - - XCTAssertTrue(swiftPackageManagerInteractor.invokedInstall) - XCTAssertFalse(swiftPackageManagerInteractor.invokedClean) - } - - func test_fetch_throws_when_noPlatforms() throws { - // Given - let rootPath = try temporaryPath() - - let dependencies = TuistGraph.Dependencies( - carthage: .init([]), - swiftPackageManager: .init(.packages([]), productTypes: [:], baseSettings: .default, targetSettings: [:]), - platforms: [] - ) - - // When / Then - XCTAssertThrowsSpecific( - try subject.fetch(at: rootPath, dependencies: dependencies, swiftVersion: nil), - DependenciesControllerError.noPlatforms - ) - } - - func test_fetch_no_dependencies() throws { - // Given - let rootPath = try temporaryPath() - - let dependencies = TuistGraph.Dependencies( - carthage: .init([]), - swiftPackageManager: .init(.packages([]), productTypes: [:], baseSettings: .default, targetSettings: [:]), - platforms: [.iOS] - ) - - // When - let graphManifest = try subject.fetch(at: rootPath, dependencies: dependencies, swiftVersion: nil) - - // Then - XCTAssertEqual(graphManifest, .none) - XCTAssertFalse(carthageInteractor.invokedInstall) - XCTAssertTrue(carthageInteractor.invokedClean) - } - - // MARK: - Update - - func test_update_carthage() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectoryPath = rootPath - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - - let platforms: Set = [.iOS] - let carthageDependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - .github(path: "RxSwift", requirement: .exact("2.0.0")), - ] - ) - let dependencies = Dependencies( - carthage: carthageDependencies, - swiftPackageManager: nil, - platforms: platforms - ) - let expectedGraph = TuistCore.DependenciesGraph.testXCFramework(name: "Name") - - carthageInteractor.installStub = { arg0, arg1, arg2, arg3 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, carthageDependencies) - XCTAssertEqual(arg2, platforms) - XCTAssertTrue(arg3) - - return expectedGraph - } - - // When - let graphManifest = try subject.update(at: rootPath, dependencies: dependencies, swiftVersion: nil) - - // Then - XCTAssertEqual(graphManifest, expectedGraph) - XCTAssertFalse(carthageInteractor.invokedClean) - XCTAssertTrue(carthageInteractor.invokedInstall) - - XCTAssertTrue(swiftPackageManagerInteractor.invokedClean) - XCTAssertFalse(swiftPackageManagerInteractor.invokedInstall) - } - - func test_update_swiftPackageManger() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectoryPath = rootPath - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - - let platforms: Set = [.iOS] - let swiftPackageManagerDependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "Moya", requirement: .exact("2.3.4")), - .remote(url: "Alamofire", requirement: .upToNextMajor("5.0.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - let dependencies = Dependencies( - carthage: nil, - swiftPackageManager: swiftPackageManagerDependencies, - platforms: platforms - ) - let swiftVersion = TSCUtility.Version(5, 4, 0) - - swiftPackageManagerInteractor.installStub = { arg0, arg1, arg2, arg3, arg4 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, swiftPackageManagerDependencies) - XCTAssertEqual(arg2, [.iOS]) - XCTAssertTrue(arg3) - XCTAssertEqual(arg4, swiftVersion) - return .test() - } - - // When - let graphManifest = try subject.update(at: rootPath, dependencies: dependencies, swiftVersion: swiftVersion) - - // Then - XCTAssertEqual(graphManifest, .test()) - XCTAssertFalse(swiftPackageManagerInteractor.invokedClean) - XCTAssertTrue(swiftPackageManagerInteractor.invokedInstall) - - XCTAssertTrue(carthageInteractor.invokedClean) - XCTAssertFalse(carthageInteractor.invokedInstall) - } - - func test_update_carthage_swiftPackageManger() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectoryPath = rootPath - .appending(component: Constants.tuistDirectoryName) - .appending(component: Constants.DependenciesDirectory.name) - - let platforms: Set = [.iOS] - let carthageDependencies = CarthageDependencies( - [ - .github(path: "Moya", requirement: .exact("1.1.1")), - .github(path: "RxSwift", requirement: .exact("2.0.0")), - ] - ) - let swiftPackageManagerDependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "Moya", requirement: .exact("2.3.4")), - .remote(url: "Alamofire", requirement: .upToNextMajor("5.0.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - let dependencies = Dependencies( - carthage: carthageDependencies, - swiftPackageManager: swiftPackageManagerDependencies, - platforms: platforms - ) - let swiftVersion = TSCUtility.Version(5, 4, 0) - let expectedGraph = TuistCore.DependenciesGraph.testXCFramework(name: "Name") - - carthageInteractor.installStub = { arg0, arg1, arg2, arg3 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, carthageDependencies) - XCTAssertEqual(arg2, platforms) - XCTAssertTrue(arg3) - - return expectedGraph - } - swiftPackageManagerInteractor.installStub = { arg0, arg1, arg2, arg3, arg4 in - XCTAssertEqual(arg0, dependenciesDirectoryPath) - XCTAssertEqual(arg1, swiftPackageManagerDependencies) - XCTAssertEqual(arg2, [.iOS]) - XCTAssertTrue(arg3) - XCTAssertEqual(arg4, swiftVersion) - return .test() - } - - // When - let graphManifest = try subject.update(at: rootPath, dependencies: dependencies, swiftVersion: swiftVersion) - - // Then - XCTAssertEqual(graphManifest, expectedGraph) - XCTAssertFalse(carthageInteractor.invokedClean) - XCTAssertTrue(carthageInteractor.invokedInstall) - - XCTAssertFalse(swiftPackageManagerInteractor.invokedClean) - XCTAssertTrue(swiftPackageManagerInteractor.invokedInstall) - } - - func test_update_throws_when_noPlatforms() throws { - // Given - let rootPath = try temporaryPath() - - let dependencies = TuistGraph.Dependencies( - carthage: .init([]), - swiftPackageManager: .init(.packages([]), productTypes: [:], baseSettings: .default, targetSettings: [:]), - platforms: [] - ) - - // When / Then - XCTAssertThrowsSpecific( - try subject.update(at: rootPath, dependencies: dependencies, swiftVersion: nil), - DependenciesControllerError.noPlatforms - ) - } - - func test_update_no_dependencies() throws { - // Given - let rootPath = try temporaryPath() - - let dependencies = TuistGraph.Dependencies( - carthage: .init([]), - swiftPackageManager: .init(.packages([]), productTypes: [:], baseSettings: .default, targetSettings: [:]), - platforms: [.iOS] - ) - - // When - let graphManifest = try subject.update(at: rootPath, dependencies: dependencies, swiftVersion: nil) - - // Then - XCTAssertEqual(graphManifest, .none) - XCTAssertFalse(carthageInteractor.invokedInstall) - XCTAssertTrue(carthageInteractor.invokedClean) - } - - func test_save() throws { - // Given - let rootPath = try temporaryPath() - - let dependenciesGraph = TuistGraph.DependenciesGraph( - externalDependencies: [ - "library": [.xcframework(path: "/library.xcframework", status: .required)], - "anotherLibrary": [.project(target: "Target", path: "/anotherLibrary")], - ], - externalProjects: [ - "/anotherLibrary": .test(), - ] - ) - - dependenciesGraphController.saveStub = { arg0, arg1 in - XCTAssertEqual(arg0, dependenciesGraph) - XCTAssertEqual(arg1, rootPath) - } - - // When - try subject.save(dependenciesGraph: dependenciesGraph, to: rootPath) - - // Then - XCTAssertFalse(dependenciesGraphController.invokedClean) - XCTAssertTrue(dependenciesGraphController.invokedSave) - } - - func test_save_no_dependencies() throws { - // Given - let rootPath = try temporaryPath() - - dependenciesGraphController.saveStub = { arg0, arg1 in - XCTAssertEqual(arg0, .none) - XCTAssertEqual(arg1, rootPath) - } - - // When - try subject.save(dependenciesGraph: .none, to: rootPath) - - // Then - XCTAssertFalse(dependenciesGraphController.invokedClean) - XCTAssertTrue(dependenciesGraphController.invokedSave) - } -} diff --git a/Tests/TuistDependenciesTests/DependenciesGraph/DependenciesGraphControllerTests.swift b/Tests/TuistDependenciesTests/DependenciesGraph/DependenciesGraphControllerTests.swift deleted file mode 100644 index 93fc3397578..00000000000 --- a/Tests/TuistDependenciesTests/DependenciesGraph/DependenciesGraphControllerTests.swift +++ /dev/null @@ -1,138 +0,0 @@ -import Foundation -import TSCBasic -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistGraphTesting -@testable import TuistSupportTesting - -public final class DependenciesGraphControllerTests: TuistUnitTestCase { - private var subject: DependenciesGraphController! - - override public func setUp() { - super.setUp() - subject = DependenciesGraphController() - } - - override public func tearDown() { - subject = nil - super.tearDown() - } - - func test_save() throws { - // Given - let root = try temporaryPath() - let graph = TuistGraph.DependenciesGraph.test() - - // When - try subject.save(graph, to: root) - - // Then - let graphPath = root.appending(components: "Tuist", "Dependencies", "graph.json") - XCTAssertTrue(fileHandler.exists(graphPath)) - } - - func test_load() throws { - // Given - let root = try temporaryPath() - - let dependenciesPath = root.appending(components: "Tuist", "Dependencies.swift") - try fileHandler.touch(dependenciesPath) - - try fileHandler.write(TuistGraph.DependenciesGraph.testDependenciesFile, path: dependenciesPath, atomically: true) - - let graphPath = root.appending(components: "Tuist", "Dependencies", "graph.json") - try fileHandler.touch(graphPath) - - try fileHandler.write(TuistGraph.DependenciesGraph.testJson, path: graphPath, atomically: true) - - // When - let got = try subject.load(at: root) - - // Then - let expected = TuistGraph.DependenciesGraph( - externalDependencies: ["RxSwift": [.xcframework(path: "/Tuist/Dependencies/Carthage/RxSwift.xcframework", - status: .required)]], - externalProjects: [:] - ) - - XCTAssertEqual(got, expected) - } - - func test_load_failed() throws { - // Given - let root = try temporaryPath() - - let dependenciesPath = root.appending(components: "Tuist", "Dependencies.swift") - try fileHandler.touch(dependenciesPath) - - try fileHandler.write(TuistGraph.DependenciesGraph.testDependenciesFile, path: dependenciesPath, atomically: true) - - let graphPath = root.appending(components: "Tuist", "Dependencies", "graph.json") - try fileHandler.touch(graphPath) - - try fileHandler.write( - """ - { - "externalDependencies": {}, - "externalProjects": [ - "ProjectPath", - { - "invalid": "Project" - } - ] - } - """, - path: graphPath, - atomically: true - ) - - // When / Then - XCTAssertThrowsSpecific( - try subject.load(at: root), - DependenciesGraphControllerError.failedToDecodeDependenciesGraph - ) - } - - func test_load_without_fetching() throws { - // Given - let root = try temporaryPath() - - let dependenciesPath = root.appending(components: "Tuist", "Dependencies.swift") - try fileHandler.touch(dependenciesPath) - - try fileHandler.write(TuistGraph.DependenciesGraph.testDependenciesFile, path: dependenciesPath, atomically: true) - - // When / Then - XCTAssertThrowsSpecific( - try subject.load(at: root), - DependenciesGraphControllerError.dependenciesWerentFetched - ) - } - - func test_load_no_dependencies() throws { - // Given - let root = try temporaryPath() - let dependenciesPath = root.appending(components: "Tuist") - try fileHandler.touch(dependenciesPath) - - // When / Then - XCTAssertEqual(try subject.load(at: root), .none) - } - - func test_clean() throws { - // Given - let root = try temporaryPath() - let graphPath = root.appending(components: "Tuist", "Dependencies", "graph.json") - try fileHandler.touch(graphPath) - - // When - try subject.clean(at: root) - - // Then - XCTAssertFalse(fileHandler.exists(graphPath)) - } -} diff --git a/Tests/TuistDependenciesTests/Mappers/ExternalDependencyPathWorkspaceMapper.swift b/Tests/TuistDependenciesTests/Mappers/ExternalDependencyPathWorkspaceMapper.swift new file mode 100644 index 00000000000..61e086100c3 --- /dev/null +++ b/Tests/TuistDependenciesTests/Mappers/ExternalDependencyPathWorkspaceMapper.swift @@ -0,0 +1,96 @@ +import Foundation +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistDependencies + +final class ExternalDependencyPathWorkspaceMapperTests: TuistUnitTestCase { + private var subject: ExternalDependencyPathWorkspaceMapper! + + override func setUp() { + super.setUp() + subject = ExternalDependencyPathWorkspaceMapper() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_map() throws { + // Given + let projectPath = try temporaryPath() + let project = Project.test( + path: projectPath, + sourceRootPath: projectPath, + xcodeProjPath: projectPath.appending(component: "A.xcodeproj"), + name: "A" + ) + + let externalProjectBasePath = try temporaryPath() + .appending(component: Constants.SwiftPackageManager.packageBuildDirectoryName) + let externalProjectPath = externalProjectBasePath.appending( + components: [ + "checkouts", + "ExternalDependency", + ] + ) + let externalProject = Project.test( + path: externalProjectPath, + sourceRootPath: externalProjectPath, + xcodeProjPath: externalProjectPath.appending(component: "ExternalDependency.xcodeproj"), + name: "ExternalDependency", + isExternal: true + ) + + let workspace = Workspace.test( + name: "A" + ) + + // When + let (gotWorkspaceWithProjects, _) = try subject.map( + workspace: WorkspaceWithProjects( + workspace: workspace, + projects: [ + project, + externalProject, + ] + ) + ) + + // Then + XCTAssertBetterEqual( + gotWorkspaceWithProjects.projects, + [ + Project.test( + path: projectPath, + sourceRootPath: projectPath, + xcodeProjPath: projectPath.appending(component: "A.xcodeproj"), + name: "A" + ), + Project.test( + path: externalProjectPath, + sourceRootPath: externalProject.sourceRootPath, + xcodeProjPath: externalProjectBasePath.appending( + components: [ + Constants.DerivedDirectory.dependenciesDerivedDirectory, + "ExternalDependency", + "ExternalDependency.xcodeproj", + ] + ), + name: "ExternalDependency", + settings: Settings.test( + base: [ + "SRCROOT": .string(externalProjectPath.pathString), + ] + ), + isExternal: true + ), + ] + ) + } +} diff --git a/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift b/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift index aebb4b81d2f..9c56f8c53c2 100644 --- a/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift +++ b/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift @@ -1,11 +1,10 @@ import Foundation -import TuistGraph -import TuistGraphTesting +import TuistCore +import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase { var subject: ExternalProjectsPlatformNarrowerGraphMapper! @@ -43,26 +42,22 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase 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) + let (mappedGraph, _, _) = try await subject.map(graph: graph, environment: MapperEnvironment()) // Then - XCTAssertEqual(try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), Set([.iOS])) + + XCTAssertEqual( + try XCTUnwrap(mappedGraph.projects[project.path]?.targets[appTarget.name])?.supportedPlatforms, + Set([.iOS]) + ) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]![externalPackage.name]?.supportedPlatforms), + try XCTUnwrap(mappedGraph.projects[externalProject.path]?.targets[externalPackage.name]?.supportedPlatforms), Set([.iOS]) ) } @@ -94,14 +89,6 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase directory: project, packagesDirectory: externalProject, ], - targets: [ - project.path: [ - appTarget.name: appTarget, - ], - externalProject.path: [ - externalPackage.name: externalPackage, - ], - ], dependencies: [ appTargetDependency: Set([externalPackageDependency]), ], @@ -111,19 +98,20 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase ) // When - let (mappedGraph, _) = try await subject.map(graph: graph) + let (mappedGraph, _, _) = try await subject.map(graph: graph, environment: MapperEnvironment()) // Then + XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), + try XCTUnwrap(mappedGraph.projects[project.path]?.targets[appTarget.name]?.supportedPlatforms), Set([.iOS, .macOS, .tvOS, .watchOS]) ) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]![externalPackage.name]?.supportedPlatforms), + try XCTUnwrap(mappedGraph.projects[externalProject.path]?.targets[externalPackage.name]?.supportedPlatforms), Set([.iOS]) ) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]![externalPackage.name]?.deploymentTargets), + try XCTUnwrap(mappedGraph.projects[externalProject.path]?.targets[externalPackage.name]?.deploymentTargets), .iOS("16.0") ) } @@ -164,15 +152,6 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase 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]), @@ -180,16 +159,23 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase ) // When - let (mappedGraph, _) = try await subject.map(graph: graph) + let (mappedGraph, _, _) = try await subject.map(graph: graph, environment: MapperEnvironment()) // Then - XCTAssertEqual(try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), Set([.iOS])) + + XCTAssertEqual( + try XCTUnwrap(mappedGraph.projects[project.path]?.targets[appTarget.name]?.supportedPlatforms), + Set([.iOS]) + ) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]?[directExternalPackage.name]?.supportedPlatforms), + try XCTUnwrap(mappedGraph.projects[externalProject.path]?.targets[directExternalPackage.name]?.supportedPlatforms), Set([.iOS]) ) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]?[transitiveExternalPackage.name]?.supportedPlatforms), + try XCTUnwrap( + mappedGraph.projects[externalProject.path]?.targets[transitiveExternalPackage.name]? + .supportedPlatforms + ), Set([.iOS]) ) } @@ -229,15 +215,6 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase 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]), @@ -245,16 +222,19 @@ final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase ) // When - let (mappedGraph, _) = try await subject.map(graph: graph) + let (mappedGraph, _, _) = try await subject.map(graph: graph, environment: MapperEnvironment()) // Then - XCTAssertEqual(try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), Set([.iOS])) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]?[externalMacroFramework.name]?.supportedPlatforms), + try XCTUnwrap(mappedGraph.projects[project.path]?.targets[appTarget.name]?.supportedPlatforms), + Set([.iOS]) + ) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.projects[externalProject.path]?.targets[externalMacroFramework.name]?.supportedPlatforms), Set([.iOS]) ) XCTAssertEqual( - try XCTUnwrap(mappedGraph.targets[externalProject.path]?[externalMacroExecutable.name]?.supportedPlatforms), + try XCTUnwrap(mappedGraph.projects[externalProject.path]?.targets[externalMacroExecutable.name]?.supportedPlatforms), Set([.macOS]) ) } diff --git a/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift b/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift index 4b61b5d4bce..a3d61d12787 100644 --- a/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift +++ b/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift @@ -1,12 +1,11 @@ import Foundation -import TSCBasic -import TuistGraph -import TuistGraphTesting +import Path +import TuistCore +import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting final class PruneOrphanExternalTargetsGraphMapperTests: TuistUnitTestCase { var subject: PruneOrphanExternalTargetsGraphMapper! @@ -28,11 +27,21 @@ final class PruneOrphanExternalTargetsGraphMapperTests: TuistUnitTestCase { 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 transitivePackageProductWithNoDestinations = Target.test( + name: "TransitivePackageWithNoDestination", + destinations: [], + 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], + targets: [ + directPackageProduct, + transitivePackageProduct, + transitivePackageProductWithNoDestinations, + packageDevProduct, + ], isExternal: true ) let directPackageProductDependency = GraphDependency.target(name: directPackageProduct.name, path: packageProject.path) @@ -40,30 +49,35 @@ final class PruneOrphanExternalTargetsGraphMapperTests: TuistUnitTestCase { name: transitivePackageProduct.name, path: packageProject.path ) + let transitivePackageProductWithNoDestinationsDependency = GraphDependency.target( + name: transitivePackageProductWithNoDestinations.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]), + directPackageProductDependency: Set([ + transitivePackageProductDependency, + transitivePackageProductWithNoDestinationsDependency, + ]), ] ) // When - let (gotGraph, _) = try await subject.map(graph: graph) + let (gotGraph, _, _) = try await subject.map(graph: graph, environment: MapperEnvironment()) // 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]) + + XCTAssertEqual(gotGraph.projects[project.path]?.targets[app.name]?.prune, false) + XCTAssertEqual(gotGraph.projects[packageProject.path]?.targets[directPackageProduct.name]?.prune, false) + XCTAssertEqual(gotGraph.projects[packageProject.path]?.targets[transitivePackageProduct.name]?.prune, false) + XCTAssertEqual(gotGraph.projects[packageProject.path]?.targets[packageDevProduct.name]?.prune, true) + XCTAssertEqual( + gotGraph.projects[packageProject.path]?.targets[transitivePackageProductWithNoDestinations.name]?.prune, + true + ) } } diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/SwiftPackageManagerInteractorTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/SwiftPackageManagerInteractorTests.swift deleted file mode 100644 index cc30cede041..00000000000 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/SwiftPackageManagerInteractorTests.swift +++ /dev/null @@ -1,366 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class SwiftPackageManagerInteractorTests: TuistUnitTestCase { - private var subject: SwiftPackageManagerInteractor! - private var swiftPackageManagerController: MockSwiftPackageManagerController! - private var swiftPackageManagerGraphGenerator: MockSwiftPackageManagerGraphGenerator! - - override func setUp() { - super.setUp() - - swiftPackageManagerController = MockSwiftPackageManagerController() - swiftPackageManagerGraphGenerator = MockSwiftPackageManagerGraphGenerator() - subject = SwiftPackageManagerInteractor( - swiftPackageManagerController: swiftPackageManagerController, - swiftPackageManagerGraphGenerator: swiftPackageManagerGraphGenerator - ) - } - - override func tearDown() { - subject = nil - swiftPackageManagerController = nil - - super.tearDown() - } - - func test_install_when_shouldNotBeUpdated() throws { - // Given - let rootPath = try TemporaryDirectory(removeTreeOnDeinit: true).path - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - let swiftPackageManagerDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.swiftPackageManagerDirectoryName) - let swiftPackageManagerBuildDirectory = swiftPackageManagerDirectory.appending(component: ".build") - - let dependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "https://github.com/Alamofire/Alamofire.git", requirement: .upToNextMajor("5.2.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - - system.swiftVersionStub = { "5.6.0" } - swiftPackageManagerController.resolveStub = { path, printOutput in - XCTAssertEqual(path, swiftPackageManagerDirectory) - XCTAssertTrue(printOutput) - try self.simulateSPMOutput(at: path) - } - swiftPackageManagerController.setToolsVersionStub = { path, version in - XCTAssertEqual(path, swiftPackageManagerDirectory) - XCTAssertEqual(version, TSCUtility.Version(5, 6, 0)) - } - - swiftPackageManagerGraphGenerator - .generateStub = - { path, automaticProductType, platforms, baseSettings, targetSettings, swiftToolsVersion, projectOptions in - XCTAssertEqual(path, swiftPackageManagerBuildDirectory) - XCTAssertEqual(platforms, [.iOS]) - XCTAssertEqual(automaticProductType, [:]) - XCTAssertEqual(baseSettings, .default) - XCTAssertEqual(targetSettings, [:]) - XCTAssertNil(swiftToolsVersion) - XCTAssertEqual(projectOptions, [:]) - return .test() - } - - // When - let dependenciesGraph = try subject.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: dependencies, - platforms: [.iOS], - shouldUpdate: false, - swiftToolsVersion: nil - ) - - // Then - XCTAssertEqual(dependenciesGraph, .test()) - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - Constants.DependenciesDirectory.swiftPackageManagerDirectoryName, - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - Constants.DependenciesDirectory.packageResolvedName, - ] - ) - try XCTAssertDirectoryContentEqual( - swiftPackageManagerDirectory, - [ - ".build", - "Package.swift", - ] - ) - try XCTAssertDirectoryContentEqual( - swiftPackageManagerBuildDirectory, - [ - "manifest.db", - "workspace-state.json", - "artifacts", - "checkouts", - "repositories", - ] - ) - - XCTAssertTrue(swiftPackageManagerController.invokedResolve) - XCTAssertTrue(swiftPackageManagerController.invokedSetToolsVersion) - XCTAssertFalse(swiftPackageManagerController.invokedUpdate) - } - - func test_install_when_shouldNotBeUpdated_and_swiftToolsVersionPassed() throws { - // Given - let rootPath = try TemporaryDirectory(removeTreeOnDeinit: true).path - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - let swiftPackageManagerDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.swiftPackageManagerDirectoryName) - let swiftPackageManagerBuildDirectory = swiftPackageManagerDirectory.appending(component: ".build") - - let swiftToolsVersion = TSCUtility.Version(5, 3, 0) - let dependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "https://github.com/Alamofire/Alamofire.git", requirement: .upToNextMajor("5.2.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - - system.swiftVersionStub = { "5.6.0" } - swiftPackageManagerController.resolveStub = { path, printOutput in - XCTAssertEqual(path, swiftPackageManagerDirectory) - XCTAssertTrue(printOutput) - try self.simulateSPMOutput(at: path) - } - swiftPackageManagerController.setToolsVersionStub = { path, version in - XCTAssertEqual(path, swiftPackageManagerDirectory) - XCTAssertEqual(version, swiftToolsVersion) - } - - swiftPackageManagerGraphGenerator - .generateStub = { path, automaticProductType, platforms, baseSettings, targetSettings, swiftVersion, projectOptions in - XCTAssertEqual(path, swiftPackageManagerBuildDirectory) - XCTAssertEqual(automaticProductType, [:]) - XCTAssertEqual(platforms, [.iOS]) - XCTAssertEqual(baseSettings, .default) - XCTAssertEqual(targetSettings, [:]) - XCTAssertEqual(swiftVersion, swiftToolsVersion) - XCTAssertEqual(projectOptions, [:]) - return .test() - } - - // When - let dependenciesGraph = try subject.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: dependencies, - platforms: [.iOS], - shouldUpdate: false, - swiftToolsVersion: swiftToolsVersion - ) - - // Then - XCTAssertEqual(dependenciesGraph, .test()) - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - Constants.DependenciesDirectory.swiftPackageManagerDirectoryName, - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - Constants.DependenciesDirectory.packageResolvedName, - ] - ) - try XCTAssertDirectoryContentEqual( - swiftPackageManagerDirectory, - [ - ".build", - "Package.swift", - ] - ) - try XCTAssertDirectoryContentEqual( - swiftPackageManagerBuildDirectory, - [ - "manifest.db", - "workspace-state.json", - "artifacts", - "checkouts", - "repositories", - ] - ) - - XCTAssertTrue(swiftPackageManagerController.invokedResolve) - XCTAssertTrue(swiftPackageManagerController.invokedSetToolsVersion) - XCTAssertFalse(swiftPackageManagerController.invokedUpdate) - } - - func test_install_when_shouldBeUpdated() throws { - // Given - let rootPath = try TemporaryDirectory(removeTreeOnDeinit: true).path - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - let swiftPackageManagerDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.swiftPackageManagerDirectoryName) - let swiftPackageManagerBuildDirectory = swiftPackageManagerDirectory.appending(component: ".build") - - let dependencies = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "https://github.com/Alamofire/Alamofire.git", requirement: .upToNextMajor("5.2.0")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ) - - system.swiftVersionStub = { "5.6.0" } - swiftPackageManagerController.updateStub = { path, printOutput in - XCTAssertEqual(path, swiftPackageManagerDirectory) - XCTAssertTrue(printOutput) - try self.simulateSPMOutput(at: path) - } - swiftPackageManagerController.setToolsVersionStub = { path, version in - XCTAssertEqual(path, swiftPackageManagerDirectory) - XCTAssertEqual(version, Version("5.6.0")) - } - - swiftPackageManagerGraphGenerator - .generateStub = - { path, automaticProductType, platforms, baseSettings, targetSettings, swiftToolsVersion, configuration in - XCTAssertEqual(path, swiftPackageManagerBuildDirectory) - XCTAssertEqual(automaticProductType, [:]) - XCTAssertEqual(platforms, [.iOS]) - XCTAssertEqual(baseSettings, .default) - XCTAssertEqual(targetSettings, [:]) - XCTAssertNil(swiftToolsVersion) - XCTAssertEqual(configuration, [:]) - return .test() - } - - // When - let dependenciesGraph = try subject.install( - dependenciesDirectory: dependenciesDirectory, - dependencies: dependencies, - platforms: [.iOS], - shouldUpdate: true, - swiftToolsVersion: nil - ) - - // Then - XCTAssertEqual(dependenciesGraph, .test()) - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - Constants.DependenciesDirectory.swiftPackageManagerDirectoryName, - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - Constants.DependenciesDirectory.packageResolvedName, - ] - ) - try XCTAssertDirectoryContentEqual( - swiftPackageManagerDirectory, - [ - ".build", - "Package.swift", - ] - ) - try XCTAssertDirectoryContentEqual( - swiftPackageManagerBuildDirectory, - [ - "manifest.db", - "workspace-state.json", - "artifacts", - "checkouts", - "repositories", - ] - ) - - XCTAssertTrue(swiftPackageManagerController.invokedUpdate) - XCTAssertTrue(swiftPackageManagerController.invokedSetToolsVersion) - XCTAssertFalse(swiftPackageManagerController.invokedResolve) - } - - func test_clean() throws { - // Given - let rootPath = try temporaryPath() - let dependenciesDirectory = rootPath - .appending(component: Constants.DependenciesDirectory.name) - let lockfilesDirectory = dependenciesDirectory - .appending(component: Constants.DependenciesDirectory.lockfilesDirectoryName) - - try createFiles([ - "Dependencies/SwiftPackageManager/Package.swift", - "Dependencies/Lockfiles/Package.resolved", - "Dependencies/Lockfiles/OtherLockfile.lock", - "Dependencies/SwiftPackageManager/Info.plist", - "Dependencies/OtherDependenciesManager/bar.bar", - ]) - - // When - try subject.clean(dependenciesDirectory: dependenciesDirectory) - - // Then - try XCTAssertDirectoryContentEqual( - dependenciesDirectory, - [ - Constants.DependenciesDirectory.lockfilesDirectoryName, - "OtherDependenciesManager", - ] - ) - try XCTAssertDirectoryContentEqual( - lockfilesDirectory, - [ - "OtherLockfile.lock", - ] - ) - - XCTAssertFalse(swiftPackageManagerController.invokedUpdate) - XCTAssertFalse(swiftPackageManagerController.invokedSetToolsVersion) - XCTAssertFalse(swiftPackageManagerController.invokedResolve) - } -} - -// MARK: - Helpers - -extension SwiftPackageManagerInteractorTests { - private func simulateSPMOutput(at path: AbsolutePath) throws { - try [ - "Package.swift", - "Package.resolved", - ".build/manifest.db", - ".build/workspace-state.json", - ".build/artifacts/foo.txt", - ".build/checkouts/Alamofire/Info.plist", - ".build/repositories/checkouts-state.json", - ".build/repositories/Alamofire-e8f130fe/config", - ].forEach { - try fileHandler.touch(path.appending(try RelativePath(validating: $0))) - } - } -} diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoGraphPlatformTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoGraphPlatformTests.swift deleted file mode 100644 index 9a885d7c85e..00000000000 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoGraphPlatformTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import ProjectDescription -import TuistGraph -import TuistSupport - -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class PackageInfoGraphPlatformTests: TuistUnitTestCase { - func test_platformNameCorrectCase() throws { - let iosPlatform = PackageInfo.Platform(platformName: "iOS", version: "17.0.0", options: []) - - let graphPlatform = try iosPlatform.graphPlatform() - let destinations = try iosPlatform.destinations() - - XCTAssertEqual(graphPlatform, .iOS) - XCTAssertEqual(destinations, [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign]) - } - - func test_platformNameLowerCase() throws { - let iosPlatform = PackageInfo.Platform(platformName: "ios", version: "17.0.0", options: []) - - let graphPlatform = try iosPlatform.graphPlatform() - let destinations = try iosPlatform.destinations() - - XCTAssertEqual(graphPlatform, .iOS) - XCTAssertEqual(destinations, [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign]) - } - - func test_platformNameMixedCase() throws { - let iosPlatform = PackageInfo.Platform(platformName: "VisiOnOS", version: "17.0.0", options: []) - - let graphPlatform = try iosPlatform.graphPlatform() - let destinations = try iosPlatform.destinations() - - XCTAssertEqual(graphPlatform, .visionOS) - XCTAssertEqual(destinations, [.appleVision]) - } -} diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift deleted file mode 100644 index 1b0b44415c3..00000000000 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift +++ /dev/null @@ -1,3348 +0,0 @@ -import ProjectDescription -import TSCBasic -import TSCUtility -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistDependencies -@testable import TuistDependenciesTesting -@testable import TuistSupportTesting - -final class PackageInfoMapperTests: TuistUnitTestCase { - private var subject: PackageInfoMapper! - - override func setUp() { - super.setUp() - - system.swiftVersionStub = { "5.7.0" } - subject = PackageInfoMapper() - } - - override func tearDown() { - subject = nil - - super.tearDown() - } - - func testPreprocess_whenProductContainsBinaryTarget_mapsToXcframework() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Sources/Target_1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Sources/Target_2"))) - let preprocessInfo = try subject.preprocess( - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target_1", "Target_2"]), - ], - targets: [ - .test(name: "Target_1", type: .binary, url: "https://binary.target.com"), - .test(name: "Target_2"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - idToPackage: [:], - packageToFolder: ["Package": basePath], - packageToTargetsToArtifactPaths: ["Package": [ - "Target_1": try! - .init(validating: "/artifacts/Package/Target_1.xcframework"), - ]] - ) - - XCTAssertEqual( - preprocessInfo.targetToProducts, - [ - "Target_1": [.init(name: "Product1", type: .library(.automatic), targets: ["Target_1", "Target_2"])], - "Target_2": [.init(name: "Product1", type: .library(.automatic), targets: ["Target_1", "Target_2"])], - ] - ) - XCTAssertEqual( - preprocessInfo.targetToResolvedDependencies, - [ - "Target_1": [], - "Target_2": [], - ] - ) - XCTAssertEqual( - preprocessInfo.productToExternalDependencies, - [ - "Product1": [ - .xcframework(path: "/artifacts/Package/Target_1.xcframework"), - .project(target: "Target_2", path: .relativeToManifest(basePath.pathString)), - ], - ] - ) - } - - func testPreprocess_whenPackageIDDifferentThanName() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_2"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target_2"))) - let preprocessInfo = try subject.preprocess( - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), - ], - targets: [ - .test( - name: "Target_1", - dependencies: [ - .product( - name: "Product2", - package: "Package2_different_name", - moduleAliases: nil, - condition: nil - ), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - "Package2": .init( - products: [ - .init(name: "Product2", type: .library(.automatic), targets: ["Target_2"]), - ], - targets: [ - .test(name: "Target_2"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - idToPackage: ["package2_different_name": "Package2"], - packageToFolder: [ - "Package": basePath.appending(component: "Package"), - "Package2": basePath.appending(component: "Package2"), - ], - packageToTargetsToArtifactPaths: [:] - ) - - XCTAssertEqual( - preprocessInfo.targetToResolvedDependencies, - [ - "Target_1": [.externalTarget(package: "Package2", target: "Target_2", condition: nil)], - "Target_2": [], - ] - ) - } - - func testPreprocess_whenDependencyNameContainsDot_mapsToUnderscoreInTargetName() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_1"))) - try fileHandler - .createFolder(basePath.appending(try RelativePath(validating: "com.example.dep-1/Sources/com.example.dep-1"))) - let preprocessInfo = try subject.preprocess( - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), - ], - targets: [ - .test( - name: "Target_1", - dependencies: [ - .product( - name: "com.example.dep-1", - package: "com.example.dep-1", - moduleAliases: nil, - condition: nil - ), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - "com.example.dep-1": .init( - products: [ - .init(name: "com.example.dep-1", type: .library(.automatic), targets: ["com.example.dep-1"]), - ], - targets: [ - .test(name: "com.example.dep-1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - idToPackage: [:], - packageToFolder: [ - "Package": basePath.appending(component: "Package"), - "com.example.dep-1": basePath.appending(component: "com.example.dep-1"), - ], - packageToTargetsToArtifactPaths: [:] - ) - - XCTAssertEqual( - preprocessInfo.targetToResolvedDependencies, - [ - "Target_1": [ - .externalTarget(package: "com.example.dep-1", target: "com_example_dep-1", condition: nil), - ], - "com.example.dep-1": [], - ] - ) - } - - func testPreprocess_whenTargetDependenciesOnTargetHaveConditions() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency_1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency_2"))) - let preprocessInfo = try subject.preprocess( - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product", type: .library(.automatic), targets: ["Target_1"]), - ], - targets: [ - .test( - name: "Target_1", - dependencies: [ - .byName(name: "Dependency_1", condition: .init(platformNames: ["ios"], config: nil)), - .target(name: "Dependency_2", condition: .init(platformNames: ["tvos"], config: nil)), - ] - ), - .test(name: "Dependency_1"), - .test(name: "Dependency_2"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - idToPackage: [:], - packageToFolder: [ - "Package": basePath.appending(component: "Package"), - ], - packageToTargetsToArtifactPaths: [:] - ) - - XCTAssertEqual( - preprocessInfo.targetToResolvedDependencies, - [ - "Target_1": [ - .target(name: "Dependency_1", condition: .when([.ios])), - .target(name: "Dependency_2", condition: .when([.tvos])), - ], - "Dependency_1": [], - "Dependency_2": [], - ] - ) - } - - func testPreprocess_whenTargetDependenciesOnProductHaveConditions() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package_1/Sources/Target_1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package_2/Sources/Target_2"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package_2/Sources/Target_3"))) - let preprocessInfo = try subject.preprocess( - packageInfos: [ - "Package_1": .init( - products: [ - .init(name: "Product_1", type: .library(.automatic), targets: ["Target_1"]), - ], - targets: [ - .test( - name: "Target_1", - dependencies: [ - .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) - ), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - "Package_2": .init( - products: [ - .init(name: "Product_2", type: .library(.automatic), targets: ["Target_2"]), - .init(name: "Product_3", type: .library(.automatic), targets: ["Target_3"]), - ], - targets: [ - .test(name: "Target_2"), - .test(name: "Target_3"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - idToPackage: [:], - packageToFolder: [ - "Package_1": basePath.appending(component: "Package_1"), - "Package_2": basePath.appending(component: "Package_2"), - ], - packageToTargetsToArtifactPaths: [:] - ) - - XCTAssertEqual( - preprocessInfo.targetToResolvedDependencies, - [ - "Target_2": [], - "Target_3": [], - "Target_1": [ - .externalTarget(package: "Package_2", target: "Target_2", condition: .when([.ios])), - .externalTarget(package: "Package_2", target: "Target_3", condition: .when([.tvos])), - ], - ] - ) - } - - func testMap() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenLegacySwift_usesLegacyIOSVersion() throws { - system.swiftVersionStub = { "5.6.0" } - - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - deploymentTargets: .iOS("9.0") - ), - ] - ) - ) - } - - func testMap_whenMacCatalyst() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [.init(platformName: "maccatalyst", version: "13.0", options: [])], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - platforms: [.macCatalyst] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - destinations: [.macCatalyst], - deploymentTargets: .iOS("13.0") - ), - ] - ) - ) - } - - func testMap_whenAlternativeDefaultSources() throws { - for alternativeDefaultSource in ["Source", "src", "srcs"] { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/\(alternativeDefaultSource)/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSources: .custom(.init( - globs: [ - basePath - .appending(try RelativePath(validating: "Package/\(alternativeDefaultSource)/Target1/**")) - .pathString, - ] - )) - ), - ] - ) - ) - - try fileHandler.delete(sourcesPath) - } - } - - func testMap_whenOnlyBinaries_doesNotCreateProject() throws { - let project = try subject.map( - package: "Package", - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), - ], - targets: [ - .test(name: "Target_1", type: .binary, url: "https://binary.target.com"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - - XCTAssertNil(project) - } - - func testMap_whenNameContainsUnderscores_mapsToDashInBundleID() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target_1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), - ], - targets: [ - .test(name: "Target_1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target_1", basePath: basePath, customBundleID: "Target.1"), - ] - ) - ) - } - - func testMap_whenSettingsDefinesContainsQuotes() throws { - // When having a manifest that includes a GCC definition like `FOO="BAR"`, SPM successfully maintains the quotes - // and it will convert it to a compiler parameter like `-DFOO=\"BAR\"`. - // Xcode configuration, instead, treats the quotes as value assignment, resulting in `-DFOO=BAR`, - // which has a different meaning in GCC macros, building packages incorrectly. - // Tuist needs to escape those definitions for SPM manifests, as SPM is doing, so they can be built the same way. - - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .c, name: .define, condition: nil, value: ["FOO1=\"BAR1\""]), - .init(tool: .cxx, name: .define, condition: nil, value: ["FOO2=\"BAR2\""]), - .init(tool: .cxx, name: .define, condition: nil, value: ["FOO3=3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: [ - "GCC_PREPROCESSOR_DEFINITIONS": [ - // Escaped - "FOO1='\"BAR1\"'", - // Escaped - "FOO2='\"BAR2\"'", - // Not escaped - "FOO3=3", - ], - ] - ), - ] - ) - ) - } - - func testMap_whenNameContainsDot_mapsToUnderscoreInTargetName() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/com.example.target-1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "com.example.product-1", type: .library(.automatic), targets: ["com.example.target-1"]), - ], - targets: [ - .test(name: "com.example.target-1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "com_example_target-1", - basePath: basePath, - customProductName: "com_example_target_1", - customBundleID: "com.example.target-1", - customSources: .custom(.init(globs: [ - basePath - .appending(try RelativePath(validating: "Package/Sources/com.example.target-1/**")).pathString, - ])) - ), - ] - ) - ) - } - - func testMap_whenTargetNotInProduct_ignoresIt() throws { - let basePath = try temporaryPath() - let sourcesPath1 = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let sourcesPath2 = basePath.appending(try RelativePath(validating: "Package/Sources/Target2")) - try fileHandler.createFolder(sourcesPath1) - try fileHandler.createFolder(sourcesPath2) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - .test(name: "Target2"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenTargetIsNotRegular_ignoresTarget() throws { - let basePath = try temporaryPath() - let sourcesPath1 = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let sourcesPath2 = basePath.appending(try RelativePath(validating: "Package/Sources/Target2")) - let sourcesPath3 = basePath.appending(try RelativePath(validating: "Package/Sources/Target3")) - try fileHandler.createFolder(sourcesPath1) - try fileHandler.createFolder(sourcesPath2) - try fileHandler.createFolder(sourcesPath3) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1", "Target2", "Target3"]), - ], - targets: [ - .test(name: "Target1"), - .test(name: "Target2", type: .test), - .test(name: "Target3", type: .binary), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenProductIsNotLibrary_ignoresProduct() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target2"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target3"))) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - .init(name: "Product2", type: .plugin, targets: ["Target2"]), - .init(name: "Product3", type: .test, targets: ["Target3"]), - ], - targets: [ - .test(name: "Target1"), - .test(name: "Target2"), - .test(name: "Target3"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenCustomSources() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1", sources: ["Subfolder", "Another/Subfolder/file.swift"]), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSources: .custom([ - .init( - stringLiteral: - basePath.appending(try RelativePath(validating: "Package/Sources/Target1/Subfolder/**")) - .pathString - ), - .init( - stringLiteral: - basePath - .appending( - try RelativePath(validating: "Package/Sources/Target1/Another/Subfolder/file.swift") - ) - .pathString - ), - ]) - ), - ] - ) - ) - } - - func testMap_whenHasResources() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - // Create resources files and directories - let resource1 = sourcesPath.appending(try RelativePath(validating: "Resource/Folder")) - let resource2 = sourcesPath.appending(try RelativePath(validating: "Another/Resource/Folder")) - let resource3 = sourcesPath.appending(try RelativePath(validating: "AnotherOne/Resource/Folder")) - - try fileHandler.createFolder(resource1) - try fileHandler.createFolder(resource2) - try fileHandler.createFolder(resource3) - - // Project declaration - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - resources: [ - .init(rule: .copy, path: "Resource/Folder"), - .init(rule: .process, path: "Another/Resource/Folder"), - .init(rule: .process, path: "AnotherOne/Resource/Folder"), - ], - exclude: [ - "AnotherOne/Resource", - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSources: .custom(.init(globs: [ - .glob( - Path(basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")).pathString), - excluding: [ - Path( - basePath - .appending( - try RelativePath(validating: "Package/Sources/Target1/AnotherOne/Resource/**") - ) - .pathString - ), - ] - ), - ])), - resources: [ - .folderReference( - path: Path( - basePath.appending(try RelativePath(validating: "Package/Sources/Target1/Resource/Folder")) - .pathString - ), - tags: [] - ), - .glob( - pattern: Path( - basePath - .appending( - try RelativePath(validating: "Package/Sources/Target1/Another/Resource/Folder/**") - ) - .pathString - ), - excluding: [ - Path( - basePath - .appending( - try RelativePath(validating: "Package/Sources/Target1/AnotherOne/Resource/**") - ) - .pathString - ), - ], - tags: [] - ), - .glob( - pattern: Path( - basePath - .appending( - try RelativePath(validating: "Package/Sources/Target1/AnotherOne/Resource/Folder/**") - ) - .pathString - ), - excluding: [ - Path( - basePath - .appending( - try RelativePath(validating: "Package/Sources/Target1/AnotherOne/Resource/**") - ) - .pathString - ), - ], - tags: [] - ), - ] - ), - ] - ) - ) - } - - func testMap_whenHasDefaultResources() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let defaultResourcePath = sourcesPath.appending(try RelativePath(validating: "Resources/file.xib")) - try fileHandler.createFolder(sourcesPath) - fileHandler.stubGlob = { path, glob in - XCTAssertEqual(path, sourcesPath) - if glob == "**/*.xib" { - return [defaultResourcePath] - } else { - return [] - } - } - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - resources: [ - .glob( - pattern: Path(defaultResourcePath.pathString), - excluding: [], - tags: [] - ), - ] - ), - ] - ) - ) - } - - func testMap_whenHasHeadersWithCustomModuleMap() throws { - let basePath = try temporaryPath() - let headersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) - let moduleMapPath = headersPath.appending(component: "module.modulemap") - let topHeaderPath = headersPath.appending(component: "AnHeader.h") - let nestedHeaderPath = headersPath.appending(component: "Subfolder").appending(component: "AnotherHeader.h") - try fileHandler.createFolder(headersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: moduleMapPath, atomically: true) - try fileHandler.write("", path: topHeaderPath, atomically: true) - try fileHandler.write("", path: nestedHeaderPath, atomically: true) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1" - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: [ - "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/include"], - ], - moduleMap: "$(SRCROOT)/Sources/Target1/include/module.modulemap" - ), - ] - ) - ) - } - - func testMap_whenHasSystemLibrary() throws { - let basePath = try temporaryPath() - let targetPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let moduleMapPath = targetPath.appending(component: "module.modulemap") - try fileHandler.createFolder(targetPath) - try fileHandler.write("", path: moduleMapPath, atomically: true) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - type: .system - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - platforms: [.iOS] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSources: .custom(nil), - moduleMap: "$(SRCROOT)/Sources/Target1/module.modulemap" - ), - ] - ) - ) - } - - func testMap_errorWhenSystemLibraryHasMissingModuleMap() throws { - let basePath = try temporaryPath() - let targetPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let moduleMapPath = targetPath.appending(component: "module.modulemap") - try fileHandler.createFolder(targetPath) - - let error = PackageInfoMapperError.modulemapMissing( - moduleMapPath: moduleMapPath.pathString, - package: "Package", - target: "Target1" - ) - - XCTAssertThrowsSpecific(try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - type: .system - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ), error) - } - - func testMap_whenHasHeadersWithUmbrellaHeader() throws { - let basePath = try temporaryPath() - let headersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) - let topHeaderPath = headersPath.appending(component: "Target1.h") - let nestedHeaderPath = headersPath.appending(component: "Subfolder").appending(component: "AnHeader.h") - try fileHandler.createFolder(headersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: topHeaderPath, atomically: true) - try fileHandler.write("", path: nestedHeaderPath, atomically: true) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1" - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - headers: .headers(public: [nestedHeaderPath.pathString, topHeaderPath.pathString]), - customSettings: [ - "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/include"], - ] - ), - ] - ) - ) - } - - func testMap_whenDependenciesHaveHeaders() throws { - let basePath = try temporaryPath() - let target1HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) - let target1ModuleMapPath = target1HeadersPath.appending(component: "module.modulemap") - let dependency1HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1/include")) - let dependency1ModuleMapPath = dependency1HeadersPath.appending(component: "module.modulemap") - let dependency2HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency2/include")) - let dependency2ModuleMapPath = dependency2HeadersPath.appending(component: "module.modulemap") - try fileHandler.createFolder(target1HeadersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: target1ModuleMapPath, atomically: true) - try fileHandler.createFolder(dependency1HeadersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: dependency1ModuleMapPath, atomically: true) - try fileHandler.createFolder(dependency2HeadersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: dependency2ModuleMapPath, atomically: true) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.target(name: "Dependency1", condition: nil)] - ), - .test( - name: "Dependency1", - dependencies: [.target(name: "Dependency2", condition: nil)] - ), - .test(name: "Dependency2"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [.target(name: "Dependency1")], - customSettings: [ - "HEADER_SEARCH_PATHS": [ - "$(SRCROOT)/Sources/Target1/include", - "$(SRCROOT)/Sources/Dependency1/include", - "$(SRCROOT)/Sources/Dependency2/include", - ], - ], - moduleMap: "$(SRCROOT)/Sources/Target1/include/module.modulemap" - ), - .test( - "Dependency1", - basePath: basePath, - dependencies: [.target(name: "Dependency2")], - customSettings: [ - "HEADER_SEARCH_PATHS": [ - "$(SRCROOT)/Sources/Dependency1/include", - "$(SRCROOT)/Sources/Dependency2/include", - ], - ], - moduleMap: "$(SRCROOT)/Sources/Dependency1/include/module.modulemap" - ), - .test( - "Dependency2", - basePath: basePath, - customSettings: [ - "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Dependency2/include"], - ], - moduleMap: "$(SRCROOT)/Sources/Dependency2/include/module.modulemap" - ), - ] - ) - ) - } - - func testMap_whenExternalDependenciesHaveHeaders() throws { - let basePath = try temporaryPath() - let target1HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) - let target1ModuleMapPath = target1HeadersPath.appending(component: "module.modulemap") - let dependency1HeadersPath = basePath.appending(try RelativePath(validating: "Package2/Sources/Dependency1/include")) - let dependency1ModuleMapPath = dependency1HeadersPath.appending(component: "module.modulemap") - let dependency2HeadersPath = basePath.appending(try RelativePath(validating: "Package3/Sources/Dependency2/include")) - let dependency2ModuleMapPath = dependency2HeadersPath.appending(component: "module.modulemap") - try fileHandler.createFolder(target1HeadersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: target1ModuleMapPath, atomically: true) - try fileHandler.createFolder(dependency1HeadersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: dependency1ModuleMapPath, atomically: true) - try fileHandler.createFolder(dependency2HeadersPath.appending(component: "Subfolder")) - try fileHandler.write("", path: dependency2ModuleMapPath, atomically: true) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.product(name: "Dependency1", package: "Package2", moduleAliases: nil, condition: nil)] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - "Package2": .init( - products: [ - .init(name: "Dependency1", type: .library(.automatic), targets: ["Dependency1"]), - ], - targets: [ - .test( - name: "Dependency1", - dependencies: [.product(name: "Dependency2", package: "Package3", moduleAliases: nil, condition: nil)] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - "Package3": .init( - products: [ - .init(name: "Dependency2", type: .library(.automatic), targets: ["Dependency2"]), - ], - targets: [ - .test( - name: "Dependency2" - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [.project(target: "Dependency1", path: Path("\(basePath.pathString)/Package2"))], - customSettings: [ - "HEADER_SEARCH_PATHS": [ - "$(SRCROOT)/Sources/Target1/include", - "$(SRCROOT)/../Package2/Sources/Dependency1/include", - "$(SRCROOT)/../Package3/Sources/Dependency2/include", - ], - ], - moduleMap: "$(SRCROOT)/Sources/Target1/include/module.modulemap" - ), - ] - ) - ) - } - - func testMap_whenCustomPath() throws { - let basePath = try temporaryPath() - - // Create resources files and directories - let headersPath = basePath.appending(try RelativePath(validating: "Package/Custom/Headers")) - let headerPath = headersPath.appending(component: "module.h") - let moduleMapPath = headersPath.appending(component: "module.modulemap") - try fileHandler.createFolder(headersPath) - try fileHandler.write("", path: headerPath, atomically: true) - try fileHandler.write("", path: moduleMapPath, atomically: true) - - let resourceFolderPathCustomTarget = basePath.appending(try RelativePath(validating: "Package/Custom/Resource/Folder")) - try fileHandler.createFolder(resourceFolderPathCustomTarget) - - // Project declaration - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - path: "Custom", - sources: ["Sources/Folder"], - resources: [.init(rule: .copy, path: "Resource/Folder")], - publicHeadersPath: "Headers" - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSources: .custom(.init(globs: [ - basePath - .appending(try RelativePath(validating: "Package/Custom/Sources/Folder/**")).pathString, - ])), - resources: [ - .folderReference( - path: Path( - basePath.appending(try RelativePath(validating: "Package/Custom/Resource/Folder")) - .pathString - ), - tags: [] - ), - ], - customSettings: [ - "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Custom/Headers"], - ], - moduleMap: "$(SRCROOT)/Custom/Headers/module.modulemap" - ), - ] - ) - ) - } - - func testMap_whenDependencyHasHeaders_addsThemToHeaderSearchPath() throws { - let basePath = try temporaryPath() - let dependencyHeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1/include")) - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(dependencyHeadersPath) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.target(name: "Dependency1", condition: nil)] - ), - .test(name: "Dependency1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [.target(name: "Dependency1")], - customSettings: [ - "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Dependency1/include"], - ] - ), - .test( - "Dependency1", - basePath: basePath, - customSettings: [ - "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Dependency1/include"], - ], - moduleMap: "$(SRCROOT)/Sources/Dependency1/include/Dependency1.modulemap" - ), - ] - ) - ) - } - - func testMap_whenMultipleAvailable_takesMultiple() throws { - // Given - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - // When - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - platforms: [.iOS, .tvOS] - ) - - // Then - let expected: ProjectDescription.Project = .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - destinations: [.iPad, .iPhone, .macWithiPadDesign, .appleVisionWithiPadDesign, .appleTv], - customProductName: "Target1", - customBundleID: "Target1", - deploymentTargets: .init(iOS: "11.0", tvOS: "11.0"), - customSources: .custom(.init(globs: [ - basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")) - .pathString, - ])) - ), - ] - ) - - XCTAssertEqual(project?.name, expected.name) - - // Need to sort targets of projects, because internally a set is used to generate targets for different platforms - // That could lead to mixed orders - let projectTargets = project?.targets.sorted(by: \.name) - let expectedTargets = expected.targets.sorted(by: \.name) - XCTAssertEqual(projectTargets, expectedTargets) - } - - func testMap_whenIOSNotAvailable_takesOthers() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - platforms: [.tvOS] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - - basePath: basePath, - destinations: [.appleTv], - deploymentTargets: .tvOS("11.0") - ), - ] - ) - ) - } - - func testMap_whenPackageDefinesPlatform_configuresDeploymentTarget() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [.init(platformName: "ios", version: "13.0", options: [])], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - platforms: [.iOS] - ) - - let other = ProjectDescription.Project.testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - deploymentTargets: .iOS("13.0") - ), - ] - ) - - dump(project?.targets.first?.destinations) - dump(other.targets.first?.destinations) - - XCTAssertEqual( - project, - other - ) - } - - func testMap_whenSettingsContainsCHeaderSearchPath_mapsToHeaderSearchPathsSetting() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [.init(tool: .c, name: .headerSearchPath, condition: nil, value: ["value"])] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: ["HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/value"]] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsCXXHeaderSearchPath_mapsToHeaderSearchPathsSetting() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [.init(tool: .cxx, name: .headerSearchPath, condition: nil, value: ["value"])] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: ["HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/value"]] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsCDefine_mapsToGccPreprocessorDefinitions() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .c, name: .define, condition: nil, value: ["key1"]), - .init(tool: .c, name: .define, condition: nil, value: ["key2=value"]), - .init(tool: .c, name: .define, condition: nil, value: ["key3="]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: ["GCC_PREPROCESSOR_DEFINITIONS": ["key1=1", "key2=value", "key3="]] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsCXXDefine_mapsToGccPreprocessorDefinitions() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .cxx, name: .define, condition: nil, value: ["key1"]), - .init(tool: .cxx, name: .define, condition: nil, value: ["key2=value"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: ["GCC_PREPROCESSOR_DEFINITIONS": ["key1=1", "key2=value"]] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsSwiftDefine_mapsToSwiftActiveCompilationConditions() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .swift, name: .define, condition: nil, value: ["key"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, customSettings: ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "key"]), - ] - ) - ) - } - - func testMap_whenSettingsContainsCUnsafeFlags_mapsToOtherCFlags() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .c, name: .unsafeFlags, condition: nil, value: ["key1"]), - .init(tool: .c, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, customSettings: ["OTHER_CFLAGS": ["key1", "key2", "key3"]]), - ] - ) - ) - } - - func testMap_whenSettingsContainsCXXUnsafeFlags_mapsToOtherCPlusPlusFlags() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .cxx, name: .unsafeFlags, condition: nil, value: ["key1"]), - .init(tool: .cxx, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, customSettings: ["OTHER_CPLUSPLUSFLAGS": ["key1", "key2", "key3"]]), - ] - ) - ) - } - - func testMap_whenSettingsContainsSwiftUnsafeFlags_mapsToOtherSwiftFlags() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .swift, name: .unsafeFlags, condition: nil, value: ["key1"]), - .init(tool: .swift, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, customSettings: ["OTHER_SWIFT_FLAGS": ["key1", "key2", "key3"]]), - ] - ) - ) - } - - func testMap_whenSettingsContainsEnableUpcomingFeature_mapsToOtherSwiftFlags() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .swift, name: .enableUpcomingFeature, condition: nil, value: ["Foo"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, customSettings: ["OTHER_SWIFT_FLAGS": ["-enable-upcoming-feature Foo"]]), - ] - ) - ) - } - - func testMap_whenSettingsContainsEnableExperimentalFeature_mapsToOtherSwiftFlags() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .swift, name: .enableExperimentalFeature, condition: nil, value: ["Foo"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: ["OTHER_SWIFT_FLAGS": ["-enable-experimental-feature Foo"]] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsLinkerUnsafeFlags_mapsToOtherLdFlags() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key1"]), - .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, customSettings: ["OTHER_LDFLAGS": ["key1", "key2", "key3"]]), - ] - ) - ) - } - - func testMap_whenConfigurationContainsBaseSettingsDictionary_usesBaseSettings() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key1"]), - .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - baseSettings: .init( - configurations: [ - .init(name: "Debug", variant: .debug): .init( - settings: ["CUSTOM_SETTING_1": .string("CUSTOM_VALUE_1")], - xcconfig: sourcesPath.appending(component: "Config.xcconfig") - ), - .init(name: "Release", variant: .release): .init( - settings: ["CUSTOM_SETTING_2": .string("CUSTOM_VALUE_2")], - xcconfig: sourcesPath.appending(component: "Config.xcconfig") - ), - ] - ) - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - baseSettings: .settings( - configurations: [ - .debug( - name: "Debug", - settings: ["CUSTOM_SETTING_1": .string("CUSTOM_VALUE_1")], - xcconfig: "Sources/Target1/Config.xcconfig" - ), - .release( - name: "Release", - settings: ["CUSTOM_SETTING_2": .string("CUSTOM_VALUE_2")], - xcconfig: "Sources/Target1/Config.xcconfig" - ), - ], - defaultSettings: .recommended - ), - customSettings: [ - "OTHER_LDFLAGS": ["key1", "key2", "key3"], - ] - ), - ] - ) - ) - } - - func testMap_whenConfigurationContainsTargetSettingsDictionary_mapsToCustomSettings() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let customSettings: TuistGraph.SettingsDictionary = ["CUSTOM_SETTING": .string("CUSTOM_VALUE")] - - let targetSettings = ["Target1": customSettings] - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key1"]), - .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - targetSettings: targetSettings - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: [ - "OTHER_LDFLAGS": ["key1", "key2", "key3"], - "CUSTOM_SETTING": "CUSTOM_VALUE", - ] - ), - ] - ) - ) - } - - func testMap_whenConditionalSetting_ignoresByPlatform() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init( - tool: .c, - name: .headerSearchPath, - condition: .init(platformNames: ["tvos"], config: nil), - value: ["value"] - ), - .init( - tool: .c, - name: .headerSearchPath, - condition: nil, - value: ["otherValue"] - ), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - customSettings: ["HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/otherValue"]] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsLinkedFramework_mapsToSDKDependency() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .linker, name: .linkedFramework, condition: nil, value: ["Framework"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [.sdk(name: "Framework", type: .framework, status: .required)] - ), - ] - ) - ) - } - - func testMap_whenSettingsContainsLinkedLibrary_mapsToSDKDependency() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - settings: [ - .init(tool: .linker, name: .linkedLibrary, condition: nil, value: ["Library"]), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [.sdk(name: "Library", type: .library, status: .required)] - ), - ] - ) - ) - } - - func testMap_whenTargetDependency_mapsToTargetDependency() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let dependenciesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1")) - try fileHandler.createFolder(sourcesPath) - try fileHandler.createFolder(dependenciesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.target(name: "Dependency1", condition: nil)] - ), - .test(name: "Dependency1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, dependencies: [.target(name: "Dependency1")]), - .test("Dependency1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenBinaryTargetDependency_mapsToXcframework() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let dependenciesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1")) - try fileHandler.createFolder(sourcesPath) - try fileHandler.createFolder(dependenciesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.target(name: "Dependency1", condition: nil)] - ), - .test(name: "Dependency1", type: .binary), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [ - .xcframework(path: Path( - basePath.appending(try RelativePath(validating: "artifacts/Package/Dependency1.xcframework")) - .pathString - )), - ] - ), - ] - ) - ) - } - - func testMap_whenTargetByNameDependency_mapsToTargetDependency() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - let dependenciesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1")) - try fileHandler.createFolder(sourcesPath) - try fileHandler.createFolder(dependenciesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.byName(name: "Dependency1", condition: nil)] - ), - .test(name: "Dependency1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("Target1", basePath: basePath, dependencies: [.target(name: "Dependency1")]), - .test("Dependency1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenBinaryTargetURLByNameDependency_mapsToXcFramework() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [ - .byName(name: "Dependency1", condition: nil), - ] - ), - .test(name: "Dependency1", type: .binary, url: "someURL"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [ - .xcframework(path: Path( - basePath.appending(try RelativePath(validating: "artifacts/Package/Dependency1.xcframework")) - .pathString - )), - ] - ), - ] - ) - ) - } - - func testMap_whenBinaryTargetPathByNameDependency_mapsToXcFramework() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [ - .byName(name: "Dependency1", condition: nil), - ] - ), - .test(name: "Dependency1", type: .binary, path: "Dependency1/Dependency1.xcframework"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [.xcframework(path: Path( - basePath - .appending(try RelativePath(validating: "Package/Dependency1/Dependency1.xcframework")).pathString - ))] - ), - ] - ) - ) - } - - func testMap_whenExternalProductDependency_mapsToProjectDependencies() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target2"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target3"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target4"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1"))) - - let package1 = PackageInfo( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.product(name: "Product2", package: "Package2", moduleAliases: nil, condition: nil)] - ), - .test(name: "Dependency1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ) - let package2 = PackageInfo( - products: [ - .init(name: "Product2", type: .library(.automatic), targets: ["Target2", "Target3"]), - .init(name: "Product3", type: .library(.automatic), targets: ["Target4"]), - ], - targets: [ - .test(name: "Target2"), - .test(name: "Target3"), - .test(name: "Target4"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: ["Package": package1, "Package2": package2] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [ - .project( - target: "Target2", - path: Path(basePath.appending(try RelativePath(validating: "Package2")).pathString) - ), - .project( - target: "Target3", - path: Path(basePath.appending(try RelativePath(validating: "Package2")).pathString) - ), - ] - ), - ] - ) - ) - } - - func testMap_whenExternalByNameProductDependency_mapsToProjectDependencies() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target2"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target3"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target4"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1"))) - - let package1 = PackageInfo( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [.byName(name: "Product2", condition: nil)] - ), - .test(name: "Dependency1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ) - let package2 = PackageInfo( - products: [ - .init(name: "Product2", type: .library(.automatic), targets: ["Target2", "Target3"]), - .init(name: "Product3", type: .library(.automatic), targets: ["Target4"]), - ], - targets: [ - .test(name: "Target2"), - .test(name: "Target3"), - .test(name: "Target4"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: ["Package": package1, "Package2": package2] - ) - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - dependencies: [ - .project( - target: "Target2", - path: Path(basePath.appending(try RelativePath(validating: "Package2")).pathString) - ), - .project( - target: "Target3", - path: Path(basePath.appending(try RelativePath(validating: "Package2")).pathString) - ), - ] - ), - ] - ) - ) - } - - func testMap_whenCustomCVersion_mapsToGccCLanguageStandardSetting() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: "c99", - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .test( - name: "Package", - settings: .settings(base: ["GCC_C_LANGUAGE_STANDARD": "c99"]), - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenCustomCXXVersion_mapsToClangCxxLanguageStandardSetting() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: "gnu++14", - swiftLanguageVersions: nil - ), - ] - ) - XCTAssertEqual( - project, - .test( - name: "Package", - settings: .settings(base: ["CLANG_CXX_LANGUAGE_STANDARD": "gnu++14"]), - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenCustomSwiftVersion_mapsToSwiftVersionSetting() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: ["4.0.0"] - ), - ] - ) - XCTAssertEqual( - project, - .test( - name: "Package", - settings: .settings(base: ["SWIFT_VERSION": "4.0.0"]), - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenMultipleCustomSwiftVersions_mapsLargestToSwiftVersionSetting() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: ["4.0.0", "5.0.0", "4.2.0"] - ), - ] - ) - XCTAssertEqual( - project, - .test( - name: "Package", - settings: .settings(base: ["SWIFT_VERSION": "5.0.0"]), - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenMultipleCustomSwiftVersionsAndConfiguredVersion_mapsLargestToSwiftVersionLowerThanConfigured() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: ["4.0.0", "5.0.0", "4.2.0"] - ), - ], - swiftToolsVersion: "4.4.0" - ) - XCTAssertEqual( - project, - .test( - name: "Package", - settings: .settings(base: ["SWIFT_VERSION": "4.2.0"]), - targets: [ - .test("Target1", basePath: basePath), - ] - ) - ) - } - - func testMap_whenDependenciesContainsCustomConfiguration_mapsToProjectWithCustomConfig() throws { - let basePath = try temporaryPath() - let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) - try fileHandler.createFolder(sourcesPath) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test(name: "Target1"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - baseSettings: Settings( - configurations: [.release: nil, .debug: nil, .init(name: "Custom", variant: .release): nil], - defaultSettings: .recommended - ), - swiftToolsVersion: "4.4.0" - ) - - XCTAssertNotNil(project?.settings?.configurations.first(where: { $0.name == "Custom" })) - } - - func testMap_whenTargetsWithDefaultHardcodedMapping() throws { - let basePath = try temporaryPath() - let testTargets = [ - "Nimble", - "Quick", - "RxTest", - "RxTest-Dynamic", - "SnapshotTesting", - "TempuraTesting", - "TSCTestSupport", - "ViewInspector", - "XCTVapor", - ] - let allTargets = ["RxSwift"] + testTargets - try allTargets - .map { basePath.appending(try RelativePath(validating: "Package/Sources/\($0)")) } - .forEach { try fileHandler.createFolder($0) } - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: allTargets), - ], - targets: allTargets.map { .test(name: $0) }, - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - targetSettings: [ - "Nimble": ["ENABLE_TESTING_SEARCH_PATHS": "NO", "ANOTHER_SETTING": "YES"], - "Quick": ["ANOTHER_SETTING": "YES"], - ] - ) - - XCTAssertEqual( - project, - .testWithDefaultConfigs( - name: "Package", - targets: [ - .test("RxSwift", basePath: basePath, product: .framework), - ] + testTargets.map { - let customSettings: ProjectDescription.SettingsDictionary - var customProductName: String? - switch $0 { - case "Nimble": - customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "NO", "ANOTHER_SETTING": "YES"] - case "Quick": - customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "YES", "ANOTHER_SETTING": "YES"] - case "RxTest-Dynamic": // because RxTest does have an "-" we need to account for the custom mapping to product - // names - customProductName = "RxTest_Dynamic" - customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "YES"] - default: - customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "YES"] - } - - return .test( - $0, - basePath: basePath, - customProductName: customProductName, - customSettings: customSettings - ) - } - ) - ) - } - - func testMap_whenTargetDependenciesOnTargetHaveConditions() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency2"))) - - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: [ - "Package": .init( - products: [ - .init(name: "Product", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [ - .target(name: "Dependency1", condition: .init(platformNames: ["ios"], config: nil)), - .target(name: "Dependency2", condition: .init(platformNames: ["tvos"], config: nil)), - ] - ), - .test(name: "Dependency1"), - .test(name: "Dependency2"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ), - ], - platforms: [.iOS, .tvOS] - ) - - let expected: ProjectDescription.Project = .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign, .appleTv], - customProductName: "Target1", - customBundleID: "Target1", - deploymentTargets: .init(iOS: "11.0", tvOS: "11.0"), - customSources: .custom(.init(globs: [ - basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")).pathString, - ])), - dependencies: [ - .target(name: "Dependency1", condition: .when([.ios])), - .target(name: "Dependency2", condition: .when([.tvos])), - ] - ), - .test( - "Dependency1", - basePath: basePath, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign, .appleTv], - customProductName: "Dependency1", - customBundleID: "Dependency1", - deploymentTargets: .init(iOS: "11.0", tvOS: "11.0"), - customSources: .custom(.init(globs: [ - basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1/**")).pathString, - ])) - ), - .test( - "Dependency2", - basePath: basePath, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign, .appleTv], - customProductName: "Dependency2", - customBundleID: "Dependency2", - deploymentTargets: .init(iOS: "11.0", tvOS: "11.0"), - customSources: .custom(.init(globs: [ - basePath.appending(try RelativePath(validating: "Package/Sources/Dependency2/**")).pathString, - ])) - ), - ] - ) - - XCTAssertEqual(project?.name, expected.name) - - let projectTargets = project!.targets.sorted(by: \.name) - let expectedTargets = expected.targets.sorted(by: \.name) - XCTAssertEqual(projectTargets, expectedTargets) - } - - func testMap_whenTargetDependenciesOnProductHaveConditions() throws { - let basePath = try temporaryPath() - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target2"))) - try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target3"))) - - let package1 = PackageInfo( - products: [ - .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), - ], - targets: [ - .test( - name: "Target1", - dependencies: [ - .product( - name: "Product2", - package: "Package2", - moduleAliases: nil, - condition: .init(platformNames: ["ios"], config: nil) - ), - ] - ), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ) - let package2 = PackageInfo( - products: [ - .init(name: "Product2", type: .library(.automatic), targets: ["Target2", "Target3"]), - ], - targets: [ - .test(name: "Target2"), - .test(name: "Target3"), - ], - platforms: [], - cLanguageStandard: nil, - cxxLanguageStandard: nil, - swiftLanguageVersions: nil - ) - let project = try subject.map( - package: "Package", - basePath: basePath, - packageInfos: ["Package": package1, "Package2": package2], - platforms: [.iOS, .tvOS] - ) - - let expected: ProjectDescription.Project = .testWithDefaultConfigs( - name: "Package", - targets: [ - .test( - "Target1", - basePath: basePath, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign, .appleTv], - customProductName: "Target1", - customBundleID: "Target1", - deploymentTargets: .init(iOS: "11.0", tvOS: "11.0"), - customSources: .custom(.init(globs: [ - basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")).pathString, - ])), - dependencies: [ - .project( - target: "Target2", - path: Path(basePath.appending(try RelativePath(validating: "Package2")).pathString), - condition: .when([.ios]) - ), - .project( - target: "Target3", - path: Path(basePath.appending(try RelativePath(validating: "Package2")).pathString), - condition: .when([.ios]) - ), - ] - ), - ] - ) - - XCTAssertEqual(project?.name, expected.name) - - let projectTargets = project!.targets.sorted(by: \.name) - let expectedTargets = expected.targets.sorted(by: \.name) - - XCTAssertEqual( - projectTargets, - expectedTargets - ) - } -} - -private func defaultSpmResources(_ target: String, customPath: String? = nil) -> ResourceFileElements { - let fullPath: String - if let customPath { - fullPath = customPath - } else { - fullPath = "/Package/Sources/\(target)" - } - return [ - "\(fullPath)/**/*.xib", - "\(fullPath)/**/*.storyboard", - "\(fullPath)/**/*.xcdatamodeld", - "\(fullPath)/**/*.xcmappingmodel", - "\(fullPath)/**/*.xcassets", - "\(fullPath)/**/*.lproj", - ] -} - -extension PackageInfoMapping { - fileprivate func map( - package: String, - basePath: AbsolutePath = "/", - packageInfos: [String: PackageInfo] = [:], - platforms: Set = [.iOS], - baseSettings: TuistGraph.Settings = .default, - targetSettings: [String: TuistGraph.SettingsDictionary] = [:], - swiftToolsVersion: TSCUtility.Version? = nil, - projectOptions: TuistGraph.Project.Options? = nil - ) throws -> ProjectDescription.Project? { - let packageToFolder: [String: AbsolutePath] = packageInfos.keys.reduce(into: [:]) { result, packageName in - result[packageName] = basePath.appending(component: packageName) - } - let packageToTargetsToArtifactPaths: [String: [String: AbsolutePath]] = try packageInfos - .reduce(into: [:]) { packagesResult, element in - let (packageName, packageInfo) = element - packagesResult[packageName] = try packageInfo.targets - .reduce(into: [String: AbsolutePath]()) { targetsResult, target in - guard target.type == .binary, target.path == nil else { - return - } - targetsResult[target.name] = basePath.appending( - try RelativePath(validating: "artifacts/\(packageName)/\(target.name).xcframework") - ) - } - } - - let preprocessInfo = try preprocess( - packageInfos: packageInfos, - idToPackage: [:], - packageToFolder: packageToFolder, - packageToTargetsToArtifactPaths: packageToTargetsToArtifactPaths - ) - - let destinations: ProjectDescription.Destinations = Set(platforms.flatMap { platform -> ProjectDescription.Destinations in - switch platform { - case .iOS: - [.iPhone, .iPad, .appleVisionWithiPadDesign, .macWithiPadDesign] - case .macCatalyst: - [.macCatalyst] - case .macOS: - [.mac] - case .tvOS: - [.appleTv] - case .watchOS: - [.appleWatch] - case .visionOS: - [.appleVision] - } - }) - - return try map( - packageInfo: packageInfos[package]!, - packageInfos: packageInfos, - name: package, - path: basePath.appending(component: package), - productTypes: [:], - baseSettings: baseSettings, - targetSettings: targetSettings, - projectOptions: projectOptions, - minDeploymentTargets: preprocessInfo.platformToMinDeploymentTarget, - destinations: destinations, - targetToProducts: preprocessInfo.targetToProducts, - targetToResolvedDependencies: preprocessInfo.targetToResolvedDependencies, - macroDependencies: preprocessInfo.macroDependencies, - targetToModuleMap: preprocessInfo.targetToModuleMap, - packageToProject: Dictionary(uniqueKeysWithValues: packageInfos.keys.map { - ($0, basePath.appending(component: $0)) - }), - swiftToolsVersion: swiftToolsVersion - ) - } -} - -extension PackageInfo.Target { - fileprivate static func test( - name: String, - type: PackageInfo.Target.TargetType = .regular, - path: String? = nil, - url: String? = nil, - sources: [String]? = nil, - resources: [PackageInfo.Target.Resource] = [], - exclude: [String] = [], - dependencies: [PackageInfo.Target.Dependency] = [], - publicHeadersPath: String? = nil, - settings: [TargetBuildSettingDescription.Setting] = [] - ) -> Self { - .init( - name: name, - path: path, - url: url, - sources: sources, - resources: resources, - exclude: exclude, - dependencies: dependencies, - publicHeadersPath: publicHeadersPath, - type: type, - settings: settings, - checksum: nil - ) - } -} - -extension ProjectDescription.Project { - fileprivate static func test( - name: String, - settings: ProjectDescription.Settings? = nil, - targets: [ProjectDescription.Target] - ) -> Self { - .init( - name: name, - options: .options( - automaticSchemesOptions: .disabled, - disableBundleAccessors: false, - disableSynthesizedResourceAccessors: true, - textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) - ), - settings: settings, - targets: targets, - resourceSynthesizers: .default - ) - } - - fileprivate static func testWithDefaultConfigs( - name: String, - targets: [ProjectDescription.Target] - ) -> Self { - Project.test( - name: name, - settings: .settings(configurations: [ - .debug(name: .debug), - .release(name: .release), - ]), - targets: targets - ) - } -} - -extension ProjectDescription.Target { - fileprivate enum SourceFilesListType { - case `default` - case custom(SourceFilesList?) - } - - fileprivate static func test( - _ name: String, - basePath: AbsolutePath = "/", - destinations: ProjectDescription.Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign], - product: ProjectDescription.Product = .staticFramework, - customProductName: String? = nil, - customBundleID: String? = nil, - deploymentTargets: ProjectDescription.DeploymentTargets = DeploymentTargets(iOS: "11.0"), - customSources: SourceFilesListType = .default, - resources: [ProjectDescription.ResourceFileElement] = [], - headers: ProjectDescription.Headers? = nil, - dependencies: [ProjectDescription.TargetDependency] = [], - baseSettings: ProjectDescription.Settings = .settings(), - customSettings: ProjectDescription.SettingsDictionary = [:], - moduleMap: String? = nil - ) -> Self { - let sources: SourceFilesList? - - switch customSources { - case let .custom(list): - sources = list - case .default: - // swiftlint:disable:next force_try - sources = .init(globs: [basePath.appending(try! RelativePath(validating: "Package/Sources/\(name)/**")).pathString]) - } - - return ProjectDescription.Target( - name: name, - destinations: destinations, - product: product, - productName: customProductName ?? name, - bundleId: customBundleID ?? name, - deploymentTargets: deploymentTargets, - infoPlist: .default, - sources: sources, - resources: resources.isEmpty ? nil : ResourceFileElements(resources: resources), - headers: headers, - dependencies: dependencies, - settings: DependenciesGraph.spmSettings(baseSettings: baseSettings, with: customSettings, moduleMap: moduleMap) - ) - } -} - -extension [ProjectDescription.ResourceFileElement] { - static func defaultResources( - path: AbsolutePath, - excluding: [Path] = [] - ) -> Self { - ["xib", "storyboard", "xcdatamodeld", "xcmappingmodel", "xcassets", "lproj"] - .map { file -> ProjectDescription.ResourceFileElement in - ResourceFileElement.glob( - pattern: Path("\(path.appending(component: "**").pathString)/*.\(file)"), - excluding: excluding - ) - } - } -} - -extension Sequence { - func sorted(by keyPath: KeyPath) -> [Element] { - sorted { a, b in - a[keyPath: keyPath] < b[keyPath: keyPath] - } - } -} diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift deleted file mode 100644 index f05c3ac5bd7..00000000000 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift +++ /dev/null @@ -1,520 +0,0 @@ -import ProjectDescription -import TSCBasic -import TuistCore -import TuistDependencies -import TuistGraph -import TuistSupport -import XCTest -@testable import TuistDependenciesTesting -@testable import TuistLoaderTesting -@testable import TuistSupportTesting - -class SwiftPackageManagerGraphGeneratorTests: TuistUnitTestCase { - private var swiftPackageManagerController: MockSwiftPackageManagerController! - private var subject: SwiftPackageManagerGraphGenerator! - private var path: AbsolutePath { try! temporaryPath() } - private var spmFolder: Path { Path(path.pathString) } - private var checkoutsPath: AbsolutePath { path.appending(component: "checkouts") } - - override func setUp() { - super.setUp() - - swiftPackageManagerController = MockSwiftPackageManagerController() - system.swiftVersionStub = { "5.7.0" } - subject = SwiftPackageManagerGraphGenerator(swiftPackageManagerController: swiftPackageManagerController) - } - - override func tearDown() { - swiftPackageManagerController = nil - subject = nil - super.tearDown() - } - - func test_generate_alamofire_spm_pre_v5_6() throws { - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "alamofire", - "kind": "remote", - "name": "Alamofire", - "path": "https://github.com/Alamofire/Alamofire" - }, - "subpath": "Alamofire" - } - ] - """, - loadPackageInfoStub: { packagePath in - XCTAssertEqual(packagePath, self.path.appending(component: "checkouts").appending(component: "Alamofire")) - return PackageInfo.alamofire - }, - dependenciesGraph: DependenciesGraph.alamofire(spmFolder: spmFolder) - ) - } - - func test_generate_alamofire_spm_v5_6() throws { - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "alamofire", - "kind": "remoteSourceControl", - "name": "Alamofire", - "path": "https://github.com/Alamofire/Alamofire" - }, - "subpath": "Alamofire" - } - ] - """, - loadPackageInfoStub: { packagePath in - XCTAssertEqual(packagePath, self.path.appending(component: "checkouts").appending(component: "Alamofire")) - return PackageInfo.alamofire - }, - dependenciesGraph: DependenciesGraph.alamofire(spmFolder: spmFolder) - ) - } - - func test_generate_google_measurement() throws { - try fileHandler.createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/nanopb/Sources/nanopb")) - try fileHandler - .createFolder( - try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/GoogleUtilities/Sources/GULAppDelegateSwizzler") - ) - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/GoogleUtilities/Sources/GULNSData")) - try fileHandler - .createFolder( - try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/GoogleUtilities/Sources/GULMethodSwizzler") - ) - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/GoogleUtilities/Sources/GULNetwork")) - - // swiftformat:disable wrap - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "googleappmeasurement", - "kind": "remote", - "name": "GoogleAppMeasurement", - "path": "https://github.com/google/GoogleAppMeasurement" - }, - "subpath": "GoogleAppMeasurement" - }, - { - "packageRef": { - "identity" : "googleutilities", - "kind": "remote", - "name": "GoogleUtilities", - "path": "https://github.com/google/GoogleUtilities" - }, - "subpath": "GoogleUtilities" - }, - { - "packageRef": { - "identity" : "nanopb", - "kind": "remote", - "name": "nanopb", - "path": "https://github.com/nanopb/nanopb" - }, - "subpath": "nanopb" - } - ] - """, - workspaceArtifactsJSON: """ - [ - { - "packageRef" : { - "identity" : "googleappmeasurement", - "kind" : "remote", - "path" : "https://github.com/google/GoogleAppMeasurement", - "name" : "GoogleAppMeasurement" - }, - "path" : "\(spmFolder.pathString)/artifacts/GoogleAppMeasurement/GoogleAppMeasurement.xcframework", - "targetName" : "GoogleAppMeasurement" - }, - { - "packageRef" : { - "identity" : "googleappmeasurement", - "kind" : "remote", - "path" : "https://github.com/google/GoogleAppMeasurement", - "name" : "GoogleAppMeasurement" - }, - "path" : "\(spmFolder.pathString)/artifacts/GoogleAppMeasurement/GoogleAppMeasurementWithoutAdIdSupport.xcframework", - "targetName" : "GoogleAppMeasurementWithoutAdIdSupport" - }, - ] - """, - loadPackageInfoStub: { packagePath in - switch packagePath { - case self.checkoutsPath.appending(component: "GoogleAppMeasurement"): - return PackageInfo.googleAppMeasurement - case self.checkoutsPath.appending(component: "GoogleUtilities"): - return PackageInfo.googleUtilities - case self.checkoutsPath.appending(component: "nanopb"): - return PackageInfo.nanopb - default: - XCTFail("Unexpected path: \(self.path)") - return .test - } - }, - dependenciesGraph: try DependenciesGraph.googleAppMeasurement(spmFolder: spmFolder) - .merging(with: DependenciesGraph.googleUtilities( - spmFolder: spmFolder, - customProductTypes: [ - "GULMethodSwizzler": .framework, - "GULNetwork": .dynamicLibrary, - ] - )) - .merging(with: DependenciesGraph.nanopb(spmFolder: spmFolder)) - ) - // swiftformat:enable wrap - } - - func test_generate_test_local_path() throws { - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibrary")) - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibraryUtils")) - try fileHandler - .createFolder( - try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/another-dependency/Sources/AnotherLibrary") - ) - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TestUtilities")) - - let testPath = try AbsolutePath(validating: "/tmp/localPackage") - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "test", - "kind": "local", - "name": "test", - "path": "\(testPath.pathString)" - }, - "subpath": "test" - }, - { - "packageRef": { - "identity" : "a-dependency", - "kind": "remote", - "name": "a-dependency" - }, - "subpath": "ADependency" - }, - { - "packageRef": { - "identity" : "another-dependency", - "kind": "remote", - "name": "another-dependency" - }, - "subpath": "another-dependency" - } - ] - """, - loadPackageInfoStub: { packagePath in - switch packagePath { - case testPath: - return PackageInfo.test - case self.checkoutsPath.appending(component: "ADependency"): - return PackageInfo.aDependency - case self.checkoutsPath.appending(component: "another-dependency"): - return PackageInfo.anotherDependency - default: - XCTFail("Unexpected path: \(self.path)") - return .test - } - }, - dependenciesGraph: DependenciesGraph.test( - spmFolder: spmFolder, - packageFolder: Path(testPath.pathString), - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign], - fileHandler: fileHandler - ) - .merging(with: DependenciesGraph.aDependency( - spmFolder: spmFolder, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - )) - .merging(with: DependenciesGraph.anotherDependency( - spmFolder: spmFolder, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - )) - ) - } - - func test_generate_test_local_location_spm_pre_v5_6() throws { - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibrary")) - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibraryUtils")) - try fileHandler - .createFolder( - try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/another-dependency/Sources/AnotherLibrary") - ) - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TestUtilities")) - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TuistKit")) - - let testPath = try AbsolutePath(validating: "/tmp/localPackage") - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "test", - "kind": "local", - "name": "test", - "location": "\(testPath.pathString)" - }, - "subpath": "test" - }, - { - "packageRef": { - "identity" : "a-dependency", - "kind": "remote", - "name": "a-dependency" - }, - "subpath": "ADependency" - }, - { - "packageRef": { - "identity" : "another-dependency", - "kind": "remote", - "name": "another-dependency" - }, - "subpath": "another-dependency" - } - ] - """, - loadPackageInfoStub: { packagePath in - switch packagePath { - case testPath: - return PackageInfo.test - case self.checkoutsPath.appending(component: "ADependency"): - return PackageInfo.aDependency - case self.checkoutsPath.appending(component: "another-dependency"): - return PackageInfo.anotherDependency - default: - XCTFail("Unexpected path: \(self.path)") - return .test - } - }, - dependenciesGraph: DependenciesGraph.test( - spmFolder: spmFolder, - packageFolder: Path(testPath.pathString), - fileHandler: fileHandler - ) - .merging(with: DependenciesGraph.aDependency(spmFolder: spmFolder)) - .merging(with: DependenciesGraph.anotherDependency(spmFolder: spmFolder)) - ) - } - - func test_generate_test_fileSystem_location_spm_v5_6() throws { - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibrary")) - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibraryUtils")) - try fileHandler - .createFolder( - try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/another-dependency/Sources/AnotherLibrary") - ) - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TestUtilities")) - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TuistKit")) - - let testPath = try AbsolutePath(validating: "/tmp/localPackage") - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "test", - "kind": "fileSystem", - "name": "test", - "location": "\(testPath.pathString)" - }, - "subpath": "test" - }, - { - "packageRef": { - "identity" : "a-dependency", - "kind": "remoteSourceControl", - "name": "a-dependency" - }, - "subpath": "ADependency" - }, - { - "packageRef": { - "identity": "another-dependency", - "kind": "remoteSourceControl", - "name": "another-dependency" - }, - "subpath": "another-dependency" - } - ] - """, - loadPackageInfoStub: { packagePath in - switch packagePath { - case testPath: - return PackageInfo.test - case self.checkoutsPath.appending(component: "ADependency"): - return PackageInfo.aDependency - case self.checkoutsPath.appending(component: "another-dependency"): - return PackageInfo.anotherDependency - default: - XCTFail("Unexpected path: \(self.path)") - return .test - } - }, - dependenciesGraph: DependenciesGraph.test( - spmFolder: spmFolder, - packageFolder: Path(testPath.pathString), - fileHandler: fileHandler - ) - .merging(with: DependenciesGraph.aDependency(spmFolder: spmFolder)) - .merging(with: DependenciesGraph.anotherDependency(spmFolder: spmFolder)) - ) - } - - func test_generate_test_localSourceControl_location_spm_v5_6() throws { - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibrary")) - try fileHandler - .createFolder(try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/ADependency/Sources/ALibraryUtils")) - try fileHandler - .createFolder( - try AbsolutePath(validating: "\(spmFolder.pathString)/checkouts/another-dependency/Sources/AnotherLibrary") - ) - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TuistKit")) - - let testPath = try AbsolutePath(validating: "/tmp/localPackage") - try checkGenerated( - workspaceDependenciesJSON: """ - [ - { - "packageRef": { - "identity" : "test", - "kind": "localSourceControl", - "name": "test", - "location": "\(testPath.pathString)" - }, - "subpath": "test" - }, - { - "packageRef": { - "identity" : "a-dependency", - "kind": "remoteSourceControl", - "name": "a-dependency" - }, - "subpath": "ADependency" - }, - { - "packageRef": { - "identity": "another-dependency", - "kind": "remoteSourceControl", - "name": "another-dependency" - }, - "subpath": "another-dependency" - } - ] - """, - loadPackageInfoStub: { packagePath in - switch packagePath { - case testPath: - return PackageInfo.test - case self.checkoutsPath.appending(component: "ADependency"): - return PackageInfo.aDependency - case self.checkoutsPath.appending(component: "another-dependency"): - return PackageInfo.anotherDependency - default: - XCTFail("Unexpected path: \(self.path)") - return .test - } - }, - dependenciesGraph: DependenciesGraph.test( - spmFolder: spmFolder, - packageFolder: Path(testPath.pathString), - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign], - fileHandler: fileHandler - ) - .merging(with: DependenciesGraph.aDependency( - spmFolder: spmFolder, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - )) - .merging(with: DependenciesGraph.anotherDependency( - spmFolder: spmFolder, - destinations: [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] - )) - ) - } - - private func checkGenerated( - workspaceDependenciesJSON: String, - workspaceArtifactsJSON: String = "[]", - loadPackageInfoStub: @escaping (AbsolutePath) -> PackageInfo, - dependenciesGraph: TuistCore.DependenciesGraph - ) throws { - // Given - try fileHandler.createFolder(try AbsolutePath(validating: "/tmp/localPackage/Sources/TestUtilities")) - fileHandler.stubReadFile = { - XCTAssertEqual($0, self.path.appending(component: "workspace-state.json")) - return """ - { - "object": { - "dependencies": \(workspaceDependenciesJSON), - "artifacts": \(workspaceArtifactsJSON) - } - } - """.data(using: .utf8)! - } - - fileHandler.stubIsFolder = { _ in - // called to convert globs to AbsolutePath - true - } - - swiftPackageManagerController.loadPackageInfoStub = loadPackageInfoStub - - // When - let got = try subject.generate( - at: path, - productTypes: [ - "GULMethodSwizzler": .framework, - "GULNetwork": .dynamicLibrary, - ], - platforms: [.iOS], - baseSettings: .default, - targetSettings: [:], - swiftToolsVersion: nil, - projectOptions: [:] - ) - - // Then - XCTAssertEqual(got, dependenciesGraph) - } -} - -extension TuistCore.DependenciesGraph { - public func merging(with other: Self) throws -> Self { - var mergedExternalDependencies: [String: [ProjectDescription.TargetDependency]] = - externalDependencies - - for (name, dependency) in other.externalDependencies { - if let alreadyPresent = mergedExternalDependencies[name] { - fatalError("Dupliacted Entry(\(name), \(alreadyPresent), \(dependency)") - } - mergedExternalDependencies[name] = dependency - } - - let mergedExternalProjects = other.externalProjects.reduce(into: externalProjects) { result, entry in - if let alreadyPresent = result[entry.key] { - fatalError("Dupliacted Entry(\(entry.key), \(alreadyPresent), \(entry.value)") - } - result[entry.key] = entry.value - } - - return .init(externalDependencies: mergedExternalDependencies, externalProjects: mergedExternalProjects) - } -} diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerModuleMapGeneratorTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerModuleMapGeneratorTests.swift deleted file mode 100644 index 021d1b14c7e..00000000000 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerModuleMapGeneratorTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -import TSCBasic -import TuistSupportTesting -import XCTest - -@testable import TuistDependencies - -class SwiftPackageManagerModuleMapGeneratorTests: TuistTestCase { - private var subject: SwiftPackageManagerModuleMapGenerator! - - override func setUp() { - super.setUp() - subject = SwiftPackageManagerModuleMapGenerator() - } - - override func tearDown() { - subject = nil - super.tearDown() - } - - func test_generate_when_no_headers() throws { - try test_generate(for: .none) - } - - func test_generate_when_custom_module_map() throws { - try test_generate(for: .custom("/Absolute/Public/Headers/Path/module.modulemap")) - } - - func test_generate_when_umbrella_header() throws { - try test_generate(for: .header) - } - - func test_generate_when_nested_umbrella_header() throws { - try test_generate(for: .nestedHeader) - } - - private func test_generate(for moduleMap: ModuleMap) throws { - var writeCalled = false - fileHandler.stubContentsOfDirectory = { _ in - switch moduleMap { - case .none: - return [] - case .custom: - return ["/Absolute/Public/Headers/Path/module.modulemap"] - case .header: - return ["/Absolute/Public/Headers/Path/Module.h"] - case .nestedHeader: - return ["/Absolute/Public/Headers/Path/Module/Module.h"] - case .directory: - return ["/Absolute/Public/Headers/Path/AnotherHeader.h"] - } - } - fileHandler.stubExists = { path in - switch path { - case "/Absolute/Public/Headers/Path": - return moduleMap != .none - case "/Absolute/Public/Headers/Path/module.modulemap": - return moduleMap == .custom("/Absolute/Public/Headers/Path/module.modulemap") - case "/Absolute/Public/Headers/Path/Module.h": - return moduleMap == .header - case "/Absolute/Public/Headers/Path/Module/Module.h": - return moduleMap == .nestedHeader - default: - XCTFail("Unexpected exists call: \(path)") - return false - } - } - fileHandler.stubWrite = { content, path, atomically in - writeCalled = true - let expectedContent: String - switch moduleMap { - case .none, .custom, .header: - XCTFail("FileHandler.write should not be called") - return - case .nestedHeader: - expectedContent = """ - module Module { - umbrella header "/Absolute/Public/Headers/Path/Module/Module.h" - export * - } - - """ - case .directory: - expectedContent = """ - module Module { - umbrella "/Absolute/Public/Headers/Path" - export * - } - - """ - } - XCTAssertEqual(content, expectedContent) - XCTAssertEqual(path, "/Absolute/Public/Headers/Path/Module.modulemap") - XCTAssertTrue(atomically) - } - let got = try subject.generate(moduleName: "Module", publicHeadersPath: "/Absolute/Public/Headers/Path") - XCTAssertEqual(got, moduleMap) - switch moduleMap { - case .none, .custom, .header, .nestedHeader: - XCTAssertFalse(writeCalled) - case .directory: - XCTAssertTrue(writeCalled) - } - } -} diff --git a/Tests/TuistEnvKitTests/Commands/CommandRunnerTests.swift b/Tests/TuistEnvKitTests/Commands/CommandRunnerTests.swift deleted file mode 100644 index ef86f5af9a9..00000000000 --- a/Tests/TuistEnvKitTests/Commands/CommandRunnerTests.swift +++ /dev/null @@ -1,203 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class CommandRunnerErrorTests: XCTestCase { - func test_type() { - XCTAssertEqual(CommandRunnerError.versionNotFound.type, .abort) - } - - func test_description() { - XCTAssertEqual(CommandRunnerError.versionNotFound.description, "No valid version has been found locally") - } -} - -final class CommandRunnerTests: TuistUnitTestCase { - var versionResolver: MockVersionResolver! - var updater: MockUpdater! - var versionsController: MockVersionsController! - var installer: MockInstaller! - var arguments: [String] = [] - var exited: Int? - var subject: CommandRunner! - - override func setUp() { - super.setUp() - versionResolver = MockVersionResolver() - updater = MockUpdater() - versionsController = try! MockVersionsController() - installer = MockInstaller() - subject = CommandRunner( - versionResolver: versionResolver, - updater: updater, - installer: installer, - versionsController: versionsController, - arguments: { self.arguments }, - exiter: { self.exited = $0 } - ) - } - - override func tearDown() { - versionResolver = nil - updater = nil - versionsController = nil - installer = nil - subject = nil - super.tearDown() - } - - func test_when_binary() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionResolver.resolveStub = { _ in ResolvedVersion.bin(temporaryPath) } - system.succeedCommand([binaryPath.pathString, "--help"], output: "output") - try subject.run() - } - - func test_when_binary_and_throws() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionResolver.resolveStub = { _ in ResolvedVersion.bin(temporaryPath) } - system.errorCommand([binaryPath.pathString, "--help"], error: "error") - - try subject.run() - XCTAssertTrue(exited == 1) - } - - func test_when_version_file() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionsController.versionsStub = [] - versionsController.pathStub = { - $0 == "3.2.1" ? temporaryPath : try AbsolutePath(validating: "/invalid") - } - - versionResolver.resolveStub = { _ in ResolvedVersion.versionFile(temporaryPath, "3.2.1") } - - var installArgs: [String] = [] - installer.installStub = { version in installArgs.append(version) } - system.succeedCommand([binaryPath.pathString, "--help"], output: "") - - try subject.run() - - XCTAssertEqual(installArgs.count, 1) - XCTAssertEqual(installArgs.first, "3.2.1") - } - - func test_when_version_file_and_install_fails() throws { - let temporaryPath = try temporaryPath() - versionsController.versionsStub = [] - - versionResolver.resolveStub = { _ in ResolvedVersion.versionFile(temporaryPath, "3.2.1") } - - let error = NSError.test() - installer.installStub = { _ in throw error } - - XCTAssertThrowsError(try subject.run()) { - XCTAssertEqual($0 as NSError, error) - } - } - - func test_when_version_file_and_command_fails() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionsController.versionsStub = [] - versionsController.pathStub = { - $0 == "3.2.1" ? temporaryPath : try AbsolutePath(validating: "/invalid") - } - - versionResolver.resolveStub = { _ in ResolvedVersion.versionFile(temporaryPath, "3.2.1") - } - - system.errorCommand([binaryPath.pathString, "--help"], error: "error") - - try subject.run() - XCTAssertTrue(exited == 1) - } - - func test_when_highest_local_version_and_version_exists() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionResolver.resolveStub = { _ in ResolvedVersion.undefined } - - versionsController.semverVersionsStub = [Version("3.2.1")] - versionsController.pathStub = { - $0 == "3.2.1" ? temporaryPath : try AbsolutePath(validating: "/invalid") - } - - system.succeedCommand([binaryPath.pathString, "--help"], output: "") - - try subject.run() - } - - func test_when_highest_local_version_and_no_local_version() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionResolver.resolveStub = { _ in ResolvedVersion.undefined } - - versionsController.semverVersionsStub = [] - updater.updateStub = { - self.versionsController.semverVersionsStub = [Version("3.2.1")] - } - - versionsController.pathStub = { - $0 == "3.2.1" ? temporaryPath : try AbsolutePath(validating: "/invalid") - } - - system.succeedCommand([binaryPath.pathString, "--help"], output: "") - - try subject.run() - } - - func test_when_highest_local_version_and_no_local_version_and_update_fails() throws { - arguments = ["tuist", "--help"] - - versionResolver.resolveStub = { _ in ResolvedVersion.undefined } - - versionsController.semverVersionsStub = [] - let error = NSError.test() - updater.updateStub = { - throw error - } - - XCTAssertThrowsError(try subject.run()) { - XCTAssertEqual($0 as NSError, error) - } - } - - // TODO: And update fails - - func test_when_highest_local_version_and_command_fails() throws { - let temporaryPath = try temporaryPath() - let binaryPath = temporaryPath.appending(component: "tuist") - arguments = ["tuist", "--help"] - - versionResolver.resolveStub = { _ in ResolvedVersion.undefined } - - versionsController.semverVersionsStub = [Version("3.2.1")] - versionsController.pathStub = { - $0 == "3.2.1" ? temporaryPath : try AbsolutePath(validating: "/invalid") - } - - system.errorCommand([binaryPath.pathString, "--help"], error: "error") - - try subject.run() - XCTAssertTrue(exited == 1) - } -} diff --git a/Tests/TuistEnvKitTests/Commands/Mocks/MockCommandRunner.swift b/Tests/TuistEnvKitTests/Commands/Mocks/MockCommandRunner.swift deleted file mode 100644 index a531b5ec977..00000000000 --- a/Tests/TuistEnvKitTests/Commands/Mocks/MockCommandRunner.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -@testable import TuistEnvKit - -final class MockCommandRunner: CommandRunning { - var runCallCount: UInt = 0 - var runStub: Error? - - func run() throws { - runCallCount += 1 - if let runStub { throw runStub } - } -} diff --git a/Tests/TuistEnvKitTests/GitHub/Mocks/MockVersionProvider.swift b/Tests/TuistEnvKitTests/GitHub/Mocks/MockVersionProvider.swift deleted file mode 100644 index 53efbf4e051..00000000000 --- a/Tests/TuistEnvKitTests/GitHub/Mocks/MockVersionProvider.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Combine -import Foundation -import TSCBasic -import TSCUtility -import TuistSupport -import TuistSupportTesting -@testable import TuistEnvKit - -final class MockVersionProvider: VersionProviding { - var invokedVersions = false - var invokedVersionsCount = 0 - var stubbedVersionsResult: [Version]! - - func versions() -> [Version] { - invokedVersions = true - invokedVersionsCount += 1 - return stubbedVersionsResult - } - - var invokedLatestVersion = false - var invokedLatestVersionCount = 0 - var stubbedLatestVersionResult: Version? - - func latestVersion() -> Version? { - invokedLatestVersion = true - invokedLatestVersionCount += 1 - return stubbedLatestVersionResult - } -} diff --git a/Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift b/Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift deleted file mode 100644 index 44dbecf97fa..00000000000 --- a/Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import TuistSupport -import XCTest - -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class HTTPClientErrorTests: XCTestCase { - func test_type() { - // Given - let error = NSError.test() - let url = URL.test() - - // Then - XCTAssertEqual(HTTPClientError.clientError(url, error).type, .abort) - XCTAssertEqual(HTTPClientError.noData(url).type, .abort) - } - - func test_description() { - // Given - let error = NSError.test() - let url = URL.test() - - // Then - XCTAssertEqual( - HTTPClientError.clientError(url, error).description, - "The request to \(url.absoluteString) errored with: \(error.localizedDescription)" - ) - XCTAssertEqual(HTTPClientError.noData(url).description, "The request to \(url.absoluteString) returned no data") - } -} diff --git a/Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift b/Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift deleted file mode 100644 index 20fc4cf89e5..00000000000 --- a/Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest - -@testable import TuistEnvKit - -final class MockHTTPClient: HTTPClienting { - fileprivate var readStubs: [URL: Result] = [:] - fileprivate var downloadStubs: [URL: Result] = [:] - - func succeedRead(url: URL, response: Data) { - readStubs[url] = .success(response) - } - - func failRead(url: URL, error: Error) { - readStubs[url] = .failure(error) - } - - func read(url: URL) throws -> Data { - if let result = readStubs[url] { - switch result { - case let .failure(error): throw error - case let .success(data): return data - } - } else { - XCTFail("Read request to non-stubbed URL \(url)") - return Data() - } - } - - func download(url: URL, to: AbsolutePath) throws { - if let result = downloadStubs[url] { - switch result { - case let .failure(error): throw error - case let .success(from): - do { - try FileHandler.shared.copy(from: from, to: to) - } catch { - XCTFail("Error copying stubbed download to \(to.pathString)") - } - } - } else { - XCTFail("Download request to non-stubbed URL \(url)") - } - } -} diff --git a/Tests/TuistEnvKitTests/Installer/EnvInstallerTests.swift b/Tests/TuistEnvKitTests/Installer/EnvInstallerTests.swift deleted file mode 100644 index a3b0583efa7..00000000000 --- a/Tests/TuistEnvKitTests/Installer/EnvInstallerTests.swift +++ /dev/null @@ -1,220 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class EnvInstallerTests: TuistUnitTestCase { - var buildCopier: MockBuildCopier! - var versionsController: MockVersionsController! - var subject: EnvInstaller! - var tmpDir: TemporaryDirectory! - - override func setUp() { - super.setUp() - buildCopier = MockBuildCopier() - versionsController = try! MockVersionsController() - tmpDir = try! TemporaryDirectory(removeTreeOnDeinit: true) - subject = EnvInstaller( - buildCopier: buildCopier, - versionsController: versionsController - ) - } - - override func tearDown() { - buildCopier = nil - versionsController = nil - tmpDir = nil - subject = nil - super.tearDown() - } - - func test_install_when_bundled_release() throws { - let version = "3.2.1" - let temporaryPath = try temporaryPath() - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuistenv.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.envBundleName) - system.whichStub = { _ in "/path/to/tuist" } - system.succeedCommand([ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ]) - system.succeedCommand([ - "/usr/bin/unzip", - "-q", - downloadPath.pathString, - "tuistenv", - "-d", - temporaryDirectory.path.pathString, - ]) - system.succeedCommand([ - "rm", - "/path/to/tuist", - ]) - system.succeedCommand([ - "mv", - temporaryDirectory.path.appending(component: "tuistenv").pathString, - "/path/to/tuist", - ]) - - try subject.install( - version: version, - temporaryDirectory: temporaryDirectory.path - ) - - XCTAssertPrinterOutputContains(""" - Downloading TuistEnv version 3.2.1 - Installing… - TuistEnv Version \(version) installed - """) - } - - func test_install_when_cp_fails() throws { - let version = "3.2.1" - let temporaryPath = try temporaryPath() - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuistenv.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.envBundleName) - system.whichStub = { _ in "/path/to/tuist" } - system.succeedCommand([ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ]) - system.succeedCommand([ - "/usr/bin/unzip", - "-q", - downloadPath.pathString, - "tuistenv", - "-d", - temporaryDirectory.path.pathString, - ]) - system.succeedCommand([ - "sudo", - "rm", - "/path/to/tuist", - ]) - system.succeedCommand([ - "sudo", - "mv", - temporaryDirectory.path.appending(component: "tuistenv").pathString, - "/path/to/tuist", - ]) - - try subject.install( - version: version, - temporaryDirectory: temporaryDirectory.path - ) - - XCTAssertPrinterOutputContains(""" - Downloading TuistEnv version 3.2.1 - Installing… - TuistEnv Version \(version) installed - """) - } - - func test_install_when_bundled_release_and_download_fails() throws { - let temporaryPath = try temporaryPath() - let version = "3.2.1" - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuistenv.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.envBundleName) - system.whichStub = { _ in "/path/to/tuist" } - system.errorCommand( - [ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ], - error: "download_error" - ) - - let expectedError = TuistSupport.SystemError.terminated( - command: "/usr/bin/curl", - code: 1, - standardError: Data("download_error".utf8) - ) - XCTAssertThrowsSpecific(try subject.install(version: version, temporaryDirectory: temporaryDirectory.path), expectedError) - } - - func test_install_when_bundled_release_when_unzip_fails() throws { - let temporaryPath = try temporaryPath() - let version = "3.2.1" - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuistenv.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.envBundleName) - system.whichStub = { _ in "/path/to/tuist" } - system.succeedCommand([ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ]) - system.errorCommand( - [ - "/usr/bin/unzip", - "-q", - downloadPath.pathString, - "tuistenv", - "-d", - temporaryDirectory.path.pathString, - ], - error: "unzip_error" - ) - - let expectedError = TuistSupport.SystemError.terminated( - command: "/usr/bin/unzip", - code: 1, - standardError: Data("unzip_error".utf8) - ) - XCTAssertThrowsSpecific(try subject.install(version: version, temporaryDirectory: temporaryDirectory.path), expectedError) - } - - // MARK: - Fileprivate - - fileprivate func stubLocalAndRemoveSwiftVersions() { - system.swiftVersionStub = { "5.0.0" } - } -} diff --git a/Tests/TuistEnvKitTests/Installer/InstallerTests.swift b/Tests/TuistEnvKitTests/Installer/InstallerTests.swift deleted file mode 100644 index 86d115ca2bd..00000000000 --- a/Tests/TuistEnvKitTests/Installer/InstallerTests.swift +++ /dev/null @@ -1,141 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class InstallerTests: TuistUnitTestCase { - var buildCopier: MockBuildCopier! - var versionsController: MockVersionsController! - var subject: Installer! - var tmpDir: TemporaryDirectory! - - override func setUp() { - super.setUp() - buildCopier = MockBuildCopier() - versionsController = try! MockVersionsController() - tmpDir = try! TemporaryDirectory(removeTreeOnDeinit: true) - subject = Installer( - buildCopier: buildCopier, - versionsController: versionsController - ) - } - - override func tearDown() { - buildCopier = nil - versionsController = nil - tmpDir = nil - subject = nil - super.tearDown() - } - - func test_install_when_bundled_release() throws { - let version = "3.2.1" - let temporaryPath = try temporaryPath() - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuist.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.bundleName) - system.succeedCommand([ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ]) - system.succeedCommand([ - "/usr/bin/unzip", - "-q", - downloadPath.pathString, - "-d", - temporaryPath.pathString, - ]) - - try subject.install( - version: version, - temporaryDirectory: temporaryDirectory.path - ) - - XCTAssertPrinterOutputContains(""" - Downloading version 3.2.1 - Installing… - Version \(version) installed - """) - } - - func test_install_when_bundled_release_and_download_fails() throws { - let temporaryPath = try temporaryPath() - let version = "3.2.1" - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuist.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.bundleName) - system.errorCommand( - [ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ], - error: "download_error" - ) - - XCTAssertThrowsError(try subject.install(version: version, temporaryDirectory: temporaryDirectory.path)) - } - - func test_install_when_bundled_release_when_unzip_fails() throws { - let temporaryPath = try temporaryPath() - let version = "3.2.1" - stubLocalAndRemoveSwiftVersions() - let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) - let downloadURL = URL(string: "https://github.com/tuist/tuist/releases/download/3.2.1/tuist.zip")! - - versionsController.installStub = { _, closure in - try closure(temporaryPath) - } - - let downloadPath = temporaryDirectory - .path - .appending(component: Constants.bundleName) - system.succeedCommand([ - "/usr/bin/curl", - "-LSs", - "--output", - downloadPath.pathString, - downloadURL.absoluteString, - ]) - system.errorCommand( - [ - "/usr/bin/unzip", - downloadPath.pathString, - "-d", - temporaryPath.pathString, - ], - error: "unzip_error" - ) - - XCTAssertThrowsError(try subject.install(version: version, temporaryDirectory: temporaryDirectory.path)) - } - - // MARK: - Fileprivate - - fileprivate func stubLocalAndRemoveSwiftVersions() { - system.swiftVersionStub = { "5.0.0" } - } -} diff --git a/Tests/TuistEnvKitTests/Installer/Mocks/MockBuildCopier.swift b/Tests/TuistEnvKitTests/Installer/Mocks/MockBuildCopier.swift deleted file mode 100644 index c41d93da60b..00000000000 --- a/Tests/TuistEnvKitTests/Installer/Mocks/MockBuildCopier.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistEnvKit - -final class MockBuildCopier: BuildCopying { - var copyCallCount: UInt = 0 - var copyStub: ((AbsolutePath, AbsolutePath) throws -> Void)? - - func copy(from: AbsolutePath, to: AbsolutePath) throws { - copyCallCount += 1 - try copyStub?(from, to) - } -} diff --git a/Tests/TuistEnvKitTests/Installer/Mocks/MockEnvInstaller.swift b/Tests/TuistEnvKitTests/Installer/Mocks/MockEnvInstaller.swift deleted file mode 100644 index 3d9c49ab802..00000000000 --- a/Tests/TuistEnvKitTests/Installer/Mocks/MockEnvInstaller.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -@testable import TuistEnvKit - -final class MockEnvInstaller: EnvInstalling { - var installCallCount: UInt = 0 - var installStub: ((String) throws -> Void)? - - func install(version: String) throws { - installCallCount += 1 - try installStub?(version) - } -} diff --git a/Tests/TuistEnvKitTests/Installer/Mocks/MockInstaller.swift b/Tests/TuistEnvKitTests/Installer/Mocks/MockInstaller.swift deleted file mode 100644 index 9dfbe270d2b..00000000000 --- a/Tests/TuistEnvKitTests/Installer/Mocks/MockInstaller.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -@testable import TuistEnvKit - -final class MockInstaller: Installing { - var installCallCount: UInt = 0 - var installStub: ((String) throws -> Void)? - - func install(version: String) throws { - installCallCount += 1 - try installStub?(version) - } -} diff --git a/Tests/TuistEnvKitTests/Services/BundleServiceTests.swift b/Tests/TuistEnvKitTests/Services/BundleServiceTests.swift deleted file mode 100644 index a5b69d639f7..00000000000 --- a/Tests/TuistEnvKitTests/Services/BundleServiceTests.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import TSCBasic -import XCTest -@testable import TuistEnvKit -@testable import TuistSupport -@testable import TuistSupportTesting - -final class BundleServiceErrorTests: XCTestCase { - func test_type() throws { - let path = try AbsolutePath(validating: "/test") - XCTAssertEqual(BundleServiceError.missingVersionFile(path).type, .abort) - } - - func test_description() throws { - let path = try AbsolutePath(validating: "/test") - XCTAssertEqual( - BundleServiceError.missingVersionFile(path).description, - "Couldn't find a .tuist-version file in the directory \(path.pathString)" - ) - } -} - -final class BundleServiceTests: TuistUnitTestCase { - var versionsController: MockVersionsController! - var installer: MockInstaller! - var subject: BundleService! - var tmpDir: TemporaryDirectory! - - override func setUp() { - super.setUp() - versionsController = try! MockVersionsController() - installer = MockInstaller() - tmpDir = try! TemporaryDirectory(removeTreeOnDeinit: true) - subject = BundleService( - versionsController: versionsController, - installer: installer - ) - } - - override func tearDown() { - versionsController = nil - installer = nil - subject = nil - tmpDir = nil - super.tearDown() - } - - func test_run_throws_when_there_is_no_xmp_version_in_the_directory() throws { - let temporaryPath = try temporaryPath() - XCTAssertThrowsSpecific(try subject.run(), BundleServiceError.missingVersionFile(temporaryPath)) - } - - func test_run_installs_the_app_if_it_doesnt_exist() throws { - let temporaryPath = try temporaryPath() - let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName) - try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8) - - installer.installStub = { version in - let versionPath = try self.versionsController.path(version: version) - try FileHandler.shared.createFolder(versionPath) - try Data().write(to: versionPath.appending(component: "test").url) - } - - try subject.run() - - let bundledTestFilePath = temporaryPath - .appending(component: Constants.binFolderName) - .appending(component: "test") - - XCTAssertTrue(FileHandler.shared.exists(bundledTestFilePath)) - } - - func test_run_doesnt_install_the_app_if_it_already_exists() throws { - let temporaryPath = try temporaryPath() - - let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName) - try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8) - let versionPath = try versionsController.path(version: "3.2.1") - try FileHandler.shared.createFolder(versionPath) - - try subject.run() - - XCTAssertEqual(installer.installCallCount, 0) - } - - func test_run_doesnt_install_the_app_if_it_already_exists_with_whitespace_in_version_file() throws { - let temporaryPath = try temporaryPath() - - let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName) - try "3.2.1\n\t".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8) - let versionPath = try versionsController.path(version: "3.2.1") - try FileHandler.shared.createFolder(versionPath) - - try subject.run() - - XCTAssertEqual(installer.installCallCount, 0) - } - - func test_run_prints_the_right_messages() throws { - let temporaryPath = try temporaryPath() - let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName) - let binPath = temporaryPath.appending(component: Constants.binFolderName) - - try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8) - - installer.installStub = { version in - let versionPath = try self.versionsController.path(version: version) - try FileHandler.shared.createFolder(versionPath) - try Data().write(to: versionPath.appending(component: "test").url) - } - - try subject.run() - - XCTAssertPrinterOutputContains(""" - Bundling the version 3.2.1 in the directory \(binPath.pathString) - Version 3.2.1 not available locally. Installing... - tuist bundled successfully at \(binPath.pathString) - """) - } -} diff --git a/Tests/TuistEnvKitTests/Services/InstallServiceTests.swift b/Tests/TuistEnvKitTests/Services/InstallServiceTests.swift deleted file mode 100644 index 1f88cf20487..00000000000 --- a/Tests/TuistEnvKitTests/Services/InstallServiceTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class InstallServiceTests: TuistUnitTestCase { - var versionsController: MockVersionsController! - var installer: MockInstaller! - var subject: InstallService! - - override func setUp() { - super.setUp() - - versionsController = try! MockVersionsController() - installer = MockInstaller() - subject = InstallService( - versionsController: versionsController, - installer: installer - ) - } - - override func tearDown() { - versionsController = nil - installer = nil - subject = nil - - super.tearDown() - } - - func test_run_when_version_is_already_installed() throws { - versionsController.versionsStub = [InstalledVersion.reference("3.2.1")] - - try subject.run(version: "3.2.1") - - XCTAssertPrinterOutputContains("Version 3.2.1 already installed, skipping") - } - - func test_run() throws { - versionsController.versionsStub = [] - - var installArgs: [String] = [] - installer.installStub = { version in installArgs.append(version) } - - try subject.run(version: "3.2.1") - - XCTAssertEqual(installArgs.count, 1) - XCTAssertEqual(installArgs.first, "3.2.1") - } - - func test_run_when_without_trailing_zero() throws { - versionsController.versionsStub = [] - - var installArgs: [String] = [] - installer.installStub = { version in installArgs.append(version) } - - try subject.run(version: "3.2") - - XCTAssertEqual(installArgs.count, 1) - XCTAssertEqual(installArgs.first, "3.2.0") - } - - func test_run_when_force() throws { - versionsController.versionsStub = [] - - var installArgs: [String] = [] - installer.installStub = { version in installArgs.append(version) } - - try subject.run(version: "3.2.1") - - XCTAssertEqual(installArgs.count, 1) - XCTAssertEqual(installArgs.first, "3.2.1") - } -} diff --git a/Tests/TuistEnvKitTests/Services/LocalServiceTests.swift b/Tests/TuistEnvKitTests/Services/LocalServiceTests.swift deleted file mode 100644 index 0d51967b902..00000000000 --- a/Tests/TuistEnvKitTests/Services/LocalServiceTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class LocalServiceTests: TuistUnitTestCase { - var subject: LocalService! - var versionController: MockVersionsController! - - override func setUp() { - super.setUp() - - versionController = try! MockVersionsController() - subject = LocalService(versionController: versionController) - } - - override func tearDown() { - subject = nil - versionController = nil - - super.tearDown() - } - - func test_run_when_version_argument_is_passed() throws { - // Given - let temporaryPath = try temporaryPath() - - // When - try subject.run(version: "3.2.1") - - // Then - let versionPath = temporaryPath.appending(component: Constants.versionFileName) - XCTAssertEqual(try String(contentsOf: versionPath.url), "3.2.1") - } - - func test_run_prints_when_version_argument_is_passed() throws { - // Given - let temporaryPath = try temporaryPath() - - // When - try subject.run(version: "3.2.1") - - // Then - let versionPath = temporaryPath.appending(component: Constants.versionFileName) - - XCTAssertPrinterOutputContains(""" - Generating \(Constants.versionFileName) file with version 3.2.1 - File generated at path \(versionPath.pathString) - """) - } - - func test_run_prints_when_no_argument_is_passed() throws { - // Given - versionController.semverVersionsStub = [Version("1.2.3"), Version("3.2.1")] - - // When - try subject.run(version: nil) - - // Then - XCTAssertPrinterOutputContains(""" - The following versions are available in the local environment: - - 3.2.1 - - 1.2.3 - """) - } -} diff --git a/Tests/TuistEnvKitTests/Services/UninstallServiceTests.swift b/Tests/TuistEnvKitTests/Services/UninstallServiceTests.swift deleted file mode 100644 index fd94f0f42e8..00000000000 --- a/Tests/TuistEnvKitTests/Services/UninstallServiceTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class UninstallServiceTests: TuistUnitTestCase { - var versionsController: MockVersionsController! - var installer: MockInstaller! - var subject: UninstallService! - - override func setUp() { - super.setUp() - versionsController = try! MockVersionsController() - installer = MockInstaller() - subject = UninstallService( - versionsController: versionsController, - installer: installer - ) - } - - override func tearDown() { - versionsController = nil - installer = nil - subject = nil - super.tearDown() - } - - func test_run_when_version_is_installed() throws { - versionsController.versionsStub = [InstalledVersion.reference("3.2.1")] - var uninstalledVersion: String? - versionsController.uninstallStub = { uninstalledVersion = $0 } - - try subject.run(version: "3.2.1") - - XCTAssertPrinterOutputContains("Version 3.2.1 uninstalled") - XCTAssertEqual(uninstalledVersion, "3.2.1") - } - - func test_run_when_version_is_installed_and_throws() throws { - versionsController.versionsStub = [InstalledVersion.reference("3.2.1")] - - let error = NSError.test() - versionsController.uninstallStub = { _ in throw error } - - XCTAssertThrowsError(try subject.run(version: "3.2.1")) { - XCTAssertEqual($0 as NSError, error) - } - } - - func test_run_when_version_is_not_installed() throws { - versionsController.versionsStub = [] - - try subject.run(version: "3.2.1") - - XCTAssertPrinterOutputContains("Version 3.2.1 cannot be uninstalled because it's not installed") - } -} diff --git a/Tests/TuistEnvKitTests/Services/UpdateServiceTests.swift b/Tests/TuistEnvKitTests/Services/UpdateServiceTests.swift deleted file mode 100644 index 5b1b56c9c11..00000000000 --- a/Tests/TuistEnvKitTests/Services/UpdateServiceTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import TuistSupport -import XCTest - -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class UpdateServiceTests: TuistUnitTestCase { - var subject: UpdateService! - var updater: MockUpdater! - - override func setUp() { - super.setUp() - updater = MockUpdater() - subject = UpdateService(updater: updater) - } - - override func tearDown() { - updater = nil - subject = nil - super.tearDown() - } - - func test_run() throws { - var updateCallsCount: UInt = 0 - updater.updateStub = { - updateCallsCount += 1 - } - - try subject.run() - - XCTAssertPrinterOutputContains("Checking for updates...") - XCTAssertEqual(updateCallsCount, 1) - } -} diff --git a/Tests/TuistEnvKitTests/Settings/Mocks/MockSettingsController.swift b/Tests/TuistEnvKitTests/Settings/Mocks/MockSettingsController.swift deleted file mode 100644 index 00737bfe1e1..00000000000 --- a/Tests/TuistEnvKitTests/Settings/Mocks/MockSettingsController.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -@testable import TuistEnvKit - -final class MockSettingsController: SettingsControlling { - var settingsCount: UInt = 0 - var settingsStub: Settings? - var setSettingsStub: ((Settings) throws -> Void)? - var setSettingsCount: UInt = 0 - - func settings() throws -> Settings { - settingsCount += 1 - if let settingsStub { return settingsStub } - return Settings() - } - - func set(settings: Settings) throws { - setSettingsCount += 1 - try setSettingsStub?(settings) - } -} diff --git a/Tests/TuistEnvKitTests/Settings/SettingsControllerTests.swift b/Tests/TuistEnvKitTests/Settings/SettingsControllerTests.swift deleted file mode 100644 index f7236a34f66..00000000000 --- a/Tests/TuistEnvKitTests/Settings/SettingsControllerTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import TSCBasic -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class SettingsControllerTests: TuistUnitTestCase { - var subject: SettingsController! - - override func setUp() { - super.setUp() - - subject = SettingsController() - } - - override func tearDown() { - subject = nil - - super.tearDown() - } - - func test_settings_returns_the_default_settings_if_they_havent_been_set() throws { - try XCTAssertEqual(subject.settings(), Settings()) - } -} diff --git a/Tests/TuistEnvKitTests/Settings/SettingsTests.swift b/Tests/TuistEnvKitTests/Settings/SettingsTests.swift deleted file mode 100644 index cafacef2be6..00000000000 --- a/Tests/TuistEnvKitTests/Settings/SettingsTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class SettingsTests: TuistUnitTestCase { - var gitHandler: MockGitHandler! - var subject: VersionProvider! - - override func setUp() { - super.setUp() - - gitHandler = MockGitHandler() - subject = VersionProvider(gitHandler: gitHandler) - } - - override func tearDown() { - gitHandler = nil - subject = nil - - super.tearDown() - } - - func test_latest_remote_version() throws { - gitHandler.remoteTaggedVersionsStub = ["1.9.0", "2.0.0", "2.0.1"] - let highestRemoteVersion = try subject.latestVersion() - - XCTAssertEqual(highestRemoteVersion, "2.0.1") - } - - func test_versions() throws { - gitHandler.remoteTaggedVersionsStub = ["1.9.0", "2.0.0", "2.0.1"] - let versions = try subject.versions() - - XCTAssertEqual(versions, ["1.9.0", "2.0.0", "2.0.1"]) - } - - func test_error() throws { - gitHandler.remoteTaggedVersionsStub = [] - - XCTAssertEmpty(try subject.versions()) - XCTAssertNil(try subject.latestVersion()) - } -} diff --git a/Tests/TuistEnvKitTests/Updater/Mocks/MockUpdater.swift b/Tests/TuistEnvKitTests/Updater/Mocks/MockUpdater.swift deleted file mode 100644 index 5069fd16c6a..00000000000 --- a/Tests/TuistEnvKitTests/Updater/Mocks/MockUpdater.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -@testable import TuistEnvKit - -final class MockUpdater: Updating { - var updateCallCount: UInt = 0 - var updateStub: (() throws -> Void)? - - func update() throws { - updateCallCount += 1 - try updateStub?() - } -} diff --git a/Tests/TuistEnvKitTests/Updater/UpdaterTests.swift b/Tests/TuistEnvKitTests/Updater/UpdaterTests.swift deleted file mode 100644 index 3d2637fd35b..00000000000 --- a/Tests/TuistEnvKitTests/Updater/UpdaterTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation -import struct TSCUtility.Version -import TuistSupport -import XCTest -@testable import TuistEnvKit -@testable import TuistSupportTesting - -final class UpdaterTests: TuistUnitTestCase { - var versionsController: MockVersionsController! - var installer: MockInstaller! - var envInstaller: MockEnvInstaller! - var versionProvider: MockVersionProvider! - var subject: Updater! - - override func setUp() { - super.setUp() - - versionsController = try! MockVersionsController() - installer = MockInstaller() - envInstaller = MockEnvInstaller() - versionProvider = MockVersionProvider() - subject = Updater( - versionsController: versionsController, - installer: installer, - envInstaller: envInstaller, - versionProvider: versionProvider - ) - } - - override func tearDown() { - versionsController = nil - installer = nil - envInstaller = nil - subject = nil - versionProvider = nil - super.tearDown() - } - - func test_update_when_there_are_no_updates() throws { - versionsController.semverVersionsStub = ["3.2.1"] - versionProvider.stubbedLatestVersionResult = Version("3.2.1") - - try subject.update() - - XCTAssertPrinterOutputContains("There are no updates available") - } - - func test_update_when_there_are_updates() throws { - versionsController.semverVersionsStub = ["3.1.1"] - versionProvider.stubbedLatestVersionResult = Version("3.2.1") - var installArgs: [String] = [] - installer.installStub = { version in installArgs.append(version) } - envInstaller.installStub = { version in installArgs.append(version) } - - try subject.update() - - XCTAssertPrinterOutputContains("Installing new version available 3.2.1") - XCTAssertEqual(installArgs, ["3.2.1", "3.2.1"]) - } - - func test_update_when_no_local_versions_available() throws { - versionsController.semverVersionsStub = [] - versionProvider.stubbedLatestVersionResult = Version("3.2.1") - - var installArgs: [String] = [] - installer.installStub = { version in installArgs.append(version) } - envInstaller.installStub = { version in installArgs.append(version) } - - try subject.update() - - XCTAssertPrinterOutputContains("No local versions available. Installing the latest version 3.2.1") - XCTAssertEqual(installArgs, ["3.2.1", "3.2.1"]) - } -} diff --git a/Tests/TuistEnvKitTests/Versions/Mocks/MockVersionResolver.swift b/Tests/TuistEnvKitTests/Versions/Mocks/MockVersionResolver.swift deleted file mode 100644 index 77177d9ae27..00000000000 --- a/Tests/TuistEnvKitTests/Versions/Mocks/MockVersionResolver.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistEnvKit - -class MockVersionResolver: VersionResolving { - var resolveCallCount: UInt = 0 - var resolveStub: ((AbsolutePath) throws -> ResolvedVersion)? - - func resolve(path: AbsolutePath) throws -> ResolvedVersion { - resolveCallCount += 1 - return try resolveStub?(path) ?? .undefined - } -} diff --git a/Tests/TuistEnvKitTests/Versions/Mocks/MockVersionsController.swift b/Tests/TuistEnvKitTests/Versions/Mocks/MockVersionsController.swift deleted file mode 100644 index 00f59994974..00000000000 --- a/Tests/TuistEnvKitTests/Versions/Mocks/MockVersionsController.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation -import TSCBasic -import struct TSCUtility.Version -import TuistSupport -import TuistSupportTesting -@testable import TuistEnvKit - -final class MockVersionsController: VersionsControlling { - private let tmpDir: TemporaryDirectory - var path: AbsolutePath { tmpDir.path } - var pathCallCount: UInt = 0 - var pathStub: ((String) throws -> AbsolutePath)? - var installCallCount: UInt = 0 - var installStub: ((String, Installation) throws -> Void)? - var versionsCallCount: UInt = 0 - var versionsStub: [InstalledVersion] = [] - var semverVersionsCount: UInt = 0 - var semverVersionsStub: [Version] = [] - var uninstallCallCount: UInt = 0 - var uninstallStub: ((String) throws -> Void)? - - init() throws { - tmpDir = try TemporaryDirectory(removeTreeOnDeinit: true) - installStub = { version, installation in - try installation(self.path.appending(component: version)) - } - pathStub = { version in - self.path.appending(component: version) - } - } - - func path(version: String) throws -> AbsolutePath { - pathCallCount += 1 - return try pathStub?(version) ?? AbsolutePath(validating: "/test") - } - - func install(version: String, installation: Installation) throws { - installCallCount += 1 - try installStub?(version, installation) - } - - func uninstall(version: String) throws { - uninstallCallCount += 1 - try uninstallStub?(version) - } - - func versions() -> [InstalledVersion] { - versionsCallCount += 1 - return versionsStub - } - - func semverVersions() -> [Version] { - semverVersionsCount += 1 - return semverVersionsStub - } -} diff --git a/Tests/TuistEnvKitTests/Versions/VersionResolverTests.swift b/Tests/TuistEnvKitTests/Versions/VersionResolverTests.swift deleted file mode 100644 index 3c382c32103..00000000000 --- a/Tests/TuistEnvKitTests/Versions/VersionResolverTests.swift +++ /dev/null @@ -1,177 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import TuistSupportTesting -import XCTest -@testable import TuistEnvKit - -final class VersionResolverErrorTests: XCTestCase { - func test_errorDescription() throws { - let path = try AbsolutePath(validating: "/test") - XCTAssertEqual(VersionResolverError.readError(path: path).description, "Cannot read the version file at path /test.") - } - - func test_equatable() throws { - let path = try AbsolutePath(validating: "/test") - XCTAssertEqual(VersionResolverError.readError(path: path), VersionResolverError.readError(path: path)) - } -} - -final class VersionResolverTests: XCTestCase { - var subject: VersionResolver! - var settingsController: MockSettingsController! - - override func setUp() { - super.setUp() - settingsController = MockSettingsController() - subject = VersionResolver(settingsController: settingsController) - } - - override func tearDown() { - settingsController = nil - subject = nil - super.tearDown() - } - - func test_resolve_when_version_and_bin() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let versionPath = tmp_dir.path.appending(component: Constants.versionFileName) - let binPath = tmp_dir.path.appending(component: Constants.binFolderName) - let tuistPath = binPath.appending(component: Constants.binName) - - // /tmp/dir/.tuist-version - try "3.2.1".write( - to: URL(fileURLWithPath: versionPath.pathString), - atomically: true, - encoding: .utf8 - ) - // /tmp/dir/.tuist-bin - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: binPath.pathString), - withIntermediateDirectories: true, - attributes: nil - ) - // /tmp/dir/.tuist-bin/tuist - FileManager.default.createFile( - atPath: tuistPath.pathString, - contents: nil - ) - - let got = try subject.resolve(path: tmp_dir.path) - XCTAssertEqual(got, .bin(binPath)) - } - - func test_resolve_when_version() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let versionPath = tmp_dir.path.appending(component: Constants.versionFileName) - - // /tmp/dir/.tuist-version - try "3.2.1".write( - to: URL(fileURLWithPath: versionPath.pathString), - atomically: true, - encoding: .utf8 - ) - - let got = try subject.resolve(path: tmp_dir.path) - XCTAssertEqual(got, .versionFile(versionPath, "3.2.1")) - } - - func test_resolve_when_version_contains_trailing_whitespace() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let versionPath = tmp_dir.path.appending(component: Constants.versionFileName) - - // /tmp/dir/.tuist-version - try "3.2.1 \n".write( - to: URL(fileURLWithPath: versionPath.pathString), - atomically: true, - encoding: .utf8 - ) - - let got = try subject.resolve(path: tmp_dir.path) - XCTAssertEqual(got, .versionFile(versionPath, "3.2.1")) - } - - func test_resolve_when_bin() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let binPath = tmp_dir.path.appending(component: Constants.binFolderName) - let tuistPath = binPath.appending(component: Constants.binName) - - // /tmp/dir/.tuist-bin - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: binPath.pathString), - withIntermediateDirectories: true, - attributes: nil - ) - // /tmp/dir/.tuist-bin/tuist - FileManager.default.createFile( - atPath: tuistPath.pathString, - contents: nil - ) - - let got = try subject.resolve(path: tmp_dir.path) - XCTAssertEqual(got, .bin(binPath)) - } - - func test_resolve_when_empty_bin() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let binPath = tmp_dir.path.appending(component: Constants.binFolderName) - - // /tmp/dir/.tuist-bin - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: binPath.pathString), - withIntermediateDirectories: true, - attributes: nil - ) - - let got = try subject.resolve(path: tmp_dir.path) - XCTAssertEqual(got, .undefined) - } - - func test_resolve_when_version_in_parent_directory() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let versionPath = tmp_dir.path.appending(component: Constants.versionFileName) - let childPath = tmp_dir.path.appending(component: "child") - - // /tmp/dir/.tuist-version - try "3.2.1".write( - to: URL(fileURLWithPath: versionPath.pathString), - atomically: true, - encoding: .utf8 - ) - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: childPath.pathString), - withIntermediateDirectories: true, - attributes: nil - ) - - let got = try subject.resolve(path: childPath) - XCTAssertEqual(got, .versionFile(versionPath, "3.2.1")) - } - - func test_resolve_when_bin_in_parent_directory() throws { - let tmp_dir = try TemporaryDirectory(removeTreeOnDeinit: true) - let binPath = tmp_dir.path.appending(component: Constants.binFolderName) - let tuistPath = binPath.appending(component: Constants.binName) - let childPath = tmp_dir.path.appending(component: "child") - - // /tmp/dir/.tuist-bin - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: binPath.pathString), - withIntermediateDirectories: true, - attributes: nil - ) - // /tmp/dir/.tuist-bin/tuist - FileManager.default.createFile( - atPath: tuistPath.pathString, - contents: nil - ) - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: childPath.pathString), - withIntermediateDirectories: true, - attributes: nil - ) - - let got = try subject.resolve(path: childPath) - XCTAssertEqual(got, .bin(binPath)) - } -} diff --git a/Tests/TuistGenerateAcceptanceTests/GenerateAcceptanceTests.swift b/Tests/TuistGeneratorAcceptanceTests/GenerateAcceptanceTests.swift similarity index 78% rename from Tests/TuistGenerateAcceptanceTests/GenerateAcceptanceTests.swift rename to Tests/TuistGeneratorAcceptanceTests/GenerateAcceptanceTests.swift index 5bcd694ee9f..7feb6249423 100644 --- a/Tests/TuistGenerateAcceptanceTests/GenerateAcceptanceTests.swift +++ b/Tests/TuistGeneratorAcceptanceTests/GenerateAcceptanceTests.swift @@ -1,14 +1,13 @@ -import TSCBasic +import Path 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 setUpFixture(.iosAppWithTests) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -16,7 +15,7 @@ final class GenerateAcceptanceTestiOSAppWithTests: TuistAcceptanceTestCase { final class GenerateAcceptanceTestiOSAppWithFrameworks: TuistAcceptanceTestCase { func test_ios_app_with_frameworks() async throws { - try setUpFixture(.iosAppWithFrameworks) + try await setUpFixture(.iosAppWithFrameworks) try await run(GenerateCommand.self) try await run(BuildCommand.self) try await XCTAssertProductWithDestinationContainsInfoPlistKey( @@ -29,7 +28,7 @@ final class GenerateAcceptanceTestiOSAppWithFrameworks: TuistAcceptanceTestCase final class GenerateAcceptanceTestiOSAppWithHeaders: TuistAcceptanceTestCase { func test_ios_app_with_headers() async throws { - try setUpFixture(.iosAppWithHeaders) + try await setUpFixture(.iosAppWithHeaders) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -37,7 +36,7 @@ final class GenerateAcceptanceTestiOSAppWithHeaders: TuistAcceptanceTestCase { final class GenerateAcceptanceTestInvalidWorkspaceManifestName: TuistAcceptanceTestCase { func test_invalid_workspace_manifest_name() async throws { - try setUpFixture(.invalidWorkspaceManifestName) + try await setUpFixture(.invalidWorkspaceManifestName) do { try await run(GenerateCommand.self) XCTFail("Generate command should have failed") @@ -50,7 +49,7 @@ final class GenerateAcceptanceTestInvalidWorkspaceManifestName: TuistAcceptanceT // 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 setUpFixture("ios_app_with_sdk") // try await run(GenerateCommand.self) // try await run(BuildCommand.self) // try await run(BuildCommand.self, "MacFramework", "--platform", "macOS") @@ -60,7 +59,7 @@ final class GenerateAcceptanceTestInvalidWorkspaceManifestName: TuistAcceptanceT final class GenerateAcceptanceTestiOSAppWithFrameworkAndResources: TuistAcceptanceTestCase { func test_ios_app_with_framework_and_resources() async throws { - try setUpFixture(.iosAppWithFrameworkAndResources) + try await setUpFixture(.iosAppWithFrameworkAndResources) try await run(GenerateCommand.self) try await run(BuildCommand.self) for resource in [ @@ -129,12 +128,62 @@ final class GenerateAcceptanceTestiOSAppWithFrameworkAndResources: TuistAcceptan "App.app", destination: "Debug-iphonesimulator" ) + try XCTAssertDirectoryContentEqual( + fixturePath.appending(components: "App", "Derived", "PrivacyManifests", "App"), + [ + "PrivacyInfo.xcprivacy", + ] + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithOnDemandResources: TuistAcceptanceTestCase { + func test_ios_app_with_on_demand_resources() async throws { + try await setUpFixture(.iosAppWithOnDemandResources) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + let pbxprojPath = xcodeprojPath.appending(component: "project.pbxproj") + let data = try Data(contentsOf: pbxprojPath.url) + let pbxProj = try PBXProj(data: data) + let attributes = try XCTUnwrap(pbxProj.projects.first?.attributes) + let knownAssetTags = try XCTUnwrap(attributes["KnownAssetTags"] as? [String]) + let givenTags = [ + "ar-resource-group", + "cube-texture", + "data", + "data file", + "datafile", + "datafolder", + "image", + "image-stack", + "json", + "nestedimage", + "newfolder", + "sprite", + "tag with space", + "texture", + ] + XCTAssertEqual(knownAssetTags, givenTags) + } +} + +final class GenerateAcceptanceTestiOSAppWithPrivacyManifest: TuistAcceptanceTestCase { + func test_ios_app_with_privacy_manifest() async throws { + try await setUpFixture(.iosAppWithPrivacyManifest) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + try XCTAssertDirectoryContentEqual( + fixturePath.appending(components: "Derived", "PrivacyManifests", "MyApp"), + [ + "PrivacyInfo.xcprivacy", + ] + ) } } final class GenerateAcceptanceTestIosAppWithCustomDevelopmentRegion: TuistAcceptanceTestCase { func test_ios_app_with_custom_development_region() async throws { - try setUpFixture(.iosAppWithCustomDevelopmentRegion) + try await setUpFixture(.iosAppWithCustomDevelopmentRegion) try await run(GenerateCommand.self) try await run(BuildCommand.self) for resource in [ @@ -164,7 +213,7 @@ final class GenerateAcceptanceTestIosAppWithCustomDevelopmentRegion: TuistAccept final class GenerateAcceptanceTestiOSAppWithCustomResourceParserOptions: TuistAcceptanceTestCase { func test_ios_app_with_custom_resource_parser_options() async throws { - try setUpFixture(.iosWppWithCustomResourceParserOptions) + try await setUpFixture(.iosWppWithCustomResourceParserOptions) try await run(GenerateCommand.self) try await run(BuildCommand.self) for resource in [ @@ -203,7 +252,8 @@ final class GenerateAcceptanceTestiOSAppWithCustomResourceParserOptions: TuistAc final class GenerateAcceptanceTestiOSAppWithFrameworkLinkingStaticFramework: TuistAcceptanceTestCase { func test_ios_app_with_framework_linking_static_framework() async throws { - try setUpFixture(.iosAppWithFrameworkLinkingStaticFramework) + try await setUpFixture(.iosAppWithFrameworkLinkingStaticFramework) + try await run(GenerateCommand.self) try await run(BuildCommand.self) try await XCTAssertProductWithDestinationContainsResource( @@ -228,24 +278,41 @@ final class GenerateAcceptanceTestiOSAppWithFrameworkLinkingStaticFramework: Tui final class GenerateAcceptanceTestsiOSAppWithCustomScheme: TuistAcceptanceTestCase { func test_ios_app_with_custom_scheme() async throws { - try setUpFixture(.iosAppWithCustomScheme) + try await setUpFixture(.iosAppWithCustomScheme) + try await run(GenerateCommand.self) 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") + + let xcodeprojPath = fixturePath.appending(components: ["App", "MainApp.xcodeproj"]) + + try XCTAssertContainsSimulatedLocation( + xcodeprojPath: xcodeprojPath, + scheme: "App-Debug", + testTarget: "AppTests", + simulatedLocation: "Rio de Janeiro, Brazil" + ) + try XCTAssertContainsSimulatedLocation( + xcodeprojPath: xcodeprojPath, + scheme: "App-Release", + testTarget: "AppTests", + simulatedLocation: "Grand Canyon.gpx" + ) } } final class GenerateAcceptanceTestiOSAppWithLocalSwiftPackage: TuistAcceptanceTestCase { func test_ios_app_with_local_swift_package() async throws { - try setUpFixture(.iosAppWithLocalSwiftPackage) + try await setUpFixture(.iosAppWithLocalSwiftPackage) + try await run(GenerateCommand.self) try await run(BuildCommand.self) } } final class GenerateAcceptanceTestiOSAppWithMultiConfigs: TuistAcceptanceTestCase { func test_ios_app_with_multi_configs() async throws { - try setUpFixture(.iosAppWithMultiConfigs) + try await setUpFixture(.iosAppWithMultiConfigs) try await run(GenerateCommand.self) try await XCTAssertSchemeContainsBuildSettings( "App", @@ -288,7 +355,7 @@ final class GenerateAcceptanceTestiOSAppWithMultiConfigs: TuistAcceptanceTestCas final class GenerateAcceptanceTestiOSAppWithIncompatibleXcode: TuistAcceptanceTestCase { func test_ios_app_with_incompatible_xcode() async throws { - try setUpFixture(.iosAppWithIncompatibleXcode) + try await setUpFixture(.iosAppWithIncompatibleXcode) do { try await run(GenerateCommand.self) XCTFail("Generate should have failed") @@ -307,7 +374,7 @@ final class GenerateAcceptanceTestiOSAppWithIncompatibleXcode: TuistAcceptanceTe // 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 setUpFixture("ios_app_with_actions") // try await run(GenerateCommand.self) // try await run(BuildCommand.self) // @@ -345,7 +412,7 @@ final class GenerateAcceptanceTestiOSAppWithIncompatibleXcode: TuistAcceptanceTe final class GenerateAcceptanceTestiOSAppWithBuildVariables: TuistAcceptanceTestCase { func test_ios_app_with_build_variables() async throws { - try setUpFixture(.iosAppWithBuildVariables) + try await setUpFixture(.iosAppWithBuildVariables) try await run(GenerateCommand.self) let xcodeproj = try XcodeProj( pathString: fixturePath.appending(components: "App", "App.xcodeproj").pathString @@ -367,7 +434,7 @@ final class GenerateAcceptanceTestiOSAppWithBuildVariables: TuistAcceptanceTestC final class GenerateAcceptanceTestiOSAppWithRemoteSwiftPackage: TuistAcceptanceTestCase { func test_ios_app_with_remote_swift_package() async throws { - try setUpFixture(.iosAppWithRemoteSwiftPackage) + try await setUpFixture(.iosAppWithRemoteSwiftPackage) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -375,7 +442,7 @@ final class GenerateAcceptanceTestiOSAppWithRemoteSwiftPackage: TuistAcceptanceT final class GenerateAcceptanceTestVisionOSAppWithRemoteSwiftPackage: TuistAcceptanceTestCase { func test_visionos_app() async throws { - try setUpFixture(.visionosApp) + try await setUpFixture(.visionosApp) try await run(GenerateCommand.self) // TODO: Fix // try await run(BuildCommand.self) @@ -384,7 +451,7 @@ final class GenerateAcceptanceTestVisionOSAppWithRemoteSwiftPackage: TuistAccept final class GenerateAcceptanceTestiOSAppWithLocalBinarySwiftPackage: TuistAcceptanceTestCase { func test_ios_app_with_local_binary_swift_package() async throws { - try setUpFixture(.iosAppWithLocalBinarySwiftPackage) + try await setUpFixture(.iosAppWithLocalBinarySwiftPackage) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -392,7 +459,7 @@ final class GenerateAcceptanceTestiOSAppWithLocalBinarySwiftPackage: TuistAccept final class GenerateAcceptanceTestiOSAppWithExtensions: TuistAcceptanceTestCase { func test_ios_app_with_extensions() async throws { - try setUpFixture(.iosAppWithExtensions) + try await setUpFixture(.iosAppWithExtensions) try await run(GenerateCommand.self) try await run(BuildCommand.self, "App") @@ -415,13 +482,33 @@ final class GenerateAcceptanceTestiOSAppWithExtensions: TuistAcceptanceTestCase "App.app", destination: "Debug-iphonesimulator" ) + try await XCTAssertProductWithDestinationContainsResource( + "WidgetExtension.appex", + destination: "Debug-iphonesimulator", + resource: "Bundle.bundle/dummy.jpg" + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithExtensionAndTests: TuistAcceptanceTestCase { + func test_ios_app_with_extension_and_tests() async throws { + try await setUpFixture(.iosAppWithExtensionAndTests) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + + try await XCTAssertProductWithDestinationContainsExtension( + "App.app", + destination: "Debug-iphonesimulator", + extension: "AppExtension" + ) + try await run(TestCommand.self, "--test-targets", "AppExtensionTests/ExtensionTests") } } // TODO: Fix – tvOS // final class GenerateAcceptanceTestTvOSAppWithExtensions: TuistAcceptanceTestCase { // func test_tvos_app_with_extensions() async throws { -// try setUpFixture("tvos_app_with_extensions") +// try await setUpFixture("tvos_app_with_extensions") // try await run(GenerateCommand.self) // try await run(BuildCommand.self) // try await XCTAssertProductWithDestinationContainsExtension( @@ -438,7 +525,7 @@ final class GenerateAcceptanceTestiOSAppWithExtensions: TuistAcceptanceTestCase final class GenerateAcceptanceTestiOSAppWithWatchApp2: TuistAcceptanceTestCase { func test_ios_app_with_watchapp2() async throws { - try setUpFixture(.iosAppWithWatchapp2) + try await setUpFixture(.iosAppWithWatchapp2) try await run(GenerateCommand.self) try await run(BuildCommand.self, "App") try await XCTAssertProductWithDestinationContainsResource( @@ -464,7 +551,7 @@ final class GenerateAcceptanceTestiOSAppWithWatchApp2: TuistAcceptanceTestCase { final class GenerateAcceptanceTestInvalidManifest: TuistAcceptanceTestCase { func test_invalid_manifest() async throws { - try setUpFixture(.invalidManifest) + try await setUpFixture(.invalidManifest) do { try await run(GenerateCommand.self) XCTFail("Generate command should have failed") @@ -476,14 +563,14 @@ final class GenerateAcceptanceTestInvalidManifest: TuistAcceptanceTestCase { final class GenerateAcceptanceTestiOSAppLarge: TuistAcceptanceTestCase { func test_ios_app_large() async throws { - try setUpFixture(.iosAppLarge) + try await setUpFixture(.iosAppLarge) try await run(GenerateCommand.self) } } final class GenerateAcceptanceTestiOSWorkspaceWithDependencyCycle: TuistAcceptanceTestCase { func test_ios_workspace_with_dependency_cycle() async throws { - try setUpFixture(.iosWorkspaceWithDependencyCycle) + try await setUpFixture(.iosWorkspaceWithDependencyCycle) do { try await run(GenerateCommand.self) XCTFail("Generate command should have failed") @@ -495,7 +582,7 @@ final class GenerateAcceptanceTestiOSWorkspaceWithDependencyCycle: TuistAcceptan final class GenerateAcceptanceTestFrameworkWithEnvironmentVariables: TuistAcceptanceTestCase { func test_framework_with_environment_variables() async throws { - try setUpFixture(.frameworkWithEnvironmentVariables) + try await setUpFixture(.frameworkWithEnvironmentVariables) environment.manifestLoadingVariables["TUIST_FRAMEWORK_NAME"] = "FrameworkA" try await run(GenerateCommand.self) try await run(BuildCommand.self, "FrameworkA") @@ -507,7 +594,7 @@ final class GenerateAcceptanceTestFrameworkWithEnvironmentVariables: TuistAccept final class GenerateAcceptanceTestiOSAppWithCoreData: TuistAcceptanceTestCase { func test_ios_app_with_coredata() async throws { - try setUpFixture(.iosAppWithCoreData) + try await setUpFixture(.iosAppWithCoreData) try await run(GenerateCommand.self) try await run(BuildCommand.self) for resource in [ @@ -522,27 +609,51 @@ final class GenerateAcceptanceTestiOSAppWithCoreData: TuistAcceptanceTestCase { resource: resource ) } + XCTAssertTrue( + FileHandler.shared.exists( + fixturePath.appending( + components: [ + "Derived", + "Sources", + "TuistCoreData+App.swift", + ] + ) + ) + ) } } final class GenerateAcceptanceTestiOSAppWithAppClip: TuistAcceptanceTestCase { func test_ios_app_with_appclip() async throws { - try setUpFixture(.iosAppWithAppClip) + try await 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" + architecture: "arm64" + ) + try XCTAssertFrameworkEmbedded("Framework", by: "AppClip1") + try await XCTAssertProductWithDestinationContainsAppClipWithArchitecture( + "App.app", + destination: "Debug-iphonesimulator", + appClip: "AppClip1", + architecture: "arm64" ) try XCTAssertFrameworkEmbedded("Framework", by: "AppClip1") + try await XCTAssertProductWithDestinationContainsExtension( + "AppClip1.app", + destination: "Debug-iphonesimulator", + extension: "AppClip1Widgets" + ) } } final class GenerateAcceptanceTestCommandLineToolBase: TuistAcceptanceTestCase { func test_command_line_tool_basic() async throws { - try setUpFixture(.commandLineToolBasic) + try await setUpFixture(.commandLineToolBasic) + try await run(InstallCommand.self) try await run(GenerateCommand.self) try await run(BuildCommand.self, "CommandLineTool") } @@ -550,7 +661,7 @@ final class GenerateAcceptanceTestCommandLineToolBase: TuistAcceptanceTestCase { final class GenerateAcceptanceTestCommandLineToolWithStaticLibrary: TuistAcceptanceTestCase { func test_command_line_tool_with_static_library() async throws { - try setUpFixture(.commandLineToolWithStaticLibrary) + try await setUpFixture(.commandLineToolWithStaticLibrary) try await run(GenerateCommand.self) try await run(BuildCommand.self, "CommandLineTool") } @@ -558,7 +669,7 @@ final class GenerateAcceptanceTestCommandLineToolWithStaticLibrary: TuistAccepta final class GenerateAcceptanceTestCommandLineToolWithDynamicLibrary: TuistAcceptanceTestCase { func test_command_line_tool_with_dynamic_library() async throws { - try setUpFixture(.commandLineToolWithDynamicLibrary) + try await setUpFixture(.commandLineToolWithDynamicLibrary) try await run(GenerateCommand.self) try await run(BuildCommand.self, "CommandLineTool") } @@ -566,7 +677,7 @@ final class GenerateAcceptanceTestCommandLineToolWithDynamicLibrary: TuistAccept final class GenerateAcceptanceTestCommandLineToolWithDynamicFramework: TuistAcceptanceTestCase { func test_command_line_tool_with_dynamic_framework() async throws { - try setUpFixture(.commandLineToolWithDynamicFramework) + try await setUpFixture(.commandLineToolWithDynamicFramework) try await run(GenerateCommand.self) try await run(BuildCommand.self, "CommandLineTool") } @@ -574,7 +685,7 @@ final class GenerateAcceptanceTestCommandLineToolWithDynamicFramework: TuistAcce final class GenerateAcceptanceTestmacOSAppWithCopyFiles: TuistAcceptanceTestCase { func test_macos_app_with_copy_files() async throws { - try setUpFixture(.macosAppWithCopyFiles) + try await setUpFixture(.macosAppWithCopyFiles) try await run(GenerateCommand.self) try await run(BuildCommand.self) @@ -592,15 +703,29 @@ final class GenerateAcceptanceTestmacOSAppWithCopyFiles: TuistAcceptanceTestCase final class GenerateAcceptanceTestManifestWithLogs: TuistAcceptanceTestCase { func test_manifest_with_logs() async throws { - try setUpFixture(.manifestWithLogs) + try await setUpFixture(.manifestWithLogs) try await run(GenerateCommand.self) XCTAssertStandardOutput(pattern: "Target name - App") } } +final class GenerateAcceptanceTestsProjectWithClassPrefix: TuistAcceptanceTestCase { + func test_project_with_class_prefix() async throws { + try await setUpFixture(.projectWithClassPrefix) + try await run(GenerateCommand.self) + + let xcodeproj = try XcodeProj( + pathString: xcodeprojPath.pathString + ) + let attributes = try xcodeproj.pbxproj.rootProject()?.attributes + + XCTAssertEqual(attributes?["CLASSPREFIX"] as? String, "TUIST") + } +} + final class GenerateAcceptanceTestProjectWithFileHeaderTemplate: TuistAcceptanceTestCase { func test_project_with_file_header_template() async throws { - try setUpFixture(.projectWithFileHeaderTemplate) + try await setUpFixture(.projectWithFileHeaderTemplate) try await run(GenerateCommand.self) XCTAssertTrue( FileHandler.shared.exists( @@ -617,7 +742,7 @@ final class GenerateAcceptanceTestProjectWithFileHeaderTemplate: TuistAcceptance final class GenerateAcceptanceTestProjectWithInlineFileHeaderTemplate: TuistAcceptanceTestCase { func test_project_with_inline_file_header_template() async throws { - try setUpFixture(.projectWithInlineFileHeaderTemplate) + try await setUpFixture(.projectWithInlineFileHeaderTemplate) try await run(GenerateCommand.self) XCTAssertTrue( FileHandler.shared.exists( @@ -634,7 +759,7 @@ final class GenerateAcceptanceTestProjectWithInlineFileHeaderTemplate: TuistAcce final class GenerateAcceptanceTestWorkspaceWithFileHeaderTemplate: TuistAcceptanceTestCase { func test_workspace_with_file_header_template() async throws { - try setUpFixture(.workspaceWithFileHeaderTemplate) + try await setUpFixture(.workspaceWithFileHeaderTemplate) try await run(GenerateCommand.self) XCTAssertTrue( FileHandler.shared.exists( @@ -651,7 +776,7 @@ final class GenerateAcceptanceTestWorkspaceWithFileHeaderTemplate: TuistAcceptan final class GenerateAcceptanceTestWorkspaceWithInlineFileHeaderTemplate: TuistAcceptanceTestCase { func test_workspace_with_inline_file_header_template() async throws { - try setUpFixture(.workspaceWithInlineFileHeaderTemplate) + try await setUpFixture(.workspaceWithInlineFileHeaderTemplate) try await run(GenerateCommand.self) XCTAssertTrue( FileHandler.shared.exists( @@ -668,7 +793,7 @@ final class GenerateAcceptanceTestWorkspaceWithInlineFileHeaderTemplate: TuistAc final class GenerateAcceptanceTestiOSAppWithFrameworkAndDisabledResources: TuistAcceptanceTestCase { func test_ios_app_with_framework_and_disabled_resources() async throws { - try setUpFixture(.iosAppWithFrameworkAndDisabledResources) + try await setUpFixture(.iosAppWithFrameworkAndDisabledResources) try await run(GenerateCommand.self) XCTAssertFalse( FileHandler.shared.exists( @@ -711,7 +836,7 @@ final class GenerateAcceptanceTestiOSAppWithFrameworkAndDisabledResources: Tuist final class GenerateAcceptanceTestmacOSAppWithExtensions: TuistAcceptanceTestCase { func test_macos_app_with_extensions() async throws { - try setUpFixture(.macosAppWithExtensions) + try await setUpFixture(.macosAppWithExtensions) let sdkPkgPath = sourceRootPath .appending( components: [ @@ -733,7 +858,8 @@ final class GenerateAcceptanceTestmacOSAppWithExtensions: TuistAcceptanceTestCas final class GenerateAcceptanceTestiOSAppWithImplicitDependencies: TuistAcceptanceTestCase { func test_ios_app_with_implicit_dependencies() async throws { - try setUpFixture(.iosAppWithImplicitDependencies) + try await setUpFixture(.iosAppWithImplicitDependencies) + try await run(GenerateCommand.self) try await run(BuildCommand.self, "FrameworkC") do { try await run(BuildCommand.self, "App") @@ -746,6 +872,48 @@ final class GenerateAcceptanceTestiOSAppWithImplicitDependencies: TuistAcceptanc } } +final class GenerateAcceptanceTestSPMPackage: TuistAcceptanceTestCase { + func test_spm_package() async throws { + try await setUpFixture(.spmPackage) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "MyPackage", "--platform", "ios") + try await run(BuildCommand.self, "MyPackage", "--platform", "macos") + try await run(BuildCommand.self, "MyUIKitPackage", "--platform", "ios") + try await run(BuildCommand.self, "MyCLI") + try await run(TestCommand.self, "--platform", "ios") + try await run(TestCommand.self, "MyPackage", "--platform", "macos") + } +} + +final class GenerateAcceptanceTestAppWithDefaultConfiguration: TuistAcceptanceTestCase { + func test_app_with_custom_default_configuration() async throws { + try await setUpFixture(.appWithCustomDefaultConfiguration) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestAppWithGoogleMaps: TuistAcceptanceTestCase { + func test_app_with_google_maps() async throws { + try await setUpFixture(.appWithGoogleMaps) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestFrameworkWithMacroAndPluginPackages: TuistAcceptanceTestCase { + func test_framework_with_macro_and_plugin_packages() async throws { + try await setUpFixture(.frameworkWithMacroAndPluginPackages) + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "--", "-skipPackagePluginValidation") + } +} + +// frameworkWithMacroAndPluginPackages + extension TuistAcceptanceTestCase { private func resourcePath( for productName: String, diff --git a/Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift similarity index 89% rename from Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift rename to Tests/TuistGeneratorIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift index 1f452014bdd..a0ef9485dc8 100644 --- a/Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift @@ -1,8 +1,10 @@ +import MockableTest +import Path import TSCBasic +import struct TSCUtility.Version import TuistCore -import TuistGraph -import TuistGraphTesting import TuistLoaderTesting +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -13,26 +15,35 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { override func setUp() { super.setUp() do { - system.swiftVersionStub = { "5.2" } - xcodeController.selectedVersionStub = .success("11.0.0") + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + + given(xcodeController) + .selectedVersion() + .willReturn(TSCUtility.Version(11, 0, 0)) try setupTestProject() } catch { XCTFail(error.localizedDescription) } } - func testGenerateThrowsLintingErrorWhenConfigurationsAreEmpty() throws { + func testGenerateThrowsLintingErrorWhenConfigurationsAreEmpty() async throws { // Given let projectSettings = Settings(configurations: [:]) let targetSettings: Settings? = nil // When / Then - XCTAssertThrowsError( - try generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) - ) + var _error: Error? + do { + try await generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) + } catch { + _error = error + } + XCTAssertNotNil(_error) } - func testGenerateWhenSingleDebugConfigurationInProject() throws { + func testGenerateWhenSingleDebugConfigurationInProject() async throws { // Given let projectSettings = Settings( base: ["A": "A"], @@ -40,7 +51,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: nil) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: nil) // Then assertProject(expectedConfigurations: ["Debug"]) @@ -50,7 +61,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertTrue(debug.contains("A", "A")) // from base } - func testGenerateWhenConfigurationSettingsOverrideXCConfig() throws { + func testGenerateWhenConfigurationSettingsOverrideXCConfig() async throws { // Given let debugFilePath = try createFile(path: "Configs/debug.xcconfig", content: """ A=A_XCCONFIG @@ -63,7 +74,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { let projectSettings = Settings(configurations: [.debug: debugConfiguration]) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: nil) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: nil) // Then assertProject(expectedConfigurations: ["Debug"]) @@ -75,7 +86,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertTrue(debug.contains("C", "C")) // from settings } - func testGenerateWhenConfigurationSettingsOverrideBase() throws { + func testGenerateWhenConfigurationSettingsOverrideBase() async throws { // Given let debugConfiguration = Configuration(settings: ["A": "A", "C": "C"]) let projectSettings = Settings( @@ -84,7 +95,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: nil) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: nil) // Then assertProject(expectedConfigurations: ["Debug"]) @@ -96,7 +107,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertTrue(debug.contains("C", "C")) // from settings } - func testGenerateWhenBuildConfigurationWithCustomName() throws { + func testGenerateWhenBuildConfigurationWithCustomName() async throws { // Given let customConfiguration = Configuration(settings: ["A": "A", "C": "C"]) let projectSettings = Settings( @@ -108,7 +119,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: nil) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: nil) // Then assertProject(expectedConfigurations: ["Custom", "Release"]) @@ -125,7 +136,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertFalse(release.contains("C", "C")) // non-existing, only defined in Custom } - func testGenerateWhenTargetSettingsOverrideTargetXCConfig() throws { + func testGenerateWhenTargetSettingsOverrideTargetXCConfig() async throws { // Given let debugFilePath = try createFile(path: "Configs/debug.xcconfig", content: """ A=A_XCCONFIG @@ -139,7 +150,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { let targetSettings = Settings(configurations: [.debug: debugConfiguration]) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) // Then assertProject(expectedConfigurations: ["Debug"]) @@ -151,7 +162,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertTrue(debug.contains("C", "C")) // from target settings } - func testGenerateWhenMultipleConfigurations() throws { + func testGenerateWhenMultipleConfigurations() async throws { // Given let projectDebugConfiguration = Configuration(settings: [ "A": "A_PROJECT_DEBUG", @@ -175,7 +186,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ]) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) // Then assertProject(expectedConfigurations: ["Debug", "ProjectRelease"]) @@ -205,7 +216,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { - target base - target configuraiton settings */ - func testGenerateWhenTargetSettingsOverrideProjectBaseSettingsAndXCConfig() throws { + func testGenerateWhenTargetSettingsOverrideProjectBaseSettingsAndXCConfig() async throws { // Given let projectDebugFilePath = try createFile(path: "Configs/project_debug.xcconfig", content: """ A=A_PROJECT_XCCONFIG @@ -263,7 +274,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: targetSettings) // Then assertProject(expectedConfigurations: ["Debug"]) @@ -284,7 +295,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertTrue(debug.contains("TARGET", "YES")) // from target settings } - func testGenerateWhenCustomConfigurations() throws { + func testGenerateWhenCustomConfigurations() async throws { // Given let projectDebugConfiguration = Configuration(settings: [ "A": "A_PROJECT_DEBUG", @@ -304,7 +315,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ]) // When - try generateWorkspace(projectSettings: projectSettings, targetSettings: nil) + try await generateWorkspace(projectSettings: projectSettings, targetSettings: nil) // Then assertProject(expectedConfigurations: ["CustomDebug", "CustomRelease", "Debug", "Release"]) @@ -318,8 +329,9 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { XCTAssertTrue(debug.contains("GCC_PREPROCESSOR_DEFINITIONS", "DEBUG=1")) XCTAssertTrue(customDebug.contains("GCC_PREPROCESSOR_DEFINITIONS", "DEBUG=1")) - XCTAssertTrue(debug.contains("SWIFT_ACTIVE_COMPILATION_CONDITIONS", "DEBUG")) - XCTAssertTrue(customDebug.contains("SWIFT_ACTIVE_COMPILATION_CONDITIONS", "DEBUG")) + // These include a prefix space because $(inherited) + XCTAssertTrue(debug.contains("SWIFT_ACTIVE_COMPILATION_CONDITIONS", " DEBUG")) + XCTAssertTrue(customDebug.contains("SWIFT_ACTIVE_COMPILATION_CONDITIONS", " DEBUG")) XCTAssertFalse(release.contains("SWIFT_ACTIVE_COMPILATION_CONDITIONS", "DEBUG")) XCTAssertFalse(customRelease.contains("SWIFT_ACTIVE_COMPILATION_CONDITIONS", "DEBUG")) @@ -331,7 +343,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { // MARK: - Helpers - private func generateWorkspace(projectSettings: Settings, targetSettings: Settings?) throws { + private func generateWorkspace(projectSettings: Settings, targetSettings: Settings?) async throws { let models = try createModels(projectSettings: projectSettings, targetSettings: targetSettings) let subject = DescriptorGenerator() let writer = XcodeProjWriter() @@ -343,7 +355,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { let graphTraverser = GraphTraverser(graph: graph) try linter.lint(graphTraverser: graphTraverser, config: config).printAndThrowErrorsIfNeeded() let descriptor = try subject.generateWorkspace(graphTraverser: graphTraverser) - try writer.write(workspace: descriptor) + try await writer.write(workspace: descriptor) } private func setupTestProject() throws { @@ -351,7 +363,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { } @discardableResult - private func createFile(path relativePath: String, content: String) throws -> AbsolutePath { + private func createFile(path relativePath: String, content: String) throws -> Path.AbsolutePath { let temporaryPath = try temporaryPath() let absolutePath = temporaryPath.appending(try RelativePath(validating: relativePath)) try FileHandler.shared.touch(absolutePath) @@ -375,8 +387,8 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { private func createConfig() -> Config { Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: .default, + fullHandle: nil, + url: Constants.URLs.production, swiftVersion: nil, plugins: [], generationOptions: .test(), @@ -384,7 +396,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ) } - private func createWorkspace(path: AbsolutePath, projects: [String]) throws -> Workspace { + private func createWorkspace(path: Path.AbsolutePath, projects: [String]) throws -> Workspace { Workspace( path: path, xcWorkspacePath: path.appending(component: "Workspace.xcworkspace"), @@ -395,7 +407,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { } private func createProject( - path: AbsolutePath, + path: Path.AbsolutePath, settings: Settings, targets: [Target], packages: [Package] = [], @@ -407,6 +419,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { xcodeProjPath: path.appending(component: "App.xcodeproj"), name: "App", organizationName: nil, + classPrefix: nil, defaultKnownRegions: nil, developmentRegion: nil, options: .test(), @@ -436,7 +449,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { ) } - private func pathTo(_ relativePath: String) throws -> AbsolutePath { + private func pathTo(_ relativePath: String) throws -> Path.AbsolutePath { let temporaryPath = try temporaryPath() return temporaryPath.appending(try RelativePath(validating: relativePath)) } @@ -517,7 +530,7 @@ private func extractBuildSettings(path: XcodePath) throws -> ExtractedBuildSetti arguments.append(scheme) } - let rawBuildSettings = try TSCBasic.Process.checkNonZeroExit(arguments: arguments) + let rawBuildSettings = try Process.checkNonZeroExit(arguments: arguments) return ExtractedBuildSettings(rawBuildSettings: rawBuildSettings) } diff --git a/Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/StableStructureIntegrationTests.swift similarity index 97% rename from Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift rename to Tests/TuistGeneratorIntegrationTests/Generator/StableStructureIntegrationTests.swift index 6e8a57d4b80..67ad0bfe0e8 100644 --- a/Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/StableStructureIntegrationTests.swift @@ -1,9 +1,9 @@ -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph import TuistLoaderTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @@ -12,7 +12,7 @@ import XCTest @testable import TuistSupportTesting final class StableXcodeProjIntegrationTests: TuistTestCase { - func testXcodeProjStructureDoesNotChangeAfterRegeneration() throws { + func testXcodeProjStructureDoesNotChangeAfterRegeneration() async throws { // Given let temporaryPath = try temporaryPath() var capturedProjects = [[XcodeProj]]() @@ -43,7 +43,7 @@ final class StableXcodeProjIntegrationTests: TuistTestCase { // Note: While we already have access to the `XcodeProj` models in `workspaceDescriptor` // unfortunately they are not equatable, however once serialized & deserialized back they are - try writer.write(workspace: workspaceDescriptor) + try await writer.write(workspace: workspaceDescriptor) let xcworkspace = try XCWorkspace(path: workspaceDescriptor.xcworkspacePath.path) let xcodeProjs = try findXcodeProjs(in: xcworkspace) let sharedSchemes = try findSharedSchemes(in: xcworkspace) diff --git a/Tests/TuistGeneratorIntegrationTests/Generator/SwiftPackageManagerInteractorTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/SwiftPackageManagerInteractorTests.swift index 65cc50153f3..9e538e07e36 100644 --- a/Tests/TuistGeneratorIntegrationTests/Generator/SwiftPackageManagerInteractorTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/SwiftPackageManagerInteractorTests.swift @@ -1,11 +1,10 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -61,8 +60,8 @@ final class SwiftPackageManagerInteractorTests: TuistUnitTestCase { let temporaryPath = try temporaryPath() let config = Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, + fullHandle: nil, + url: Constants.URLs.production, swiftVersion: nil, plugins: [], generationOptions: .test(resolveDependenciesWithSystemScm: true), @@ -185,8 +184,8 @@ final class SwiftPackageManagerInteractorTests: TuistUnitTestCase { let temporaryPath = try temporaryPath() let config = Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, + fullHandle: nil, + url: Constants.URLs.production, swiftVersion: nil, plugins: [], generationOptions: .test(clonedSourcePackagesDirPath: temporaryPath.appending(component: "spm")), diff --git a/Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift b/Tests/TuistGeneratorIntegrationTests/Generator/TestModelGenerator.swift similarity index 97% rename from Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift rename to Tests/TuistGeneratorIntegrationTests/Generator/TestModelGenerator.swift index 6959521015d..4d96b04dc94 100644 --- a/Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/TestModelGenerator.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph import TuistLoaderTesting +import XcodeGraph @testable import TuistGenerator @testable import TuistSupport @testable import TuistSupportTesting @@ -152,6 +152,7 @@ final class TestModelGenerator { xcodeProjPath: path.appending(component: "App.xcodeproj"), name: name, organizationName: nil, + classPrefix: nil, defaultKnownRegions: nil, developmentRegion: nil, options: .test(), @@ -217,7 +218,7 @@ final class TestModelGenerator { return Headers(public: publicHeaders, private: privateHeaders, project: projectHeaders) } - private func createResources(path: AbsolutePath) throws -> [ResourceFileElement] { + private func createResources(path: AbsolutePath) throws -> ResourceFileElements { let files = try (0 ..< config.resources) .map { "Resources/Resource\($0).png" } .map { ResourceFileElement.file(path: path.appending(try RelativePath(validating: $0))) } @@ -226,7 +227,7 @@ final class TestModelGenerator { .map { "Resources/Folder\($0)" } .map { ResourceFileElement.folderReference(path: path.appending(try RelativePath(validating: $0))) } - return (files + folderReferences).shuffled() + return ResourceFileElements((files + folderReferences).shuffled()) } private func createAdditionalFiles(path: AbsolutePath) throws -> [FileElement] { @@ -332,7 +333,7 @@ final class TestModelGenerator { expandVariableFromTarget: nil, preActions: createExecutionActions(), postActions: createExecutionActions(), - diagnosticsOptions: [] + diagnosticsOptions: SchemeDiagnosticsOptions() ), runAction: RunAction( configurationName: "Debug", @@ -341,7 +342,7 @@ final class TestModelGenerator { executable: nil, filePath: nil, arguments: createArguments(), - diagnosticsOptions: [] + diagnosticsOptions: SchemeDiagnosticsOptions() ), archiveAction: ArchiveAction( configurationName: "Debug", diff --git a/Tests/TuistIntegrationTests/Generator/TuistGeneratorPerformanceTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/TuistGeneratorPerformanceTests.swift similarity index 98% rename from Tests/TuistIntegrationTests/Generator/TuistGeneratorPerformanceTests.swift rename to Tests/TuistGeneratorIntegrationTests/Generator/TuistGeneratorPerformanceTests.swift index b9f0e868ed0..bc94fa514fd 100644 --- a/Tests/TuistIntegrationTests/Generator/TuistGeneratorPerformanceTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/TuistGeneratorPerformanceTests.swift @@ -1,9 +1,9 @@ -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph import TuistLoaderTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest diff --git a/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift index b51a11ef1b5..ec7fad19bb0 100644 --- a/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator diff --git a/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift index f135e1fa7be..8a98586560f 100644 --- a/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistGeneratorTesting import TuistSupport @@ -22,20 +22,20 @@ final class XcodeProjWriterTests: TuistTestCase { super.tearDown() } - func test_writeProject() throws { + func test_writeProject() async throws { // Given let path = try temporaryPath() let xcodeProjPath = path.appending(component: "Project.xcodeproj") let descriptor = ProjectDescriptor.test(path: path, xcodeprojPath: xcodeProjPath) // When - try subject.write(project: descriptor) + try await subject.write(project: descriptor) // Then XCTAssertTrue(FileHandler.shared.exists(xcodeProjPath)) } - func test_writeProject_fileSideEffects() throws { + func test_writeProject_fileSideEffects() async throws { // Given let path = try temporaryPath() let xcodeProjPath = path.appending(component: "Project.xcodeproj") @@ -52,7 +52,7 @@ final class XcodeProjWriterTests: TuistTestCase { ) // When - try subject.write(project: descriptor) + try await subject.write(project: descriptor) // Then let fileHandler = FileHandler.shared @@ -60,7 +60,7 @@ final class XcodeProjWriterTests: TuistTestCase { XCTAssertEqual(try fileHandler.readFile(filePath), contents) } - func test_writeProject_deleteFileSideEffects() throws { + func test_writeProject_deleteFileSideEffects() async throws { // Given let path = try temporaryPath() let xcodeProjPath = path.appending(component: "Project.xcodeproj") @@ -76,13 +76,13 @@ final class XcodeProjWriterTests: TuistTestCase { ) // When - try subject.write(project: descriptor) + try await subject.write(project: descriptor) // Then XCTAssertFalse(fileHandler.exists(filePath)) } - func test_generate_doesNotWipeUserData() throws { + func test_generate_doesNotWipeUserData() async throws { // Given let path = try temporaryPath() let paths = try createFiles([ @@ -98,14 +98,14 @@ final class XcodeProjWriterTests: TuistTestCase { // When for _ in 0 ..< 2 { - try subject.write(project: descriptor) + try await subject.write(project: descriptor) } // Then XCTAssertTrue(paths.allSatisfy { FileHandler.shared.exists($0) }) } - func test_generate_replacesProjectSharedSchemes() throws { + func test_generate_replacesProjectSharedSchemes() async throws { // Given let path = try temporaryPath() let xcodeProjPath = path.appending(component: "Project.xcodeproj") @@ -125,7 +125,7 @@ final class XcodeProjWriterTests: TuistTestCase { xcodeprojPath: xcodeProjPath, schemes: schemes ) - try subject.write(project: descriptor) + try await subject.write(project: descriptor) } // Then @@ -137,7 +137,7 @@ final class XcodeProjWriterTests: TuistTestCase { ]) } - func test_generate_preservesProjectUserSchemes() throws { + func test_generate_preservesProjectUserSchemes() async throws { // Given let path = try temporaryPath() let xcodeProjPath = path.appending(component: "Project.xcodeproj") @@ -156,7 +156,7 @@ final class XcodeProjWriterTests: TuistTestCase { xcodeprojPath: xcodeProjPath, schemes: schemes ) - try subject.write(project: descriptor) + try await subject.write(project: descriptor) } // Then @@ -168,7 +168,7 @@ final class XcodeProjWriterTests: TuistTestCase { ]) } - func test_generate_replacesWorkspaceSharedSchemes() throws { + func test_generate_replacesWorkspaceSharedSchemes() async throws { // Given let path = try temporaryPath() let xcworkspacePath = path.appending(component: "Workspace.xcworkspace") @@ -188,7 +188,7 @@ final class XcodeProjWriterTests: TuistTestCase { xcworkspacePath: xcworkspacePath, schemes: schemes ) - try subject.write(workspace: descriptor) + try await subject.write(workspace: descriptor) } // Then @@ -200,7 +200,7 @@ final class XcodeProjWriterTests: TuistTestCase { ]) } - func test_generate_preservesWorkspaceUserSchemes() throws { + func test_generate_preservesWorkspaceUserSchemes() async throws { // Given let path = try temporaryPath() let xcworkspacePath = path.appending(component: "Workspace.xcworkspace") @@ -219,7 +219,7 @@ final class XcodeProjWriterTests: TuistTestCase { xcworkspacePath: xcworkspacePath, schemes: schemes ) - try subject.write(workspace: descriptor) + try await subject.write(workspace: descriptor) } // Then @@ -231,7 +231,7 @@ final class XcodeProjWriterTests: TuistTestCase { ]) } - func test_generate_local_scheme() throws { + func test_generate_local_scheme() async throws { // Given let path = try temporaryPath() let xcodeProjPath = path.appending(component: "Project.xcodeproj") @@ -239,7 +239,7 @@ final class XcodeProjWriterTests: TuistTestCase { let descriptor = ProjectDescriptor.test(path: path, xcodeprojPath: xcodeProjPath, schemes: [userScheme]) // When - try subject.write(project: descriptor) + try await subject.write(project: descriptor) // Then let fileHandler = FileHandler.shared diff --git a/Tests/TuistGeneratorTests/Extensions/Xcodeproj+ExtrasTests.swift b/Tests/TuistGeneratorTests/Extensions/Xcodeproj+ExtrasTests.swift index 491df576da1..c63f567ca24 100644 --- a/Tests/TuistGeneratorTests/Extensions/Xcodeproj+ExtrasTests.swift +++ b/Tests/TuistGeneratorTests/Extensions/Xcodeproj+ExtrasTests.swift @@ -1,8 +1,7 @@ -import TuistGraph +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator -@testable import TuistGraphTesting class XcodeprojExtrasTests: XCTestCase { func test_pbxFileElement_sort() { @@ -37,7 +36,7 @@ class XcodeprojExtrasTests: XCTestCase { // Given let target = Target.test(destinations: [.iPhone, .iPad, .mac]) let buildFile = PBXBuildFile() - let dependencyCondition: PlatformCondition = try .test([.ios, .macos]) + let dependencyCondition: PlatformCondition = try XCTUnwrap(.test([.ios, .macos])) // When buildFile.applyCondition(dependencyCondition, applicableTo: target) @@ -64,7 +63,7 @@ class XcodeprojExtrasTests: XCTestCase { // Given let target = Target.test(destinations: [.mac]) let buildFile = PBXBuildFile() - let dependencyCondition: PlatformCondition = try .test([.ios, .macos]) + let dependencyCondition: PlatformCondition = try XCTUnwrap(.test([.ios, .macos])) // When buildFile.applyCondition(dependencyCondition, applicableTo: target) @@ -78,7 +77,7 @@ class XcodeprojExtrasTests: XCTestCase { // Given let target = Target.test(destinations: [.iPhone, .iPad, .mac, .appleVision]) let buildFile = PBXBuildFile() - let dependencyCondition: PlatformCondition = try .test([.ios, .macos]) + let dependencyCondition: PlatformCondition = try XCTUnwrap(.test([.ios, .macos])) // When buildFile.applyCondition(dependencyCondition, applicableTo: target) @@ -92,7 +91,7 @@ class XcodeprojExtrasTests: XCTestCase { // Given let target = Target.test(destinations: [.iPhone, .iPad, .appleVision]) let buildFile = PBXBuildFile() - let dependencyCondition: PlatformCondition = try .test([.ios, .macos]) + let dependencyCondition: PlatformCondition = try XCTUnwrap(.test([.ios, .macos])) // When buildFile.applyCondition(dependencyCondition, applicableTo: target) @@ -106,7 +105,7 @@ class XcodeprojExtrasTests: XCTestCase { // Given let target = Target.test(destinations: [.appleVision]) let buildFile = PBXBuildFile() - let dependencyCondition: PlatformCondition = try .test([.macos]) + let dependencyCondition: PlatformCondition = try XCTUnwrap(.test([.macos])) // When buildFile.applyCondition(dependencyCondition, applicableTo: target) diff --git a/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift index b49192d0109..6b1bb9da813 100644 --- a/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift @@ -1,9 +1,10 @@ import Foundation -import TSCBasic +import MockableTest +import Path +import struct TSCUtility.Version import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -30,6 +31,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { super.setUp() subject = BuildPhaseGenerator() errorHandler = MockErrorHandler() + + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) } override func tearDown() { @@ -475,7 +480,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // When try subject.generateResourcesBuildPhase( path: "/path", - target: .test(resources: resources), + target: .test(resources: .init(resources)), graphTraverser: graphTraverser, pbxTarget: nativeTarget, fileElements: fileElements, @@ -524,7 +529,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // When try subject.generateResourcesBuildPhase( path: "/path", - target: .test(resources: files.map { .file(path: $0) }), + target: .test(resources: .init(files.map { .file(path: $0) })), graphTraverser: GraphTraverser(graph: .test(path: path)), pbxTarget: nativeTarget, fileElements: fileElements, @@ -572,7 +577,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // When try subject.generateResourcesBuildPhase( path: "/path", - target: .test(resources: resourceFiles.map { .file(path: $0) }), + target: .test(resources: .init(resourceFiles.map { .file(path: $0) })), graphTraverser: GraphTraverser(graph: .test(path: path)), pbxTarget: nativeTarget, fileElements: fileElements, @@ -591,7 +596,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { versions: [try AbsolutePath(validating: "/Model.xcdatamodeld/1.xcdatamodel")], currentVersion: "1" ) - let target = Target.test(resources: [], coreDataModels: [coreDataModel]) + let target = Target.test(resources: .init([]), coreDataModels: [coreDataModel]) let fileElements = ProjectFileElements() let pbxproj = PBXProj() @@ -627,7 +632,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given let temporaryPath = try temporaryPath() let path = try AbsolutePath(validating: "/image.png") - let target = Target.test(resources: [.file(path: path)]) + let target = Target.test(resources: .init([.file(path: path)])) let fileElements = ProjectFileElements() let pbxproj = PBXProj() let fileElement = PBXFileReference() @@ -664,7 +669,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { .file(path: "/file.type", tags: ["fileTag"]), .folderReference(path: "/folder", tags: ["folderTag"]), ] - let target = Target.test(resources: resources) + let target = Target.test(resources: .init(resources)) let fileElements = ProjectFileElements() let pbxproj = PBXProj() @@ -714,7 +719,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { .file(path: "/visionOS.type", inclusionCondition: .when([.visionos])), .file(path: "/watchOS.type", inclusionCondition: .when([.watchos])), ] - let target = Target.test(resources: resources) + let target = Target.test(resources: .init(resources)) let fileElements = ProjectFileElements() let pbxproj = PBXProj() @@ -752,7 +757,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { XCTAssertTrue(pbxBuildPhase is PBXResourcesBuildPhase) let resourceBuildPhase = try XCTUnwrap(nativeTarget.buildPhases.first as? PBXResourcesBuildPhase) - var buildFiles = try XCTUnwrap(resourceBuildPhase.files) + let buildFiles = try XCTUnwrap(resourceBuildPhase.files) for buildFile in buildFiles { // Explicitly exctracting the original path because it gets lost in translation for resource files @@ -774,12 +779,12 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [bundle1, bundle2]) - let project = Project.test(path: path) - let targets: [AbsolutePath: [String: Target]] = [project.path: [ - bundle1.name: bundle1, - bundle2.name: bundle2, - app.name: app, - ]] + let project = Project.test(path: path, targets: [ + bundle1, + bundle2, + app, + ]) + let dependencies: [GraphDependency: Set] = [ .target(name: bundle1.name, path: project.path): Set(), .target(name: bundle2.name, path: project.path): Set(), @@ -791,7 +796,6 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [project.path: project], - targets: targets, dependencies: dependencies ) let graphTraverser = GraphTraverser(graph: graph) @@ -818,27 +822,20 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given let path = try temporaryPath() let bundle = Target.test(name: "Bundle1", product: .bundle) - let projectA = Project.test(path: "/path/a") - let app = Target.test(name: "App", product: .app) - let projectB = Project.test(path: "/path/b") - let pbxproj = PBXProj() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [bundle]) - - let targets: [AbsolutePath: [String: Target]] = [ - projectA.path: [bundle.name: bundle], - projectB.path: [app.name: app], - ] + let projectA = Project.test(path: "/path/a", targets: [bundle]) + let projectB = Project.test(path: "/path/b", targets: [app]) let dependencies: [GraphDependency: Set] = [ .target(name: bundle.name, path: projectA.path): Set(), .target(name: app.name, path: projectB.path): Set([.target(name: bundle.name, path: projectA.path)]), ] + let graph = Graph.test( path: path, projects: [projectA.path: projectA, projectB.path: projectB], - targets: targets, dependencies: dependencies ) let graphTraverser = GraphTraverser(graph: graph) @@ -862,13 +859,16 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateCopyFilesBuildPhases() throws { // Given - let fonts: [FileElement] = [ + let fonts: [CopyFileElement] = [ .file(path: "/path/fonts/font1.ttf"), .file(path: "/path/fonts/font2.ttf"), - .file(path: "/path/fonts/font3.ttf"), + .file(path: "/path/fonts/font3.ttf", condition: .when([.macos])), + .file(path: "/path/fonts/font4.ttf", codeSignOnCopy: true), + .file(path: "/path/fonts/font5.ttf", condition: .when([.macos]), codeSignOnCopy: true), + .file(path: "/path/fonts/font6.ttf", codeSignOnCopy: false), ] - let templates: [FileElement] = [ + let templates: [CopyFileElement] = [ .file(path: "/path/sharedSupport/tuist.rtfd"), ] @@ -898,6 +898,27 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { "font1.ttf", "font2.ttf", "font3.ttf", + "font4.ttf", + "font5.ttf", + "font6.ttf", + ]) + + XCTAssertEqual(firstBuildPhase.files?.map(\.platformFilters), [ + nil, + nil, + ["macos"], + nil, + ["macos"], + nil, + ]) + + XCTAssertEqual(firstBuildPhase.files?.map(\.settings) as? [[String: [String]]?], [ + nil, + nil, + nil, + ["ATTRIBUTES": ["CodeSignOnCopy"]], + ["ATTRIBUTES": ["CodeSignOnCopy"]], + nil, ]) let secondBuildPhase = try XCTUnwrap(nativeTarget.buildPhases.last as? PBXCopyFilesBuildPhase) @@ -910,21 +931,15 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateAppExtensionsBuildPhase() throws { // Given let path = try temporaryPath() - 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", destinations: [.iPhone, .iPad, .mac], product: .app) let pbxproj = PBXProj() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [appExtension, stickerPackExtension]) - - let targets: [AbsolutePath: [String: Target]] = [ - projectA.path: [ - appExtension.name: appExtension, - stickerPackExtension.name: stickerPackExtension, - app.name: app, - ], - ] + let projectA = Project.test(path: "/path/a", targets: [ + appExtension, stickerPackExtension, 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) @@ -944,7 +959,6 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [projectA.path: projectA], - targets: targets, dependencies: dependencies, dependencyConditions: dependencyConditions ) @@ -984,21 +998,16 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateAppExtensionsBuildPhase_noBuildPhase_when_appDoesntHaveAppExtensions() throws { // Given let app = Target.test(name: "App", product: .app) - let project = Project.test() let pbxproj = PBXProj() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = ProjectFileElements() - - let targets: [AbsolutePath: [String: Target]] = [ - project.path: [app.name: app], - ] + let project = Project.test(targets: [app]) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set(), ] let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: targets, dependencies: dependencies ) let graphTraverser = GraphTraverser(graph: graph) @@ -1021,31 +1030,22 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given 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() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [app, watchApp]) - - let targets: [AbsolutePath: [String: Target]] = [ - project.path: [app.name: app, watchApp.name: watchApp], - ] - + let project = Project.test(targets: [app, 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] = [ 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, dependencyConditions: dependencyConditions ) @@ -1076,14 +1076,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given 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() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [app, watchApp]) - - let targets: [AbsolutePath: [String: Target]] = [ - project.path: [app.name: app, watchApp.name: watchApp], - ] + let project = Project.test(targets: [app, watchApp]) let appGraphDependency: GraphDependency = .target(name: app.name, path: project.path) let watchAppGraphDependency: GraphDependency = .target(name: watchApp.name, path: project.path) @@ -1099,7 +1095,6 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: targets, dependencies: dependencies, dependencyConditions: dependencyConditions ) @@ -1131,14 +1126,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given let app = Target.test(name: "App", platform: .macOS, product: .app) let xpcService = Target.test(name: "XPCService", platform: .macOS, product: .xpc) - let project = Project.test() let pbxproj = PBXProj() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [app, xpcService]) - - let targets: [AbsolutePath: [String: Target]] = [ - project.path: [app.name: app, xpcService.name: xpcService], - ] + let project = Project.test(targets: [app, xpcService]) let dependencies: [GraphDependency: Set] = [ .target(name: xpcService.name, path: project.path): Set(), .target(name: app.name, path: project.path): Set([.target(name: xpcService.name, path: project.path)]), @@ -1146,7 +1137,6 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: targets, dependencies: dependencies ) let graphTraverser = GraphTraverser(graph: graph) @@ -1176,14 +1166,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given let app = Target.test(name: "App", platform: .macOS, product: .app) let systemExtension = Target.test(name: "SystemExtension", platform: .macOS, product: .systemExtension) - let project = Project.test() let pbxproj = PBXProj() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [app, systemExtension]) - - let targets: [AbsolutePath: [String: Target]] = [ - project.path: [app.name: app, systemExtension.name: systemExtension], - ] + let project = Project.test(targets: [app, systemExtension]) let dependencies: [GraphDependency: Set] = [ .target(name: systemExtension.name, path: project.path): Set(), .target(name: app.name, path: project.path): Set([.target(name: systemExtension.name, path: project.path)]), @@ -1191,7 +1177,6 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: targets, dependencies: dependencies ) let graphTraverser = GraphTraverser(graph: graph) @@ -1219,7 +1204,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateTarget_actions() throws { // Given - system.swiftVersionStub = { "5.2" } + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + let fileElements = ProjectFileElements([:]) let graph = Graph.test() let graphTraverser = GraphTraverser(graph: graph) @@ -1228,7 +1216,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let pbxProject = createPbxProject(pbxproj: pbxproj) let target = Target.test( sources: [], - resources: [], + resources: .init([]), scripts: [ TargetScript( name: "post", @@ -1294,7 +1282,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateTarget_action_custom_shell() throws { // Given - system.swiftVersionStub = { "5.2" } + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + let fileElements = ProjectFileElements([:]) let graph = Graph.test() let graphTraverser = GraphTraverser(graph: graph) @@ -1303,7 +1294,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let pbxProject = createPbxProject(pbxproj: pbxproj) let target = Target.test( sources: [], - resources: [], + resources: .init([]), scripts: [ TargetScript( name: "post", @@ -1360,7 +1351,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateTarget_action_dependency_file() throws { // Given - system.swiftVersionStub = { "5.2" } + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + let fileElements = ProjectFileElements([:]) let graph = Graph.test() let graphTraverser = GraphTraverser(graph: graph) @@ -1369,7 +1363,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let pbxProject = createPbxProject(pbxproj: pbxproj) let target = Target.test( sources: [], - resources: [], + resources: .init([]), scripts: [ TargetScript( name: "post", @@ -1427,17 +1421,12 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { // Given 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() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [app, appClip]) - - let targets: [AbsolutePath: [String: Target]] = [ - project.path: [app.name: app, appClip.name: appClip], - ] + let project = Project.test(targets: [app, 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] = [ appClipGraphDependency: Set(), appGraphDependency: Set([appClipGraphDependency]), @@ -1450,7 +1439,6 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: targets, dependencies: dependencies, dependencyConditions: dependencyConditions ) @@ -1562,6 +1550,54 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { ]) } + func test_generateLinks_generatesAShellScriptBuildPhase_when_targetIsAMacroFramework() throws { + // Given + let app = Target.test(name: "app", platform: .iOS, product: .app) + 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]) + let fileElements = createProductFileElements(for: [app, macroFramework, macroExecutable]) + let pbxproj = PBXProj() + let pbxTarget = PBXNativeTarget(name: app.name) + + let graph = Graph.test(path: project.path, projects: [project.path: project], dependencies: [ + .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: macroExecutable.name, + path: project.path + )]), + .target(name: macroExecutable.name, path: project.path): Set([]), + ]) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateBuildPhases( + path: "/Project", + target: macroFramework, + graphTraverser: graphTraverser, + pbxTarget: pbxTarget, + fileElements: fileElements, + pbxproj: pbxproj + ) + + // Then + let buildPhase = pbxTarget + .buildPhases + .compactMap { $0 as? PBXShellScriptBuildPhase } + .first(where: { $0.name() == "Copy Swift Macro executable into $BUILT_PRODUCT_DIR" }) + + XCTAssertNotNil(buildPhase) + + let expectedScript = + "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("$BUILD_DIR/$CONFIGURATION/\(macroExecutable.productName)") == true) + XCTAssertEqual( + buildPhase?.outputPaths, + ["$BUILD_DIR/Debug-$EFFECTIVE_PLATFORM_NAME/\(macroExecutable.productName)"] + ) + } + // MARK: - Helpers private func createProductFileElements(for targets: [Target]) -> ProjectFileElements { diff --git a/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift index 8c97b882156..6191ef2da99 100644 --- a/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path +import struct TSCUtility.Version import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -21,6 +22,10 @@ final class ConfigGeneratorTests: TuistUnitTestCase { pbxTarget = PBXNativeTarget(name: "Test") pbxproj.add(object: pbxTarget) subject = ConfigGenerator() + + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) } override func tearDown() { @@ -258,6 +263,7 @@ final class ConfigGeneratorTests: TuistUnitTestCase { let releaseConfig = configurationList?.configuration(name: "Release") let expectedSettings: SettingsDictionary = [ + "SDKROOT": "iphoneos", "TARGETED_DEVICE_FAMILY": "1,2", "IPHONEOS_DEPLOYMENT_TARGET": "12.0", "SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD": "YES", @@ -266,6 +272,10 @@ final class ConfigGeneratorTests: TuistUnitTestCase { assert(config: debugConfig, contains: expectedSettings) assert(config: releaseConfig, contains: expectedSettings) + + // SUPPORTED_PLATFORMS is only set when multiple platforms are defined by the target + XCTAssertNil(debugConfig?.buildSettings["SUPPORTED_PLATFORMS"]) + XCTAssertNil(releaseConfig?.buildSettings["SUPPORTED_PLATFORMS"]) } func test_generateTargetWithDeploymentTarget_whenIOS_withoutMacAndVisionForIPhoneSupport() throws { @@ -515,6 +525,44 @@ final class ConfigGeneratorTests: TuistUnitTestCase { assert(config: releaseConfig, contains: expectedSettings) } + func test_generateTargetWithDeploymentTarget_whenVisionWithiPadDesign() throws { + // Given + let project = Project.test() + let target = Target.test( + destinations: [.iPhone, .iPad, .appleVisionWithiPadDesign], + deploymentTargets: .init(iOS: "16.0", visionOS: "1.0") + ) + let graph = Graph.test(path: project.path) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateTargetConfig( + target, + project: project, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + projectSettings: .default, + fileElements: ProjectFileElements(), + graphTraverser: graphTraverser, + sourceRootPath: try AbsolutePath(validating: "/project") + ) + + // Then + let configurationList = pbxTarget.buildConfigurationList + let debugConfig = configurationList?.configuration(name: "Debug") + let releaseConfig = configurationList?.configuration(name: "Release") + + let expectedSettings: SettingsDictionary = [ + "TARGETED_DEVICE_FAMILY": "1,2", + "XROS_DEPLOYMENT_TARGET": "1.0", + "IPHONEOS_DEPLOYMENT_TARGET": "16.0", + "SDKROOT": "iphoneos", + ] + + assert(config: debugConfig, contains: expectedSettings) + assert(config: releaseConfig, contains: expectedSettings) + } + func test_generateTargetWithMultiplePlatforms() throws { // Given let project = Project.test() @@ -711,13 +759,7 @@ final class ConfigGeneratorTests: TuistUnitTestCase { let macroExecutable = Target.test(name: "macro", platform: .macOS, product: .macro) let project = Project.test(targets: [app, macroFramework, macroExecutable]) - let graph = Graph.test(path: project.path, projects: [project.path: project], targets: [ - project.path: [ - app.name: app, - macroFramework.name: macroFramework, - macroExecutable.name: macroExecutable, - ], - ], dependencies: [ + let graph = Graph.test(path: project.path, projects: [project.path: project], dependencies: [ .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: macroExecutable.name, @@ -763,12 +805,7 @@ final class ConfigGeneratorTests: TuistUnitTestCase { let macroFramework = Target.test(name: "framework", platform: .macOS, product: .staticFramework) let project = Project.test(targets: [app, macroFramework]) - let graph = Graph.test(path: project.path, projects: [project.path: project], targets: [ - project.path: [ - app.name: app, - macroFramework.name: macroFramework, - ], - ], dependencies: [ + let graph = Graph.test(path: project.path, projects: [project.path: project], dependencies: [ .target(name: app.name, path: project.path): Set([.target(name: macroFramework.name, path: project.path)]), .target(name: macroFramework.name, path: project.path): Set([]), ]) @@ -796,6 +833,169 @@ final class ConfigGeneratorTests: TuistUnitTestCase { XCTAssertEqual(targetSettingsResult, nil) } + func test_generateTargetConfig_entitlementAreCorrectlyMappedToXCConfig_when_targetIsAppClipAndXCConfigIsProvided() throws { + let projectSettings = Settings.default + let appClip = Target.test( + name: "app", + platform: .iOS, + product: .appClip, + entitlements: .variable("$(MY_CUSTOM_VARIABLE)") + ) + + let project = Project.test(targets: [appClip]) + + let graph = Graph.test(path: project.path, projects: [project.path: project]) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateTargetConfig( + appClip, + project: project, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + projectSettings: projectSettings, + fileElements: ProjectFileElements(), + graphTraverser: graphTraverser, + sourceRootPath: try AbsolutePath(validating: "/project") + ) + + // Then + let targetSettingsResult = try pbxTarget + .buildConfigurationList? + .buildConfigurations + .first { $0.name == "Debug" }? + .buildSettings + .toSettings()["CODE_SIGN_ENTITLEMENTS"] + XCTAssertEqual(targetSettingsResult, "$(MY_CUSTOM_VARIABLE)") + } + + func test_generateTargetConfig_entitlementAreCorrectlyMappedToXCConfig_when_targetIsAppClipAndXCConfigIsProvidedByStringLiteral( + ) throws { + let projectSettings = Settings.default + let appClip = Target.test( + name: "app", + platform: .iOS, + product: .appClip, + entitlements: "$(MY_CUSTOM_VARIABLE)" + ) + + let project = Project.test(targets: [appClip]) + + let graph = Graph.test(path: project.path, projects: [project.path: project]) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateTargetConfig( + appClip, + project: project, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + projectSettings: projectSettings, + fileElements: ProjectFileElements(), + graphTraverser: graphTraverser, + sourceRootPath: try AbsolutePath(validating: "/project") + ) + + // Then + let targetSettingsResult = try pbxTarget + .buildConfigurationList? + .buildConfigurations + .first { $0.name == "Debug" }? + .buildSettings + .toSettings()["CODE_SIGN_ENTITLEMENTS"] + XCTAssertEqual(targetSettingsResult, "$(MY_CUSTOM_VARIABLE)") + } + + func test_generateTargetConfig_when_mergedBinaryTypeIsAutomatic_defaultSettingsIsEssential() throws { + // Given + let settings = Settings.test(defaultSettings: .essential) + let appTarget = Target.test(settings: settings, mergedBinaryType: .automatic) + let project = Project.test(targets: [appTarget]) + let graph = Graph.test(path: project.path, projects: [project.path: project]) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateTargetConfig( + appTarget, + project: project, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + projectSettings: .test(), + fileElements: ProjectFileElements(), + graphTraverser: graphTraverser, + sourceRootPath: try AbsolutePath(validating: "/project") + ) + + // Then + let targetSettingsResult = try pbxTarget + .buildConfigurationList? + .buildConfigurations + .first { $0.name == "Debug" }? + .buildSettings + .toSettings()["MERGED_BINARY_TYPE"] + XCTAssertEqual(targetSettingsResult, "automatic") + } + + func test_generateTargetConfig_when_mergedBinaryTypeIsManual_defaultSettingsIsEssential() throws { + // Given + let settings = Settings.test(defaultSettings: .essential) + let appTarget = Target.test(settings: settings, mergedBinaryType: .manual(mergeableDependencies: [])) + let project = Project.test(targets: [appTarget]) + let graph = Graph.test(path: project.path, projects: [project.path: project]) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateTargetConfig( + appTarget, + project: project, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + projectSettings: .test(), + fileElements: ProjectFileElements(), + graphTraverser: graphTraverser, + sourceRootPath: try AbsolutePath(validating: "/project") + ) + + // Then + let targetSettingsResult = try pbxTarget + .buildConfigurationList? + .buildConfigurations + .first { $0.name == "Debug" }? + .buildSettings + .toSettings()["MERGED_BINARY_TYPE"] + XCTAssertEqual(targetSettingsResult, "manual") + } + + func test_generateTargetConfig_when_mergeableIsTrue_defaultSettingsIsEssential() throws { + // Given + let settings = Settings.test(defaultSettings: .essential) + let frameworkTarget = Target.test(product: .framework, settings: settings, mergeable: true) + let project = Project.test(targets: [frameworkTarget]) + let graph = Graph.test(path: project.path, projects: [project.path: project]) + let graphTraverser = GraphTraverser(graph: graph) + + // When + try subject.generateTargetConfig( + frameworkTarget, + project: project, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + projectSettings: .test(), + fileElements: ProjectFileElements(), + graphTraverser: graphTraverser, + sourceRootPath: try AbsolutePath(validating: "/project") + ) + + // Then + let targetSettingsResult = try pbxTarget + .buildConfigurationList? + .buildConfigurations + .first { $0.name == "Debug" }? + .buildSettings + .toSettings()["MERGEABLE_LIBRARY"] + XCTAssertEqual(targetSettingsResult, "YES") + } + // MARK: - Helpers private func generateProjectConfig(config _: BuildConfiguration) throws { @@ -904,13 +1104,12 @@ final class ConfigGeneratorTests: TuistUnitTestCase { ) let target = Target.test(name: "Test", destinations: destinations, product: uiTest ? .uiTests : .unitTests) - let project = Project.test(path: dir, name: "Project", targets: [target]) + let project = Project.test(path: dir, name: "Project", targets: [target, appTarget]) let graph = Graph.test( name: project.name, path: project.path, projects: [project.path: project], - targets: [project.path: [appTarget.name: appTarget, target.name: target]], dependencies: [ GraphDependency .target(name: target.name, path: project.path): Set([.target(name: appTarget.name, path: project.path)]), diff --git a/Tests/TuistGeneratorTests/Generator/InfoPlistContentProviderTests.swift b/Tests/TuistGeneratorTests/Generator/InfoPlistContentProviderTests.swift index 613ef50c920..8db215c389b 100644 --- a/Tests/TuistGeneratorTests/Generator/InfoPlistContentProviderTests.swift +++ b/Tests/TuistGeneratorTests/Generator/InfoPlistContentProviderTests.swift @@ -1,8 +1,7 @@ import Foundation import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XCTest @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift index 0a9924cada6..abcbaa8dfd0 100644 --- a/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift @@ -1,13 +1,13 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistSupportTesting +import XcodeGraph import XcodeProj import XCTest -@testable import TuistCoreTesting + @testable import TuistGenerator -@testable import TuistSupportTesting final class LinkGeneratorPathTests: TuistUnitTestCase { func test_xcodeValue() { @@ -79,8 +79,10 @@ final class LinkGeneratorTests: XCTestCase { )) let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedEmbeddableFrameworksResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .embeddableFrameworks(path: .any, name: .any) + .willReturn(dependencies) // When try subject.generateEmbedPhase( @@ -128,8 +130,10 @@ final class LinkGeneratorTests: XCTestCase { let sourceRootPath = try AbsolutePath(validating: "/") let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedEmbeddableFrameworksResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .embeddableFrameworks(path: .any, name: .any) + .willReturn(dependencies) // When try subject.generateEmbedPhase( @@ -181,8 +185,10 @@ final class LinkGeneratorTests: XCTestCase { outputPaths: ["output/A.framework"] )) let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedEmbeddableFrameworksResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .embeddableFrameworks(path: .any, name: .any) + .willReturn(dependencies) // When try subject.generateEmbedPhase( @@ -225,8 +231,10 @@ final class LinkGeneratorTests: XCTestCase { outputPaths: ["output/A.framework"] )) let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedEmbeddableFrameworksResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .embeddableFrameworks(path: .any, name: .any) + .willReturn(dependencies) // When try subject.generateEmbedPhase( @@ -257,8 +265,10 @@ final class LinkGeneratorTests: XCTestCase { let fileElements = ProjectFileElements() let sourceRootPath = try AbsolutePath(validating: "/") let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedEmbeddableFrameworksResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .embeddableFrameworks(path: .any, name: .any) + .willReturn(dependencies) XCTAssertThrowsError(try subject.generateEmbedPhase( target: target, @@ -287,8 +297,10 @@ final class LinkGeneratorTests: XCTestCase { let fileAbsolutePath = try AbsolutePath(validating: "/Frameworks/Test.xcframework") let fileElements = createFileElements(fileAbsolutePath: fileAbsolutePath) let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedEmbeddableFrameworksResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .embeddableFrameworks(path: .any, name: .any) + .willReturn(dependencies) // When try subject.generateEmbedPhase( @@ -322,8 +334,10 @@ final class LinkGeneratorTests: XCTestCase { xcodeprojElements.config.buildSettings["LD_RUNPATH_SEARCH_PATHS"] = "my/custom/path" let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedRunPathSearchPathsResult = Set(paths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .runPathSearchPaths(path: .any, name: .any) + .willReturn(Set(paths)) // When try subject.setupRunPathSearchPaths( @@ -362,8 +376,10 @@ final class LinkGeneratorTests: XCTestCase { xcodeprojElements.config.buildSettings["FRAMEWORK_SEARCH_PATHS"] = "my/custom/path" let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedSearchablePathDependenciesResult = Set(dependencies) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .searchablePathDependencies(path: .any, name: .any) + .willReturn(Set(dependencies)) // When try subject.setupFrameworkSearchPath( target: target, @@ -403,8 +419,10 @@ final class LinkGeneratorTests: XCTestCase { let sourceRootPath = try AbsolutePath(validating: "/") let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesPublicHeadersFoldersResult = Set(headersFolders) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesPublicHeadersFolders(path: .any, name: .any) + .willReturn(Set(headersFolders)) try subject.setupHeadersSearchPath( target: target, @@ -429,8 +447,10 @@ final class LinkGeneratorTests: XCTestCase { xcodeprojElements.config.buildSettings["HEADER_SEARCH_PATHS"] = "my/custom/path" let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesPublicHeadersFoldersResult = Set(searchPaths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesPublicHeadersFolders(path: .any, name: .any) + .willReturn(Set(searchPaths)) // When try subject.setupHeadersSearchPath( @@ -462,8 +482,10 @@ final class LinkGeneratorTests: XCTestCase { let xcodeprojElements = createXcodeprojElements() let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesPublicHeadersFoldersResult = Set(searchPaths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesPublicHeadersFolders(path: .any, name: .any) + .willReturn(Set(searchPaths)) // When try subject.setupHeadersSearchPath( @@ -491,8 +513,10 @@ final class LinkGeneratorTests: XCTestCase { let sourceRootPath = try AbsolutePath(validating: "/") let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesPublicHeadersFoldersResult = Set(headersFolders) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesPublicHeadersFolders(path: .any, name: .any) + .willReturn(Set(headersFolders)) XCTAssertThrowsError(try subject.setupHeadersSearchPath( target: target, @@ -515,8 +539,10 @@ final class LinkGeneratorTests: XCTestCase { let xcodeprojElements = createXcodeprojElements() let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesSearchPathsResult = Set(searchPaths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesSearchPaths(path: .any, name: .any) + .willReturn(Set(searchPaths)) // When try subject.setupLibrarySearchPaths( @@ -540,8 +566,10 @@ final class LinkGeneratorTests: XCTestCase { let xcodeprojElements = createXcodeprojElements() let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesSearchPathsResult = Set(searchPaths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesSearchPaths(path: .any, name: .any) + .willReturn(Set(searchPaths)) // When try subject.setupLibrarySearchPaths( @@ -567,8 +595,10 @@ final class LinkGeneratorTests: XCTestCase { let xcodeprojElements = createXcodeprojElements() let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesSwiftIncludePathsResult = Set(searchPaths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesSwiftIncludePaths(path: .any, name: .any) + .willReturn(Set(searchPaths)) // When try subject.setupSwiftIncludePaths( @@ -592,8 +622,10 @@ final class LinkGeneratorTests: XCTestCase { let xcodeprojElements = createXcodeprojElements() let target = Target.test() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLibrariesSwiftIncludePathsResult = Set(searchPaths) + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .librariesSwiftIncludePaths(path: .any, name: .any) + .willReturn(Set(searchPaths)) // When try subject.setupSwiftIncludePaths( @@ -626,8 +658,10 @@ final class LinkGeneratorTests: XCTestCase { fileElements.products["Test"] = wakaFile fileElements.elements[try AbsolutePath(validating: "/test.framework")] = testFile let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLinkableDependenciesResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .linkableDependencies(path: .any, name: .any) + .willReturn(dependencies) try subject.generateLinkingPhase( target: target, @@ -664,8 +698,10 @@ final class LinkGeneratorTests: XCTestCase { fileElements.products["Test"] = wakaFile fileElements.elements[try AbsolutePath(validating: "/test.framework")] = testFile let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLinkableDependenciesResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .linkableDependencies(path: .any, name: .any) + .willReturn(dependencies) try subject.generateLinkingPhase( target: target, @@ -690,8 +726,10 @@ final class LinkGeneratorTests: XCTestCase { let (pbxTarget, target) = createTargets(product: .framework) let fileElements = ProjectFileElements() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLinkableDependenciesResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .linkableDependencies(path: .any, name: .any) + .willReturn(dependencies) XCTAssertThrowsError(try subject.generateLinkingPhase( target: target, @@ -718,8 +756,10 @@ final class LinkGeneratorTests: XCTestCase { let (pbxTarget, target) = createTargets(product: .framework) let fileElements = ProjectFileElements() let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLinkableDependenciesResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .linkableDependencies(path: .any, name: .any) + .willReturn(dependencies) XCTAssertThrowsError(try subject.generateLinkingPhase( target: target, @@ -754,8 +794,10 @@ final class LinkGeneratorTests: XCTestCase { fileElements.compiled["/Strong/Foo.framework"] = requiredFile fileElements.compiled["/Weak/Bar.framework"] = optionalFile let path = try AbsolutePath(validating: "/path/") - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedLinkableDependenciesResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .linkableDependencies(path: .any, name: .any) + .willReturn(dependencies) // When try subject.generateLinkingPhase( @@ -789,8 +831,10 @@ final class LinkGeneratorTests: XCTestCase { .insert(GraphDependencyReference.testProduct(target: "StaticDependency", productName: "libStaticDependency.a")) let staticDependency = Target.test(name: "StaticDependency", product: .staticLibrary) let target = Target.test() - let graphTraverser = MockGraphTraverser() - graphTraverser.stubbedCopyProductDependenciesResult = dependencies + let graphTraverser = MockGraphTraversing() + given(graphTraverser) + .copyProductDependencies(path: .any, name: .any) + .willReturn(dependencies) let fileElements = createProjectFileElements(for: [staticDependency]) let xcodeProjElements = createXcodeprojElements() @@ -830,14 +874,9 @@ final class LinkGeneratorTests: XCTestCase { path: path.appending(components: "XCFrameworks", "StaticLibraryB.xcframework"), linking: .static ) - + let project = Project.test(path: path, targets: [target]) let graph = Graph.test( - projects: [path: .test(path: path)], - targets: [ - path: [ - target.name: target, - ], - ], + projects: [path: project], dependencies: [ .target(name: target.name, path: path): [ xcframeworkA, @@ -889,14 +928,9 @@ final class LinkGeneratorTests: XCTestCase { let path = try AbsolutePath(validating: "/path/") let staticDependency = Target.test(name: "StaticDependency", product: .staticLibrary) let target = Target.test(name: "Dynamic", product: .framework) + let project = Project.test(path: path, targets: [target, staticDependency]) let graph = Graph.test( - projects: [path: .test(path: path)], - targets: [ - path: [ - target.name: target, - staticDependency.name: staticDependency, - ], - ], + projects: [path: project], dependencies: [ .target(name: target.name, path: path): [ .target(name: staticDependency.name, path: path), @@ -931,14 +965,9 @@ final class LinkGeneratorTests: XCTestCase { let path = try AbsolutePath(validating: "/path/") let resourceBundle = Target.test(name: "ResourceBundle", product: .bundle) let target = Target.test(name: "Target", product: .app) + let project = Project.test(path: path, targets: [target, resourceBundle]) let graph = Graph.test( - projects: [path: .test(path: path)], - targets: [ - path: [ - target.name: target, - resourceBundle.name: resourceBundle, - ], - ], + projects: [path: project], dependencies: [ .target(name: target.name, path: path): [ .target(name: resourceBundle.name, path: path), @@ -973,65 +1002,6 @@ final class LinkGeneratorTests: XCTestCase { ]) } - func test_generateLinks_generatesAShellScriptBuildPhase_when_targetIsAMacroFramework() throws { - // Given - let app = Target.test(name: "app", platform: .iOS, product: .app) - 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]) - - let graph = Graph.test(path: project.path, projects: [project.path: project], targets: [ - project.path: [ - app.name: app, - macroFramework.name: macroFramework, - macroExecutable.name: macroExecutable, - ], - ], dependencies: [ - .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: macroExecutable.name, - path: project.path - )]), - .target(name: macroExecutable.name, path: project.path): Set([]), - ]) - let graphTraverser = GraphTraverser(graph: graph) - let xcodeProjElements = createXcodeprojElements() - let fileElements = createProjectFileElements(for: [app, macroFramework, macroExecutable]) - - // When - try subject.generateLinks( - target: macroFramework, - pbxTarget: xcodeProjElements.pbxTarget, - pbxproj: xcodeProjElements.pbxproj, - fileElements: fileElements, - path: project.path, - sourceRootPath: project.path, - graphTraverser: graphTraverser - ) - - // Then - let buildPhase = xcodeProjElements - .pbxTarget - .buildPhases - .compactMap { $0 as? PBXShellScriptBuildPhase } - .first(where: { $0.name() == "Copy Swift Macro executable into $BUILT_PRODUCT_DIR" }) - - XCTAssertNotNil(buildPhase) - - let expectedScript = - "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("$BUILD_DIR/$CONFIGURATION/\(macroExecutable.productName)") == true) - XCTAssertTrue( - buildPhase?.outputPaths - .contains("$BUILD_DIR/Debug-iphonesimulator/\(macroExecutable.productName)") == true - ) - XCTAssertTrue( - buildPhase?.outputPaths - .contains("$BUILD_DIR/Debug-iphoneos/\(macroExecutable.productName)") == true - ) - } - // MARK: - Helpers struct XcodeprojElements { diff --git a/Tests/TuistGeneratorTests/Generator/Mocks/MockInfoPlistContentProvider.swift b/Tests/TuistGeneratorTests/Generator/Mocks/MockInfoPlistContentProvider.swift index aafeedd7d9f..318dd434b7b 100644 --- a/Tests/TuistGeneratorTests/Generator/Mocks/MockInfoPlistContentProvider.swift +++ b/Tests/TuistGeneratorTests/Generator/Mocks/MockInfoPlistContentProvider.swift @@ -1,8 +1,7 @@ import Foundation import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph @testable import TuistGenerator final class MockInfoPlistContentProvider: InfoPlistContentProviding { diff --git a/Tests/TuistGeneratorTests/Generator/Mocks/MockProjectDescriptorGenerator.swift b/Tests/TuistGeneratorTests/Generator/Mocks/MockProjectDescriptorGenerator.swift index 5514ad7b194..929899b4ead 100644 --- a/Tests/TuistGeneratorTests/Generator/Mocks/MockProjectDescriptorGenerator.swift +++ b/Tests/TuistGeneratorTests/Generator/Mocks/MockProjectDescriptorGenerator.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Generator/Mocks/MockSchemeDescriptorsGenerator.swift b/Tests/TuistGeneratorTests/Generator/Mocks/MockSchemeDescriptorsGenerator.swift index d16490160e4..1f8a399896c 100644 --- a/Tests/TuistGeneratorTests/Generator/Mocks/MockSchemeDescriptorsGenerator.swift +++ b/Tests/TuistGeneratorTests/Generator/Mocks/MockSchemeDescriptorsGenerator.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph @testable import TuistGenerator final class MockSchemeDescriptorsGenerator: SchemeDescriptorsGenerating { diff --git a/Tests/TuistGeneratorTests/Generator/Mocks/MockTargetGenerator.swift b/Tests/TuistGeneratorTests/Generator/Mocks/MockTargetGenerator.swift index f1290518e20..3cd3dbfc5fe 100644 --- a/Tests/TuistGeneratorTests/Generator/Mocks/MockTargetGenerator.swift +++ b/Tests/TuistGeneratorTests/Generator/Mocks/MockTargetGenerator.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Generator/Mocks/MockWorkspaceDescriptorGenerator.swift b/Tests/TuistGeneratorTests/Generator/Mocks/MockWorkspaceDescriptorGenerator.swift index f647eaaa1c2..789b1bb606f 100644 --- a/Tests/TuistGeneratorTests/Generator/Mocks/MockWorkspaceDescriptorGenerator.swift +++ b/Tests/TuistGeneratorTests/Generator/Mocks/MockWorkspaceDescriptorGenerator.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Generator/ProjectDescriptorGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/ProjectDescriptorGeneratorTests.swift index 82ae596bc0a..d6809525b97 100644 --- a/Tests/TuistGeneratorTests/Generator/ProjectDescriptorGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ProjectDescriptorGeneratorTests.swift @@ -1,11 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path import struct TSCUtility.Version import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -16,7 +16,10 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { override func setUp() { super.setUp() - system.swiftVersionStub = { "5.2" } + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + subject = ProjectDescriptorGenerator() } @@ -54,12 +57,6 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - testGraphTarget.target.name: testGraphTarget.target, - appGraphTarget.target.name: appGraphTarget.target, - ], - ], dependencies: [ .target(name: testGraphTarget.target.name, path: testGraphTarget.path): [ .target(name: appGraphTarget.target.name, path: appGraphTarget.path), @@ -68,6 +65,10 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { ) let graphTraverser = GraphTraverser(graph: graph) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let generatedProject = try subject.generate(project: project, graphTraverser: graphTraverser) @@ -89,18 +90,20 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { } func test_objectVersion_when_xcode11_and_spm() throws { - xcodeController.selectedVersionStub = .success(Version(11, 0, 0)) + given(xcodeController) + .selectedVersion() + .willReturn(Version(11, 0, 0)) // Given let temporaryPath = try temporaryPath() + let target = Target.test(name: "A") let project = Project.test( path: temporaryPath, name: "Project", - targets: [.test(dependencies: [.package(product: "A", type: .runtime)])], + targets: [target, .test(name: "B", dependencies: [.package(product: "A", type: .runtime)])], packages: [.remote(url: "A", requirement: .exact("0.1"))] ) - let target = Target.test() let graphTarget = GraphTarget.test(path: project.path, target: target, project: project) let graph = Graph.test( projects: [project.path: project], @@ -109,11 +112,6 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { "A": .remote(url: "A", requirement: .exact("0.1")), ], ], - targets: [ - graphTarget.path: [ - graphTarget.target.name: graphTarget.target, - ], - ], dependencies: [ .target(name: graphTarget.target.name, path: graphTarget.path): [ .packageProduct(path: project.path, product: "A", type: .runtime), @@ -132,7 +130,9 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { } func test_objectVersion_when_xcode11() throws { - xcodeController.selectedVersionStub = .success(Version(11, 0, 0)) + given(xcodeController) + .selectedVersion() + .willReturn(Version(11, 0, 0)) // Given let temporaryPath = try temporaryPath() @@ -156,7 +156,9 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { } func test_objectVersion_when_xcode10() throws { - xcodeController.selectedVersionStub = .success(Version(10, 2, 1)) + given(xcodeController) + .selectedVersion() + .willReturn(Version(10, 2, 1)) // Given let temporaryPath = try temporaryPath() @@ -195,12 +197,18 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { let project = Project.test( path: path, targets: [ - .test(resources: try resources.map { - .file(path: path.appending(try RelativePath(validating: $0))) - }), + .test(resources: .init( + try resources.map { + .file(path: path.appending(try RelativePath(validating: $0))) + } + )), ] ) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.generate(project: project, graphTraverser: graphTraverser) @@ -272,6 +280,29 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { ]) } + func test_generate_setsClassPrefix() throws { + // Given + let path = try temporaryPath() + let graph = Graph.test(path: path) + let graphTraverser = GraphTraverser(graph: graph) + let project = Project.test( + path: path, + classPrefix: "TUIST", + targets: [] + ) + + // When + let got = try subject.generate(project: project, graphTraverser: graphTraverser) + + // Then + let pbxProject = try XCTUnwrap(try got.xcodeProj.pbxproj.rootProject()) + let attributes = try XCTUnwrap(pbxProject.attributes as? [String: String]) + XCTAssertEqual(attributes, [ + "BuildIndependentTargetsInParallel": "YES", + "CLASSPREFIX": "TUIST", + ]) + } + func test_generate_setsResourcesTagsName() throws { // Given let path = try temporaryPath() @@ -283,9 +314,13 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { ] let project = Project.test( path: path, - targets: [.test(resources: resources)] + targets: [.test(resources: .init(resources))] ) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.generate(project: project, graphTraverser: graphTraverser) @@ -327,6 +362,10 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { targets: [] ) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.generate(project: project, graphTraverser: graphTraverser) @@ -335,19 +374,45 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { XCTAssertEqual(pbxProject.developmentRegion, "de") } + func test_generate_localSwiftPackageGroup() throws { + // Given + let project = Project.test( + packages: [ + .local(path: try AbsolutePath(validating: "/LocalPackages/LocalPackageA")), + .local(path: try AbsolutePath(validating: "/LocalPackages/LocalPackageB")), + ] + ) + + let graph = Graph.test(projects: [project.path: project]) + let graphTraverser = GraphTraverser(graph: graph) + + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + + // When + let got = try subject.generate(project: project, graphTraverser: graphTraverser) + + // Then + let pbxproj = got.xcodeProj.pbxproj + let rootGroup = try XCTUnwrap(pbxproj.rootGroup()) + let packagesGroup = rootGroup.group(named: "Packages") + let packages = try XCTUnwrap(packagesGroup?.children) + XCTAssertEqual(packages.map(\.name), ["LocalPackageA", "LocalPackageB"]) + } + func test_generate_localSwiftPackagePaths() throws { // Given let projectPath = try AbsolutePath(validating: "/Project") - let localPackagePath = try AbsolutePath(validating: "/Packages/LocalPackageA") + let localPackagePath = try AbsolutePath(validating: "/LocalPackages/LocalPackageA") + let target = Target.test(name: "A") let project = Project.test( path: projectPath, sourceRootPath: projectPath, name: "Project", - targets: [.test(dependencies: [.package(product: "A", type: .runtime)])], + targets: [target, .test(name: "B", dependencies: [.package(product: "A", type: .runtime)])], packages: [.local(path: localPackagePath)] ) - - let target = Target.test() let graphTarget = GraphTarget(path: project.path, target: target, project: project) let graph = Graph.test( projects: [project.path: project], @@ -356,11 +421,6 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { "A": .local(path: localPackagePath), ], ], - targets: [ - graphTarget.path: [ - graphTarget.target.name: graphTarget.target, - ], - ], dependencies: [ .target(name: graphTarget.target.name, path: graphTarget.path): [ .packageProduct(path: project.path, product: "A", type: .runtime), @@ -369,15 +429,20 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { ) let graphTraverser = GraphTraverser(graph: graph) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.generate(project: project, graphTraverser: graphTraverser) // Then let pbxproj = got.xcodeProj.pbxproj let rootGroup = try XCTUnwrap(pbxproj.rootGroup()) - let paths = rootGroup.children.compactMap(\.path) + let packageGroup = try XCTUnwrap(rootGroup.group(named: "Packages")) + let paths = packageGroup.children.compactMap(\.path) XCTAssertEqual(paths, [ - "../Packages/LocalPackageA", + "../LocalPackages/LocalPackageA", ]) } @@ -392,6 +457,10 @@ final class ProjectDescriptorGeneratorTests: TuistUnitTestCase { lastUpgradeCheck: .init(12, 5, 1) ) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.generate(project: project, graphTraverser: graphTraverser) diff --git a/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift b/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift index be29a7c573a..de128a6e404 100644 --- a/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift @@ -1,31 +1,38 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @testable import TuistSupportTesting final class ProjectFileElementsTests: TuistUnitTestCase { - var subject: ProjectFileElements! - var groups: ProjectGroups! - var pbxproj: PBXProj! + private var subject: ProjectFileElements! + private var groups: ProjectGroups! + private var pbxproj: PBXProj! + private var cacheDirectoriesProvider: MockCacheDirectoriesProviding! - override func setUp() { + override func setUpWithError() throws { super.setUp() + cacheDirectoriesProvider = .init() pbxproj = PBXProj() groups = ProjectGroups.generate( project: .test(path: "/path", sourceRootPath: "/path", xcodeProjPath: "/path/Project.xcodeproj"), pbxproj: pbxproj ) - subject = ProjectFileElements() + given(cacheDirectoriesProvider) + .cacheDirectory() + .willReturn(try! temporaryPath()) + + subject = ProjectFileElements(cacheDirectoriesProvider: cacheDirectoriesProvider) } override func tearDown() { + cacheDirectoriesProvider = nil pbxproj = nil groups = nil subject = nil @@ -390,10 +397,12 @@ final class ProjectFileElementsTests: TuistUnitTestCase { entitlements: .file(path: try AbsolutePath(validating: "/project/app.entitlements")), settings: settings, sources: [SourceFile(path: try AbsolutePath(validating: "/project/file.swift"))], - resources: [ - .file(path: try AbsolutePath(validating: "/project/image.png")), - .folderReference(path: try AbsolutePath(validating: "/project/reference")), - ], + resources: .init( + [ + .file(path: try AbsolutePath(validating: "/project/image.png")), + .folderReference(path: try AbsolutePath(validating: "/project/reference")), + ] + ), copyFiles: [ CopyFilesAction( name: "Copy Templates", @@ -814,14 +823,12 @@ final class ProjectFileElementsTests: TuistUnitTestCase { ) let groups = ProjectGroups.generate(project: project, pbxproj: pbxproj) - let frameworkPath = try temporaryPath().appending(component: CacheCategory.builds.directoryName) - .appending(component: "Test.framework") + let frameworkPath = try cacheDirectoriesProvider.cacheDirectory().appending(component: "Test.framework") let binaryPath = frameworkPath.appending(component: "Test") let frameworkDependency = GraphDependencyReference.framework( path: frameworkPath, binaryPath: binaryPath, - isCarthage: false, dsymPath: nil, bcsymbolmapPaths: [], linking: .static, @@ -841,7 +848,7 @@ final class ProjectFileElementsTests: TuistUnitTestCase { ) // Then - XCTAssertEqual(groups.frameworks.flattenedChildren, [ + XCTAssertEqual(groups.cachedFrameworks.flattenedChildren, [ "Test.framework", ]) @@ -852,6 +859,72 @@ final class ProjectFileElementsTests: TuistUnitTestCase { XCTAssertEqual(frameworkElement?.name, frameworkPath.basename) } + func test_generateDependencies_when_cacheCompiledArtifacts_and_sdk() throws { + // Given + let pbxproj = PBXProj() + let sourceRootPath = try AbsolutePath(validating: "/a/project/") + let project = Project.test( + path: sourceRootPath, + sourceRootPath: sourceRootPath, + xcodeProjPath: sourceRootPath.appending(component: "Project.xcodeproj") + ) + let groups = ProjectGroups.generate(project: project, pbxproj: pbxproj) + + let frameworkPath = cacheDirectoriesProvider.cacheDirectory().appending(component: "Test.framework") + let binaryPath = frameworkPath.appending(component: "Test") + + let frameworkDependency = GraphDependencyReference.framework( + path: frameworkPath, + binaryPath: binaryPath, + dsymPath: nil, + bcsymbolmapPaths: [], + linking: .static, + architectures: [.arm64], + product: .framework, + status: .required + ) + + let sdkPath = try temporaryPath().appending(component: "ARKit.framework") + let sdkStatus: SDKStatus = .required + let sdkSource: SDKSource = .developer + let sdkDependency = GraphDependencyReference.sdk( + path: sdkPath, + status: sdkStatus, + source: sdkSource + ) + + // When + try subject.generate( + dependencyReferences: [frameworkDependency, sdkDependency], + groups: groups, + pbxproj: pbxproj, + sourceRootPath: sourceRootPath, + filesGroup: .group(name: "Project") + ) + + // Then + XCTAssertEqual(groups.cachedFrameworks.flattenedChildren, [ + "Test.framework", + ]) + + let frameworkElement = subject.compiled[frameworkPath] + XCTAssertNotNil(frameworkElement) + XCTAssertEqual(frameworkElement?.sourceTree, .absolute) + XCTAssertEqual(frameworkElement?.path, frameworkPath.pathString) + XCTAssertEqual(frameworkElement?.name, frameworkPath.basename) + + // Then + XCTAssertEqual(groups.frameworks.flattenedChildren, [ + "ARKit.framework", + ]) + + let sdkElement = subject.compiled[sdkPath] + XCTAssertNotNil(sdkElement) + XCTAssertEqual(sdkElement?.sourceTree, .developerDir) + XCTAssertEqual(sdkElement?.path, sdkPath.relative(to: "/").pathString) + XCTAssertEqual(sdkElement?.name, sdkPath.basename) + } + func test_generateDependencies_remoteSwiftPackage_doNotGenerateElements() throws { // Given let pbxproj = PBXProj() @@ -878,11 +951,6 @@ final class ProjectFileElementsTests: TuistUnitTestCase { "A": .remote(url: "url", requirement: .branch("master")), ], ], - targets: [ - graphTarget.path: [ - graphTarget.target.name: graphTarget.target, - ], - ], dependencies: [ .target(name: graphTarget.target.name, path: graphTarget.path): [ .packageProduct(path: project.path, product: "A", type: .runtime), @@ -903,6 +971,66 @@ final class ProjectFileElementsTests: TuistUnitTestCase { let projectGroup = groups.sortedMain.group(named: "Project") XCTAssertEqual(projectGroup?.flattenedChildren, []) } + + func test_gpxFilesForRunAction() { + // Given + let schemes: [Scheme] = [ + .test(runAction: nil), + .test(runAction: .test( + options: RunActionOptions(simulatedLocation: .gpxFile("/gpx/A")) + )), + .test(runAction: .test( + options: RunActionOptions(simulatedLocation: .gpxFile("/gpx/B")) + )), + .test(runAction: .test( + options: RunActionOptions(simulatedLocation: .reference("London, England")) + )), + .test(runAction: .test( + options: RunActionOptions(simulatedLocation: .gpxFile("/gpx/C")) + )), + ] + let filesGroup: ProjectGroup = .group(name: "Project") + + // When + let gpxFiles = subject.gpxFilesForRunAction(in: schemes, filesGroup: filesGroup) + + // Then + XCTAssertEqual(gpxFiles, [ + GroupFileElement(path: "/gpx/A", group: filesGroup), + GroupFileElement(path: "/gpx/B", group: filesGroup), + GroupFileElement(path: "/gpx/C", group: filesGroup), + ]) + } + + func test_gpxFilesForTestAction() { + // Given + let schemes: [Scheme] = [ + .test(testAction: nil), + .test(testAction: .test(targets: [ + .test(simulatedLocation: .gpxFile("/gpx/A")), + ])), + .test(testAction: .test(targets: [ + .test(simulatedLocation: .gpxFile("/gpx/B")), + .test(simulatedLocation: .reference("London, England")), + ])), + .test(testAction: .test(targets: [ + .test(simulatedLocation: .gpxFile("/gpx/C")), + .test(simulatedLocation: .gpxFile("/gpx/D")), + ])), + ] + let filesGroup: ProjectGroup = .group(name: "Project") + + // When + let gpxFiles = subject.gpxFilesForTestAction(in: schemes, filesGroup: filesGroup) + + // Then + XCTAssertEqual(gpxFiles, [ + GroupFileElement(path: "/gpx/A", group: filesGroup), + GroupFileElement(path: "/gpx/B", group: filesGroup), + GroupFileElement(path: "/gpx/C", group: filesGroup), + GroupFileElement(path: "/gpx/D", group: filesGroup), + ]) + } } extension PBXGroup { diff --git a/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift b/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift index 114e4263aca..39717098017 100644 --- a/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift @@ -1,10 +1,9 @@ import Foundation +import Path import PathKit -import TSCBasic import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -27,14 +26,15 @@ final class ProjectGroupsTests: XCTestCase { xcodeProjPath: path.appending(component: "Project.xcodeproj"), name: "Project", organizationName: nil, + classPrefix: nil, defaultKnownRegions: nil, developmentRegion: nil, options: .test(), settings: .default, filesGroup: .group(name: "Project"), targets: [ - .test(filesGroup: .group(name: "Target")), - .test(), + .test(name: "A", filesGroup: .group(name: "Target")), + .test(name: "B"), ], packages: [], schemes: [], @@ -64,7 +64,7 @@ final class ProjectGroupsTests: XCTestCase { let main = subject.sortedMain XCTAssertNil(main.path) XCTAssertEqual(main.sourceTree, .group) - XCTAssertEqual(main.children.count, 4) + XCTAssertEqual(main.children.count, 5) XCTAssertNotNil(main.group(named: "Project")) XCTAssertNil(main.group(named: "Project")?.path) @@ -79,6 +79,11 @@ final class ProjectGroupsTests: XCTestCase { XCTAssertNil(subject.frameworks.path) XCTAssertEqual(subject.frameworks.sourceTree, .group) + XCTAssertTrue(main.children.contains(subject.cachedFrameworks)) + XCTAssertEqual(subject.cachedFrameworks.name, "Cache") + XCTAssertNil(subject.cachedFrameworks.path) + XCTAssertEqual(subject.cachedFrameworks.sourceTree, .group) + XCTAssertTrue(main.children.contains(subject.products)) XCTAssertEqual(subject.products.name, "Products") XCTAssertNil(subject.products.path) @@ -87,10 +92,10 @@ final class ProjectGroupsTests: XCTestCase { func test_generate_groupsOrder() throws { // Given - let target1 = Target.test(filesGroup: .group(name: "B")) - let target2 = Target.test(filesGroup: .group(name: "C")) - let target3 = Target.test(filesGroup: .group(name: "A")) - let target4 = Target.test(filesGroup: .group(name: "C")) + let target1 = Target.test(name: "Target1", filesGroup: .group(name: "B")) + let target2 = Target.test(name: "Target2", filesGroup: .group(name: "C")) + let target3 = Target.test(name: "Target3", filesGroup: .group(name: "A")) + let target4 = Target.test(name: "Target4", filesGroup: .group(name: "C")) let project = Project.test( filesGroup: .group(name: "P"), targets: [target1, target2, target3, target4] @@ -103,19 +108,61 @@ final class ProjectGroupsTests: XCTestCase { ) // Then - // swiftformat:disable preferKeyPath - let paths = subject.sortedMain.children.compactMap { $0.nameOrPath } - // swiftformat:enable preferKeyPath + let paths = subject.sortedMain.children.map(\.nameOrPath).sorted() XCTAssertEqual(paths, [ - "P", + "A", "B", "C", - "A", + "Cache", "Frameworks", + "P", + "Products", + ]) + } + + func test_removeEmptyAuxiliaryGroups_removesEmptyGroups() throws { + // Given + let project = Project.test() + subject = ProjectGroups.generate( + project: project, + pbxproj: pbxproj + ) + + // When + subject.removeEmptyAuxiliaryGroups() + + // Then + let paths = subject.sortedMain.children.map(\.nameOrPath) + XCTAssertEqual(paths, [ + "Project", "Products", ]) } + func test_removeEmptyAuxiliaryGroups_preservesNonEmptyGroups() throws { + // Given + let project = Project.test() + subject = ProjectGroups.generate( + project: project, + pbxproj: pbxproj + ) + + addFile(to: subject.frameworks) + addFile(to: subject.cachedFrameworks) + + // When + subject.removeEmptyAuxiliaryGroups() + + // Then + let paths = subject.sortedMain.children.map(\.nameOrPath) + XCTAssertEqual(paths, [ + "Project", + "Products", + "Frameworks", + "Cache", + ]) + } + func test_targetFrameworks() throws { subject = ProjectGroups.generate( project: project, @@ -144,9 +191,9 @@ final class ProjectGroupsTests: XCTestCase { func test_projectGroup_knownProjectGroups() throws { // Given - let target1 = Target.test(filesGroup: .group(name: "A")) - let target2 = Target.test(filesGroup: .group(name: "B")) - let target3 = Target.test(filesGroup: .group(name: "B")) + let target1 = Target.test(name: "1", filesGroup: .group(name: "A")) + let target2 = Target.test(name: "2", filesGroup: .group(name: "B")) + let target3 = Target.test(name: "3", filesGroup: .group(name: "B")) let project = Project.test( path: .root, sourceRootPath: .root, @@ -209,4 +256,12 @@ final class ProjectGroupsTests: XCTestCase { XCTAssertNil(main.tabWidth) XCTAssertNil(main.wrapsLines) } + + // MARK: - Helpers + + private func addFile(to group: PBXGroup) { + let file = PBXFileReference() + pbxproj.add(object: file) + group.children.append(file) + } } diff --git a/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift index 79b36a8fde6..3d046376a65 100644 --- a/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @@ -37,15 +36,11 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test( path: projectPath, - xcodeProjPath: xcodeProjPath + xcodeProjPath: xcodeProjPath, + targets: targets ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -86,15 +81,11 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test( path: projectPath, - xcodeProjPath: xcodeProjPath + xcodeProjPath: xcodeProjPath, + targets: targets ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -143,24 +134,18 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let projectA = Project.test( path: projectAPath, - xcodeProjPath: xcodeProjAPath + xcodeProjPath: xcodeProjAPath, + targets: [frameworkA] ) let projectB = Project.test( path: projectBPath, - xcodeProjPath: xcodeProjBPath + xcodeProjPath: xcodeProjBPath, + targets: [frameworkB] ) let graph = Graph.test( projects: [ projectA.path: projectA, projectB.path: projectB, - ], - targets: [ - projectA.path: [ - frameworkA.name: frameworkA, - ], - projectB.path: [ - frameworkB.name: frameworkB, - ], ] ) let graphTraverser = GraphTraverser(graph: graph) @@ -233,12 +218,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -293,7 +273,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { ) ) let project = Project.test(schemes: [schemeA, schemeB]) - let generatedProject = generatedProject(targets: project.targets) + let generatedProject = generatedProject(targets: Array(project.targets.values)) let graph = Graph.test(projects: [project.path: project]) let graphTraverser = GraphTraverser(graph: graph) @@ -339,12 +319,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -376,7 +351,12 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test(targets: [target, testTarget]) let testAction = TestAction.test( - targets: [TestableTarget(target: TargetReference(projectPath: project.path, name: "AppTests"))], + targets: [ + TestableTarget( + target: TargetReference(projectPath: project.path, name: "AppTests"), + simulatedLocation: .reference("Rio de Janeiro, Brazil") + ), + ], arguments: nil ) @@ -385,12 +365,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { 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), @@ -418,6 +392,8 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let buildableReference = testable.buildableReference XCTAssertEqual(testable.skipped, false) + XCTAssertEqual(testable.locationScenarioReference?.referenceType, "1") + XCTAssertEqual(testable.locationScenarioReference?.identifier, "Rio de Janeiro, Brazil") XCTAssertEqual(buildableReference.referencedContainer, "container:Project.xcodeproj") XCTAssertEqual(buildableReference.buildableName, "AppTests.xctest") XCTAssertEqual(buildableReference.blueprintName, "AppTests") @@ -441,12 +417,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { 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), @@ -501,12 +471,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - framework.name: framework, - ], - ], dependencies: [ .target(name: app.name, path: projectPath): [ .target(name: framework.name, path: projectPath), @@ -553,12 +517,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - framework.name: framework, - ], - ], dependencies: [ .target(name: app.name, path: projectPath): [ .target(name: framework.name, path: projectPath), @@ -608,12 +566,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test(path: projectPath, targets: [target, testTarget]) 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), @@ -742,12 +694,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let scheme = Scheme.test(name: "AppTests", shared: true, buildAction: buildAction, testAction: testAction) 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), @@ -791,12 +737,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { 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), @@ -837,12 +777,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test(path: projectPath, targets: [target, testTarget]) 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), @@ -879,12 +813,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { 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), @@ -953,12 +881,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let generatedProjects = createGeneratedProjects(projects: [project]) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - testTarget.name: testTarget, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1019,12 +942,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { 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), @@ -1109,12 +1026,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { path: workspacePath, xcWorkspacePath: workspacePath.appending(component: "Workspace.xcworkspace") ), - projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1186,12 +1098,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test(path: projectPath, targets: [app]) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1226,18 +1133,13 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let launchAction = RunAction.test( configurationName: "Debug", filePath: "/usr/bin/foo", - diagnosticsOptions: [.mainThreadChecker] + diagnosticsOptions: SchemeDiagnosticsOptions(mainThreadCheckerEnabled: true) ) let scheme = Scheme.test(name: "Library", buildAction: buildAction, runAction: launchAction) let project = Project.test(path: projectPath, targets: [target]) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1268,7 +1170,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let buildAction = BuildAction.test(targets: [TargetReference(projectPath: projectPath, name: "Library")]) let testAction = TestAction.test( targets: [TestableTarget(target: TargetReference(projectPath: projectPath, name: "Library"))], - diagnosticsOptions: [.mainThreadChecker] + diagnosticsOptions: SchemeDiagnosticsOptions(mainThreadCheckerEnabled: true) ) let scheme = Scheme.test(name: "Library", buildAction: buildAction, testAction: testAction, runAction: nil) @@ -1278,12 +1180,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1343,12 +1240,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1389,12 +1281,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [app] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1424,12 +1311,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [app] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1468,12 +1350,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - appExtension.name: appExtension, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: appExtension.name, path: project.path), @@ -1522,12 +1398,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - appExtension.name: appExtension, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: appExtension.name, path: project.path), @@ -1563,12 +1433,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1628,12 +1493,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1680,12 +1540,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1745,12 +1600,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1813,12 +1663,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { targets: [target] ) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1851,12 +1696,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test(path: projectPath, targets: [target]) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1893,12 +1733,42 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let project = Project.test(path: projectPath, targets: [target]) let graph = Graph.test( - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] + ) + let graphTraverser = GraphTraverser(graph: graph) + + // When + let got = try subject.schemeArchiveAction( + scheme: scheme, + graphTraverser: graphTraverser, + rootPath: project.path, + generatedProjects: createGeneratedProjects(projects: [project]) + ) + + // Then + let result = try XCTUnwrap(got) + XCTAssertEqual(result.buildConfiguration, "Beta Release") + XCTAssertEqual(result.customArchiveName, "App [Beta]") + XCTAssertEqual(result.revealArchiveInOrganizer, true) + } + + func test_schemeArchiveAction_whenNoBuildActionSpecified() throws { + // Given + let projectPath = try AbsolutePath(validating: "/Project") + let target = Target.test(name: "App", platform: .iOS, product: .app) + let archiveAction = ArchiveAction.test( + configurationName: "Beta Release", + revealArchiveInOrganizer: true, + customArchiveName: "App [Beta]" + ) + let scheme = Scheme.test( + buildAction: nil, + archiveAction: archiveAction + ) + + let project = Project.test(path: projectPath, targets: [target]) + let graph = Graph.test( + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) @@ -1928,14 +1798,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - framework.name: framework, - unitTests.name: unitTests, - uiTests.name: uiTests, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: framework.name, path: project.path), @@ -1953,7 +1815,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { // When let result = try subject.generateProjectSchemes( project: project, - generatedProject: generatedProject(targets: project.targets), + generatedProject: generatedProject(targets: Array(project.targets.values)), graphTraverser: graphTraverser ) @@ -1984,12 +1846,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - app.name: app, - appExtension.name: appExtension, - ], - ], dependencies: [ .target(name: app.name, path: project.path): [ .target(name: appExtension.name, path: project.path), @@ -2001,7 +1857,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { // When let result = try subject.generateProjectSchemes( project: project, - generatedProject: generatedProject(targets: project.targets), + generatedProject: generatedProject(targets: Array(project.targets.values)), graphTraverser: graphTraverser ) @@ -2031,11 +1887,10 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { path: project.path, workspace: workspace, projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: [:] ) let graphTraverser = GraphTraverser(graph: graph) - let generatedProject = generatedProject(targets: project.targets) + let generatedProject = generatedProject(targets: Array(project.targets.values)) // When let result = try subject.generateWorkspaceSchemes( @@ -2062,7 +1917,6 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: [:] ) let graphTraverser = GraphTraverser(graph: graph) @@ -2070,7 +1924,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { // When let result = try subject.generateProjectSchemes( project: project, - generatedProject: generatedProject(targets: project.targets), + generatedProject: generatedProject(targets: Array(project.targets.values)), graphTraverser: graphTraverser ) @@ -2087,7 +1941,7 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { ( $0.xcodeProjPath, generatedProject( - targets: $0.targets, + targets: Array($0.targets.values), projectPath: $0.xcodeProjPath.pathString ) ) diff --git a/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift index 5caf0938c7e..25c99206ef8 100644 --- a/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -116,18 +115,12 @@ final class TargetGeneratorTests: XCTestCase { destinations: [.mac, .iPhone] ) let targetC = Target.test(name: "TargetC") + let project: Project = .test(path: path, targets: [targetA, targetB, 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, - ], - ], + projects: [path: project], dependencies: [ .target(name: targetA.name, path: path): [ .target(name: targetB.name, path: path), @@ -175,7 +168,7 @@ final class TargetGeneratorTests: XCTestCase { let graphTraverser = GraphTraverser(graph: graph) let target = Target.test( sources: [], - resources: [], + resources: .init([]), scripts: [ TargetScript( name: "post", diff --git a/Tests/TuistGeneratorTests/Generator/TestData/GeneratedProject+TestData.swift b/Tests/TuistGeneratorTests/Generator/TestData/GeneratedProject+TestData.swift index 3daa4b3901d..7fa8929c0bd 100644 --- a/Tests/TuistGeneratorTests/Generator/TestData/GeneratedProject+TestData.swift +++ b/Tests/TuistGeneratorTests/Generator/TestData/GeneratedProject+TestData.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XcodeProj @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Generator/WorkspaceDescriptorGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/WorkspaceDescriptorGeneratorTests.swift index c4048ea4e89..4d92fe61c01 100644 --- a/Tests/TuistGeneratorTests/Generator/WorkspaceDescriptorGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/WorkspaceDescriptorGeneratorTests.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path +import struct TSCUtility.Version import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -15,7 +16,15 @@ final class WorkspaceDescriptorGeneratorTests: TuistUnitTestCase { override func setUp() { super.setUp() - system.swiftVersionStub = { "5.2" } + + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + subject = WorkspaceDescriptorGenerator(config: .init(projectGenerationContext: .serial)) } @@ -104,12 +113,7 @@ final class WorkspaceDescriptorGeneratorTests: TuistUnitTestCase { let graph = Graph.test( workspace: workspace, - projects: [project.path: project], - targets: [ - project.path: [ - target.name: target, - ], - ] + projects: [project.path: project] ) let graphTraverser = GraphTraverser(graph: graph) diff --git a/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorGeneratorTests.swift index 015aa183579..370dfd95e3e 100644 --- a/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorGeneratorTests.swift @@ -1,10 +1,10 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator @@ -15,7 +15,11 @@ final class WorkspaceSettingsDescriptorGeneratorTests: TuistUnitTestCase { override func setUp() { super.setUp() - system.swiftVersionStub = { "5.2" } + + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + subject = WorkspaceSettingsDescriptorGenerator() } diff --git a/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorTests.swift b/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorTests.swift index c6dd3f4f55f..12a0c23895e 100644 --- a/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/WorkspaceSettingsDescriptorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift index 821f30a9221..27221b29f7f 100644 --- a/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift @@ -1,9 +1,8 @@ -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting @@ -288,8 +287,8 @@ final class WorkspaceStructureGeneratorTests: XCTestCase { func test_generateStructure_addsDependenciesToADependenciesGroup() throws { // Given let xcodeProjPaths = try createFolders([ - "/path/to/workspace/Tuist/Dependencies/SwiftPackageManager/.build/checkouts/AEXML/AEXML.xcodeproj", - "/path/to/workspace/Tuist/Dependencies/SwiftPackageManager/.build/checkouts/SwiftSyntax/SwiftSyntax.xcodeproj", + "/path/to/workspace/Tuist/.build/tuist-derived/AEXML/AEXML.xcodeproj", + "/path/to/workspace/Tuist/.build/tuist-derived/SwiftSyntax/SwiftSyntax.xcodeproj", ]) let workspace = Workspace.test() @@ -305,9 +304,9 @@ final class WorkspaceStructureGeneratorTests: XCTestCase { // Then XCTAssertEqual(structure.contents, [ .virtualGroup(name: "Dependencies", contents: [ - .project("/path/to/workspace/Tuist/Dependencies/SwiftPackageManager/.build/checkouts/AEXML/AEXML.xcodeproj"), + .project("/path/to/workspace/Tuist/.build/tuist-derived/AEXML/AEXML.xcodeproj"), .project( - "/path/to/workspace/Tuist/Dependencies/SwiftPackageManager/.build/checkouts/SwiftSyntax/SwiftSyntax.xcodeproj" + "/path/to/workspace/Tuist/.build/tuist-derived/SwiftSyntax/SwiftSyntax.xcodeproj" ), ]), ]) @@ -389,6 +388,15 @@ final class WorkspaceStructureGeneratorTests: XCTestCase { func inTemporaryDirectory(_: @escaping (AbsolutePath) async throws -> Void) async throws {} + func files( + in _: AbsolutePath, + filter _: ((URL) -> Bool)?, + nameFilter _: Set?, + extensionFilter _: Set? + ) -> Set { + [] + } + func glob(_: AbsolutePath, glob _: String) -> [AbsolutePath] { [] } diff --git a/Tests/TuistGeneratorTests/GraphMappers/AutogeneratedWorkspaceSchemeWorkspaceMapperTests.swift b/Tests/TuistGeneratorTests/GraphMappers/AutogeneratedWorkspaceSchemeWorkspaceMapperTests.swift index 620e7b1d014..27e47d7d531 100644 --- a/Tests/TuistGeneratorTests/GraphMappers/AutogeneratedWorkspaceSchemeWorkspaceMapperTests.swift +++ b/Tests/TuistGeneratorTests/GraphMappers/AutogeneratedWorkspaceSchemeWorkspaceMapperTests.swift @@ -1,8 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XCTest @testable import TuistGenerator @@ -373,18 +372,16 @@ final class AutogeneratedWorkspaceSchemeWorkspaceMapperTests: TuistUnitTestCase XCTAssertEmpty(sideEffects) let schemes = got.workspace.schemes - XCTAssertEqual(schemes.count, 2) + XCTAssertEqual(schemes.count, 1) XCTAssertEqual( Set(schemes.map(\.name)), Set([ - "A-Workspace-iOS", - "A-Workspace-macOS", + "A-Workspace", ]) ) - let iosScheme = try XCTUnwrap(schemes.first(where: { $0.name == "A-Workspace-iOS" })) - let macOSScheme = try XCTUnwrap(schemes.first(where: { $0.name == "A-Workspace-macOS" })) + let scheme = try XCTUnwrap(schemes.first(where: { $0.name == "A-Workspace" })) XCTAssertEqual( - iosScheme.buildAction.map(\.targets) ?? [], + scheme.buildAction.map(\.targets) ?? [], [ TargetReference( projectPath: projectPath, @@ -394,11 +391,6 @@ final class AutogeneratedWorkspaceSchemeWorkspaceMapperTests: TuistUnitTestCase projectPath: projectPath, name: targetATests.name ), - ] - ) - XCTAssertEqual( - macOSScheme.buildAction.map(\.targets) ?? [], - [ TargetReference( projectPath: projectBPath, name: targetB.name @@ -411,7 +403,7 @@ final class AutogeneratedWorkspaceSchemeWorkspaceMapperTests: TuistUnitTestCase ) XCTAssertEqual( - iosScheme.testAction.map(\.targets) ?? [], + scheme.testAction.map(\.targets) ?? [], [ TestableTarget( target: TargetReference( @@ -420,11 +412,6 @@ final class AutogeneratedWorkspaceSchemeWorkspaceMapperTests: TuistUnitTestCase ), parallelizable: false ), - ] - ) - XCTAssertEqual( - macOSScheme.testAction.map(\.targets) ?? [], - [ TestableTarget( target: TargetReference( projectPath: projectBPath, diff --git a/Tests/TuistGeneratorTests/GraphViz/GraphToGraphVizMapperTests.swift b/Tests/TuistGeneratorTests/GraphViz/GraphToGraphVizMapperTests.swift index 13b6d454e3a..d3dc34d23a4 100644 --- a/Tests/TuistGeneratorTests/GraphViz/GraphToGraphVizMapperTests.swift +++ b/Tests/TuistGeneratorTests/GraphViz/GraphToGraphVizMapperTests.swift @@ -1,10 +1,10 @@ import Foundation import GraphViz -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XCTest + @testable import TuistGenerator @testable import TuistSupportTesting @@ -194,56 +194,49 @@ final class GraphToGraphVizMapperTests: XCTestCase { return graph } - private func makeGivenGraph() throws -> TuistGraph.Graph { - let project = Project.test(path: "/") - let coreProject = Project.test(path: "/Core") - let externalProject = Project.test(path: "/Tuist/Dependencies", isExternal: true) + private func makeGivenGraph() throws -> XcodeGraph.Graph { let framework = GraphDependency.testFramework(path: try AbsolutePath(validating: "/XcodeProj.framework")) let library = GraphDependency.testLibrary(path: try AbsolutePath(validating: "/RxSwift.a")) let sdk = GraphDependency.testSDK(name: "CoreData.framework", status: .required, source: .developer) - + let projectPath: AbsolutePath = "/" + let coreProjectPath: AbsolutePath = "/Core" + let externalProjectPath: AbsolutePath = "/Tuist/Dependencies" + let coreTarget = Target.test(name: "Core") let core = GraphTarget.test( - path: coreProject.path, - target: Target.test(name: "Core") + path: coreProjectPath, + target: coreTarget ) let coreDependency = GraphDependency.target(name: core.target.name, path: core.path) + let coreTestsTarget = Target.test( + name: "CoreTests", + product: .unitTests + ) let coreTests = GraphTarget.test( - path: coreProject.path, - target: Target.test( - name: "CoreTests", - product: .unitTests - ) + path: coreProjectPath, + target: coreTestsTarget ) - - let iOSApp = GraphTarget.test(target: Target.test(name: "Tuist iOS")) - let watchApp = GraphTarget.test(target: Target.test( + let iOSAppTarget = Target.test(name: "Tuist iOS") + let iOSApp = GraphTarget.test(target: iOSAppTarget) + let watchAppTarget = Target.test( name: "Tuist watchOS", platform: .watchOS, deploymentTarget: .watchOS("6") - )) + ) + let watchApp = GraphTarget.test(target: watchAppTarget) - let externalTarget = GraphTarget.test(path: externalProject.path, target: Target.test(name: "External dependency")) + let externalTargetTarget = Target.test(name: "External dependency") + let externalTarget = GraphTarget.test(path: externalProjectPath, target: externalTargetTarget) + let project = Project.test(path: projectPath, targets: [iOSAppTarget, watchAppTarget]) + let coreProject = Project.test(path: coreProjectPath, targets: [coreTarget, coreTestsTarget]) + let externalProject = Project.test(path: "/Tuist/Dependencies", targets: [externalTargetTarget], isExternal: true) let externalDependency = GraphDependency.target(name: externalTarget.target.name, path: externalTarget.path) - let graph = TuistGraph.Graph.test( + let graph = XcodeGraph.Graph.test( projects: [ project.path: project, coreProject.path: coreProject, externalProject.path: externalProject, ], - targets: [ - project.path: [ - iOSApp.target.name: iOSApp.target, - watchApp.target.name: watchApp.target, - ], - coreProject.path: [ - core.target.name: core.target, - coreTests.target.name: coreTests.target, - ], - externalProject.path: [ - externalTarget.target.name: externalTarget.target, - ], - ], dependencies: [ .target(name: core.target.name, path: core.path): [ framework, diff --git a/Tests/TuistGeneratorTests/Linter/EnvironmentLinterTests.swift b/Tests/TuistGeneratorTests/Linter/EnvironmentLinterTests.swift index 34280bc4db8..5ba93f951de 100644 --- a/Tests/TuistGeneratorTests/Linter/EnvironmentLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/EnvironmentLinterTests.swift @@ -1,22 +1,22 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @testable import TuistGenerator @testable import TuistSupportTesting final class EnvironmentLinterTests: TuistUnitTestCase { - private var rootDirectoryLocator: MockRootDirectoryLocator! + private var rootDirectoryLocator: MockRootDirectoryLocating! var subject: EnvironmentLinter! override func setUp() { super.setUp() - rootDirectoryLocator = MockRootDirectoryLocator() + rootDirectoryLocator = .init() subject = EnvironmentLinter(rootDirectoryLocator: rootDirectoryLocator) } @@ -36,7 +36,9 @@ final class EnvironmentLinterTests: TuistUnitTestCase { Config.test(compatibleXcodeVersions: ["1.0", "4.3.2"]), ] - xcodeController.selectedStub = .success(Xcode.test(infoPlist: .test(version: "4.3.2"))) + given(xcodeController) + .selected() + .willReturn(.test(infoPlist: .test(version: "4.3.2"))) // When let got = try configs.flatMap { try subject.lintXcodeVersion(config: $0) } @@ -58,7 +60,9 @@ final class EnvironmentLinterTests: TuistUnitTestCase { Config.test(compatibleXcodeVersions: .list(["3.2.1"])), ] - xcodeController.selectedStub = .success(Xcode.test(infoPlist: .test(version: "4.3.2"))) + given(xcodeController) + .selected() + .willReturn(.test(infoPlist: .test(version: "4.3.2"))) for config in configs { // When @@ -74,7 +78,9 @@ final class EnvironmentLinterTests: TuistUnitTestCase { func test_lintXcodeVersion_doesntReturnIssues_whenAllVersionsAreSupported() throws { // Given let config = Config.test(compatibleXcodeVersions: .all) - xcodeController.selectedStub = .success(Xcode.test(infoPlist: .test(version: "4.3.2"))) + given(xcodeController) + .selected() + .willReturn(.test(infoPlist: .test(version: "4.3.2"))) // When let got = try subject.lintXcodeVersion(config: config) @@ -87,6 +93,10 @@ final class EnvironmentLinterTests: TuistUnitTestCase { // Given let config = Config.test(compatibleXcodeVersions: .list(["3.2.1"])) + given(xcodeController) + .selected() + .willReturn(nil) + // When let got = try subject.lintXcodeVersion(config: config) @@ -98,7 +108,9 @@ final class EnvironmentLinterTests: TuistUnitTestCase { // Given let config = Config.test(compatibleXcodeVersions: .list(["3.2.1"])) let error = NSError.test() - xcodeController.selectedStub = .failure(error) + given(xcodeController) + .selected() + .willThrow(error) // Then XCTAssertThrowsError(try subject.lintXcodeVersion(config: config)) { @@ -109,7 +121,9 @@ final class EnvironmentLinterTests: TuistUnitTestCase { func test_lintConfigPath_returnsALintingIssue_when_configManifestIsNotLocatedAtTuistDirectory() throws { // Given let fakeRoot = try! AbsolutePath(validating: "/root") - rootDirectoryLocator.locateStub = fakeRoot + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(fakeRoot) let configPath = fakeRoot.appending(try RelativePath(validating: "Config.swift")) let config = Config.test(path: configPath) @@ -125,7 +139,9 @@ final class EnvironmentLinterTests: TuistUnitTestCase { func test_lintConfigPath_doesntReturnALintingIssue_when_configManifestIsLocatedAtTuistDirectory() throws { // Given let fakeRoot = try! AbsolutePath(validating: "/root") - rootDirectoryLocator.locateStub = fakeRoot + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(fakeRoot) let configPath = fakeRoot .appending(try RelativePath(validating: "\(Constants.tuistDirectoryName)")) diff --git a/Tests/TuistGeneratorTests/Linter/GraphLinterTests.swift b/Tests/TuistGeneratorTests/Linter/GraphLinterTests.swift index 425dc585f18..cb23d2db770 100644 --- a/Tests/TuistGeneratorTests/Linter/GraphLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/GraphLinterTests.swift @@ -1,23 +1,22 @@ import Foundation -import TSCBasic +import MockableTest +import Path import struct TSCUtility.Version import TuistCore -import TuistGraph import TuistSupport +import TuistSupportTesting +import XcodeGraph import XCTest -@testable import TuistCoreTesting @testable import TuistGenerator -@testable import TuistGraphTesting -@testable import TuistSupportTesting final class GraphLinterTests: TuistUnitTestCase { - var subject: GraphLinter! - var graphTraverser: MockGraphTraverser! + private var subject: GraphLinter! + private var graphTraverser: MockGraphTraversing! override func setUp() { super.setUp() - graphTraverser = MockGraphTraverser() + graphTraverser = .init() subject = GraphLinter( projectLinter: MockProjectLinter(), staticProductsLinter: MockStaticProductsGraphLinter() @@ -33,8 +32,8 @@ final class GraphLinterTests: TuistUnitTestCase { func test_lint_when_frameworks_are_missing() throws { // Given let temporaryPath = try temporaryPath() - let frameworkAPath = temporaryPath.appending(try RelativePath(validating: "Carthage/Build/iOS/A.framework")) - let frameworkBPath = temporaryPath.appending(try RelativePath(validating: "Carthage/Build/iOS/B.framework")) + let frameworkAPath = temporaryPath.appending(try RelativePath(validating: "Test/Build/iOS/A.framework")) + let frameworkBPath = temporaryPath.appending(try RelativePath(validating: "Test/Build/iOS/B.framework")) try FileHandler.shared.createFolder(frameworkAPath) let graph = Graph.test(dependencies: [ GraphDependency.testFramework(path: frameworkAPath): Set(), @@ -58,7 +57,9 @@ final class GraphLinterTests: TuistUnitTestCase { let path: AbsolutePath = "/project" let package = Package.remote(url: "remote", requirement: .branch("master")) let versionStub = Version(10, 0, 0) - xcodeController.selectedVersionStub = .success(versionStub) + given(xcodeController) + .selectedVersion() + .willReturn(versionStub) let graph = Graph.test(packages: [path: ["package": package]]) let config = Config.test() let graphTraverser = GraphTraverser(graph: graph) @@ -77,7 +78,9 @@ final class GraphLinterTests: TuistUnitTestCase { let path: AbsolutePath = "/project" let package = Package.remote(url: "remote", requirement: .branch("master")) let versionStub = Version(11, 0, 0) - xcodeController.selectedVersionStub = .success(versionStub) + given(xcodeController) + .selectedVersion() + .willReturn(versionStub) let graph = Graph.test(packages: [path: ["package": package]]) let config = Config.test() let graphTraverser = GraphTraverser(graph: graph) @@ -91,12 +94,113 @@ final class GraphLinterTests: TuistUnitTestCase { XCTAssertFalse(result.contains(LintingIssue(reason: reason, severity: .error))) } + func test_lint_when_scheme_has_unknown_target() throws { + // Given + let path: AbsolutePath = "/project" + let unknownBuildReferenceTarget = TargetReference(projectPath: "/project", name: "UnknownReferenceTarget") + + let scheme = Scheme.test( + name: "SomeScheme", + buildAction: .init(targets: [unknownBuildReferenceTarget]), + testAction: nil, + runAction: nil, + archiveAction: nil, + profileAction: nil, + analyzeAction: nil + ) + let project = Project.test( + path: path, + name: "TuistProject", + targets: [ + Target.test(name: "App"), + ] + ) + + let workspace = Workspace.test( + path: path, + name: "TuistWorkspace", + projects: [path], + schemes: [scheme] + ) + + let graph = Graph.test( + path: path, + workspace: workspace, + projects: [path: project] + ) + + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let result = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertEqual( + result, + [LintingIssue( + reason: "Cannot find targets UnknownReferenceTarget (.) defined in SomeScheme", + severity: .warning + )] + ) + } + + func test_lint_when_scheme_has_known_target() throws { + // Given + let path: AbsolutePath = "/project" + let unknownBuildReferenceTarget = TargetReference(projectPath: "/project", name: "KnownReferenceTarget") + + let scheme = Scheme.test( + name: "SomeScheme", + buildAction: .init(targets: [unknownBuildReferenceTarget]), + testAction: nil, + runAction: nil, + archiveAction: nil, + profileAction: nil, + analyzeAction: nil + ) + let project = Project.test( + path: path, + name: "TuistProject", + targets: [ + Target.test(name: "KnownReferenceTarget"), + ] + ) + + let workspace = Workspace.test( + path: path, + name: "TuistWorkspace", + projects: [path], + schemes: [scheme] + ) + + let graph = Graph.test( + path: path, + workspace: workspace, + projects: [path: project] + ) + + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let result = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertEqual( + result, + [] + ) + } + func test_lint_when_no_version_available() throws { // Given let path: AbsolutePath = "/project" let package = Package.remote(url: "remote", requirement: .branch("master")) let error = NSError.test() - xcodeController.selectedVersionStub = .failure(error) + given(xcodeController) + .selectedVersion() + .willThrow(error) let graph = Graph.test(packages: [path: ["package": package]]) let config = Config.test() let graphTraverser = GraphTraverser(graph: graph) @@ -137,12 +241,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - appTarget.name: appTarget, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticLibrary.name: staticLibrary, - ]], dependencies: dependencies ) let config = Config.test() @@ -184,12 +282,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - appTarget.name: appTarget, - staticLibraryA.name: staticLibraryA, - staticLibraryB.name: staticLibraryB, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -234,12 +326,33 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - appTarget.name: appTarget, - messagesExtension.name: messagesExtension, - staticFramework.name: staticFramework, - staticLibrary.name: staticLibrary, - ]], + dependencies: dependencies + ) + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let result = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertTrue(result.isEmpty) + } + + func test_lint_appExtension_canDependOnBundle() throws { + // Given + let path: AbsolutePath = "/project" + let appExtension = Target.empty(name: "app_extension", product: .appExtension) + let bundle = Target.empty(name: "bundle", product: .bundle) + let project = Project.empty(path: path) + + let dependencies: [GraphDependency: Set] = [ + .target(name: appExtension.name, path: path): Set([.target(name: bundle.name, path: path)]), + .target(name: bundle.name, path: path): Set([]), + ] + + let graph = Graph.test( + path: path, + projects: [path: project], dependencies: dependencies ) let config = Config.test() @@ -267,10 +380,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - bundle.name: bundle, - framework.name: framework, - ]], dependencies: dependencies ) let config = Config.test() @@ -298,10 +407,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - bundle.name: bundle, - application.name: application, - ]], dependencies: dependencies ) let config = Config.test() @@ -341,13 +446,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - xpc.name: xpc, - dynamicFramework.name: dynamicFramework, - dynamicLibrary.name: dynamicLibrary, - staticFramework.name: staticFramework, - staticLibrary.name: staticLibrary, - ]], dependencies: dependencies ) let config = Config.test() @@ -377,11 +475,44 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - bundle.name: bundle, - unitTests.name: unitTests, - uiTests.name: uiTests, - ]], + dependencies: dependencies + ) + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let result = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertTrue(result.isEmpty) + } + + func test_lint_testTargetsDependsOnAppExtension() throws { + // Given + let path: AbsolutePath = "/project" + let appTarget = Target.test(name: "AppTarget", product: .app) + let appExtension = Target.test(name: "app_extension", platform: .iOS, product: .appExtension) + let appExtensionTests = Target.test(name: "unitTests", platform: .iOS, product: .unitTests) + + let project = Project.test(path: "/tmp/app", name: "App", targets: [ + appTarget, + appExtension, + appExtensionTests, + ]) + + let dependencies: [GraphDependency: Set] = [ + .target(name: appTarget.name, path: path): Set([ + .target(name: appExtension.name, path: path), + ]), + .target(name: appExtension.name, path: path): Set([]), + .target(name: appExtensionTests.name, path: path): Set([ + .target(name: appExtension.name, path: path), + ]), + ] + + let graph = Graph.test( + path: path, + projects: [path: project], dependencies: dependencies ) let config = Config.test() @@ -411,11 +542,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - staticFramework.name: staticFramework, - staticLibrary.name: staticLibrary, - dynamicFramework.name: dynamicFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -434,7 +560,7 @@ final class GraphLinterTests: TuistUnitTestCase { let macStaticFramework = Target.empty(name: "MacStaticFramework", destinations: .macOS, product: .staticFramework) let iosStaticFramework = Target.empty(name: "iOSStaticFramework", destinations: .iOS, product: .staticFramework) let iosStaticLibrary = Target.empty(name: "iOSStaticLibrary", destinations: .iOS, product: .staticLibrary) - let project = Project.empty(path: path) + let project = Project.empty(path: path, targets: [macStaticFramework, iosStaticLibrary, iosStaticFramework]) let dependencies: [GraphDependency: Set] = [ .target( @@ -451,11 +577,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - macStaticFramework.name: macStaticFramework, - iosStaticFramework.name: iosStaticFramework, - iosStaticLibrary.name: iosStaticLibrary, - ]], dependencies: dependencies ) let config = Config.test() @@ -483,10 +604,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - watchExtension.name: watchExtension, - watchApp.name: watchApp, - ]], dependencies: dependencies ) let config = Config.test() @@ -504,7 +621,7 @@ final class GraphLinterTests: TuistUnitTestCase { let path: AbsolutePath = "/project" let invalidDependency = Target.empty(name: "Framework", destinations: .watchOS, product: .framework) let watchApp = Target.empty(name: "WatchApp", destinations: .watchOS, product: .watch2App) - let project = Project.empty(path: path) + let project = Project.empty(path: path, targets: [invalidDependency, watchApp]) let dependencies: [GraphDependency: Set] = [ .target(name: watchApp.name, path: path): Set([.target(name: invalidDependency.name, path: path)]), @@ -514,10 +631,6 @@ final class GraphLinterTests: TuistUnitTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - invalidDependency.name: invalidDependency, - watchApp.name: watchApp, - ]], dependencies: dependencies ) let config = Config.test() @@ -555,7 +668,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [watchApp.name: watchApp, watchAppTests.name: watchAppTests]], dependencies: dependencies ) let config = Config.test() @@ -593,7 +705,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [staticLibrary.name: staticLibrary, watchAppTests.name: watchAppTests]], dependencies: dependencies ) let config = Config.test() @@ -631,7 +742,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [framework.name: framework, watchAppTests.name: watchAppTests]], dependencies: dependencies ) let config = Config.test() @@ -669,7 +779,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [staticFramework.name: staticFramework, watchAppTests.name: watchAppTests]], dependencies: dependencies ) let config = Config.test() @@ -722,13 +831,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [ - path: [ - watchApplication.name: watchApplication, - staticFramework.name: staticFramework, - dynamicFramework.name: dynamicFramework, - ], - ], dependencies: dependencies ) let config = Config.test() @@ -773,12 +875,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [ - path: [ - watchApplication.name: watchApplication, - widgetExtension.name: widgetExtension, - ], - ], dependencies: dependencies ) let config = Config.test() @@ -820,12 +916,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [ - path: [ - app.name: app, - watchApplication.name: watchApplication, - ], - ], dependencies: dependencies ) let config = Config.test() @@ -880,11 +970,6 @@ final class GraphLinterTests: TuistUnitTestCase { projectBPath: projectB, projectCPath: projectC, ], - targets: [ - projectAPath: [targetA.name: targetA], - projectBPath: [targetB.name: targetB], - projectCPath: [targetC.name: targetC], - ], dependencies: dependencies ) let config = Config.test() @@ -955,11 +1040,6 @@ final class GraphLinterTests: TuistUnitTestCase { projectBPath: projectB, projectCPath: projectC, ], - targets: [ - projectAPath: [targetA.name: targetA], - projectBPath: [targetB.name: targetB], - projectCPath: [targetC.name: targetC], - ], dependencies: dependencies ) let config = Config.test() @@ -1032,11 +1112,6 @@ final class GraphLinterTests: TuistUnitTestCase { projectBPath: projectB, projectCPath: projectC, ], - targets: [ - projectAPath: [targetA.name: targetA], - projectBPath: [targetB.name: targetB], - projectCPath: [targetC.name: targetC], - ], dependencies: dependencies ) let config = Config.test() @@ -1081,7 +1156,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [app.name: app, watchApp.name: watchApp, watchExtension.name: watchExtension]], dependencies: dependencies ) let config = Config.test() @@ -1126,7 +1200,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [app.name: app, watchApp.name: watchApp, watchExtension.name: watchExtension]], dependencies: dependencies ) let config = Config.test() @@ -1180,7 +1253,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: temporaryPath, workspace: Workspace.test(projects: [temporaryPath]), projects: [temporaryPath: project], - targets: [temporaryPath: [app.name: app, appClip.name: appClip]], dependencies: dependencies ) let config = Config.test() @@ -1226,7 +1298,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: temporaryPath, workspace: Workspace.test(projects: [temporaryPath]), projects: [temporaryPath: project], - targets: [temporaryPath: [app.name: app, appClip.name: appClip]], dependencies: dependencies ) let config = Config.test() @@ -1269,7 +1340,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [app.name: app, appClip.name: appClip]], dependencies: dependencies ) let config = Config.test() @@ -1313,7 +1383,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [app.name: app, appClip.name: appClip]], dependencies: dependencies ) let config = Config.test() @@ -1380,7 +1449,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: temporaryPath, workspace: Workspace.test(projects: [temporaryPath]), projects: [temporaryPath: project], - targets: [temporaryPath: [app.name: app, appClip1.name: appClip1, appClip2.name: appClip2]], dependencies: dependencies ) let config = Config.test() @@ -1434,7 +1502,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: temporaryPath, workspace: Workspace.test(projects: [temporaryPath]), projects: [temporaryPath: project], - targets: [temporaryPath: [app.name: app, appClip.name: appClip, framework.name: framework]], dependencies: dependencies ) let config = Config.test() @@ -1474,7 +1541,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [tool.name: tool, dynamic.name: dynamic]], dependencies: dependencies ) let config = Config.test() @@ -1514,7 +1580,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [tool.name: tool, dynamic.name: dynamic]], dependencies: dependencies ) let config = Config.test() @@ -1565,7 +1630,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [path: [tool.name: tool, staticFmwk.name: staticFmwk, staticLib.name: staticLib]], dependencies: dependencies ) let config = Config.test() @@ -1605,10 +1669,6 @@ final class GraphLinterTests: TuistUnitTestCase { path: path, workspace: Workspace.test(projects: [path]), projects: [path: project], - targets: [ - path: [appTarget.name: appTarget], - frameworks1.path: [frameworkA.name: frameworkA, frameworkB.name: frameworkB], - ], dependencies: dependencies ) let config = Config.test() @@ -1644,17 +1704,6 @@ final class GraphLinterTests: TuistUnitTestCase { projects: [ project.path: project, ], - targets: [ - project.path: [ - appTarget.name: appTarget, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - frameworkD.name: frameworkD, - frameworkE.name: frameworkE, - frameworkF.name: frameworkF, - ], - ], dependencies: [:] ) let config = Config.test() @@ -1708,15 +1757,23 @@ final class GraphLinterTests: TuistUnitTestCase { path: temporaryPath, targets: [targetA, targetB], schemes: [ - .test(testAction: .test( - coverage: true, - codeCoverageTargets: [ - TargetReference( - projectPath: temporaryPath, - name: "TargetA" - ), - ] - )), + .test( + buildAction: nil, + testAction: .test( + targets: [], + coverage: true, + codeCoverageTargets: [ + TargetReference( + projectPath: temporaryPath, + name: "TargetA" + ), + ] + ), + runAction: nil, + archiveAction: nil, + profileAction: nil, + analyzeAction: nil + ), ] ) @@ -1726,13 +1783,7 @@ final class GraphLinterTests: TuistUnitTestCase { autogeneratedWorkspaceSchemes: .enabled(codeCoverageMode: .relevant, testingOptions: []) ) ), - projects: [temporaryPath: project], - targets: [ - temporaryPath: [ - "TargetA": targetA, - "TargetB": targetB, - ], - ] + projects: [temporaryPath: project] ) let config = Config.test() let graphTraverser = GraphTraverser(graph: graph) @@ -1789,13 +1840,7 @@ final class GraphLinterTests: TuistUnitTestCase { ) ) ), - projects: [temporaryPath: project], - targets: [ - temporaryPath: [ - "TargetA": .test(), - "TargetB": .test(), - ], - ] + projects: [temporaryPath: project] ) let config = Config.test() let graphTraverser = GraphTraverser(graph: graph) @@ -1852,13 +1897,7 @@ final class GraphLinterTests: TuistUnitTestCase { ) ) ), - projects: [temporaryPath: project], - targets: [ - temporaryPath: [ - "TargetB": .test(), - "TargetC": .test(), - ], - ] + projects: [temporaryPath: project] ) let config = Config.test() let graphTraverser = GraphTraverser(graph: graph) @@ -1893,17 +1932,17 @@ final class GraphLinterTests: TuistUnitTestCase { ) let graph = Graph.test( projects: [path: project], - targets: [ - path: [ - iOSAndMacTarget.name: iOSAndMacTarget, - macOnlyTarget.name: macOnlyTarget, - ], - ], dependencies: [ .target(name: iOSAndMacTarget.name, path: path): [ .target(name: macOnlyTarget.name, path: path), ], .target(name: macOnlyTarget.name, path: path): [], + ], + dependencyConditions: [ + GraphEdge( + from: .target(name: iOSAndMacTarget.name, path: path), + to: .target(name: macOnlyTarget.name, path: path) + ): try XCTUnwrap(.test([.macos])), ] ) let config = Config.test() @@ -1931,12 +1970,6 @@ final class GraphLinterTests: TuistUnitTestCase { ) let graph = Graph.test( projects: [path: project], - targets: [ - path: [ - iOSAndMacTarget.name: iOSAndMacTarget, - watchOnlyTarget.name: watchOnlyTarget, - ], - ], dependencies: [ .target(name: iOSAndMacTarget.name, path: path): [ .target(name: watchOnlyTarget.name, path: path), @@ -1953,4 +1986,56 @@ final class GraphLinterTests: TuistUnitTestCase { // Then XCTAssertFalse(results.isEmpty) } + + func test_lint_multiDestinationTarget_dependsOnTargetWithFewerSupportedPlatforms() throws { + // Given + let path = try temporaryPath() + let iOSAndMacTarget = Target.test(name: "IOSAndMacTarget", destinations: [.iPhone, .mac], product: .framework) + let iOSOnlyTarget = Target.test(name: "iOSOnlyTarget", destinations: [.iPhone], product: .framework) + + let iOSApp = Target.test(name: "iOSApp", destinations: [.iPhone], product: .app) + let watchApp = Target.test( + name: "WatchApp", + destinations: [.appleWatch], + product: .watch2App, + bundleId: "io.tuist.iOSApp.WatchApp" + ) + + let project = Project.test( + path: path, + targets: [ + iOSAndMacTarget, + iOSOnlyTarget, + iOSApp, + watchApp, + ] + ) + let graph = Graph.test( + projects: [path: project], + dependencies: [ + .target(name: iOSAndMacTarget.name, path: path): [ + .target(name: iOSOnlyTarget.name, path: path), + ], + .target(name: iOSOnlyTarget.name, path: path): [], + .target(name: iOSApp.name, path: path): [ + .target(name: watchApp.name, path: path), + ], + .target(name: watchApp.name, path: path): [], + ] + ) + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let results = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertEqual( + results, + [LintingIssue( + reason: "Target IOSAndMacTarget which depends on iOSOnlyTarget does not support the required platforms: macos. The dependency on iOSOnlyTarget must have a dependency condition constraining to at most: ios.", + severity: .error + )] + ) + } } diff --git a/Tests/TuistGeneratorTests/Linter/Mocks/MockPackageLinter.swift b/Tests/TuistGeneratorTests/Linter/Mocks/MockPackageLinter.swift index e62236fd82e..318297d8786 100644 --- a/Tests/TuistGeneratorTests/Linter/Mocks/MockPackageLinter.swift +++ b/Tests/TuistGeneratorTests/Linter/Mocks/MockPackageLinter.swift @@ -1,8 +1,7 @@ import Foundation import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph @testable import TuistGenerator class MockPackageLinter: PackageLinting { diff --git a/Tests/TuistGeneratorTests/Linter/Mocks/MockProjectLinter.swift b/Tests/TuistGeneratorTests/Linter/Mocks/MockProjectLinter.swift index bd1fb1fc014..8c2aef7389c 100644 --- a/Tests/TuistGeneratorTests/Linter/Mocks/MockProjectLinter.swift +++ b/Tests/TuistGeneratorTests/Linter/Mocks/MockProjectLinter.swift @@ -1,9 +1,8 @@ import Foundation import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph @testable import TuistGenerator class MockProjectLinter: ProjectLinting { diff --git a/Tests/TuistGeneratorTests/Linter/Mocks/MockSchemeLinter.swift b/Tests/TuistGeneratorTests/Linter/Mocks/MockSchemeLinter.swift index 27a85c0b22b..b2987edaec2 100644 --- a/Tests/TuistGeneratorTests/Linter/Mocks/MockSchemeLinter.swift +++ b/Tests/TuistGeneratorTests/Linter/Mocks/MockSchemeLinter.swift @@ -1,8 +1,7 @@ import Foundation import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph @testable import TuistGenerator class MockSchemeLinter: SchemeLinting { diff --git a/Tests/TuistGeneratorTests/Linter/Mocks/MockSettingsLinter.swift b/Tests/TuistGeneratorTests/Linter/Mocks/MockSettingsLinter.swift index 314816b2d16..9deb38a314c 100644 --- a/Tests/TuistGeneratorTests/Linter/Mocks/MockSettingsLinter.swift +++ b/Tests/TuistGeneratorTests/Linter/Mocks/MockSettingsLinter.swift @@ -1,8 +1,7 @@ import Foundation import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph @testable import TuistGenerator class MockSettingsLinter: SettingsLinting { diff --git a/Tests/TuistGeneratorTests/Linter/Mocks/MockStaticProductsGraphLinter.swift b/Tests/TuistGeneratorTests/Linter/Mocks/MockStaticProductsGraphLinter.swift index 892742cbde2..68582eafccf 100644 --- a/Tests/TuistGeneratorTests/Linter/Mocks/MockStaticProductsGraphLinter.swift +++ b/Tests/TuistGeneratorTests/Linter/Mocks/MockStaticProductsGraphLinter.swift @@ -1,6 +1,5 @@ import Foundation import TuistCore -import TuistGraph @testable import TuistGenerator class MockStaticProductsGraphLinter: StaticProductsGraphLinting { diff --git a/Tests/TuistGeneratorTests/Linter/Mocks/MockTargetLinter.swift b/Tests/TuistGeneratorTests/Linter/Mocks/MockTargetLinter.swift index d310a381832..f62868f7f1d 100644 --- a/Tests/TuistGeneratorTests/Linter/Mocks/MockTargetLinter.swift +++ b/Tests/TuistGeneratorTests/Linter/Mocks/MockTargetLinter.swift @@ -1,12 +1,11 @@ import Foundation import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph @testable import TuistGenerator class MockTargetLinter: TargetLinting { - func lint(target _: Target) -> [LintingIssue] { + func lint(target _: Target, options _: Project.Options) -> [LintingIssue] { [] } } diff --git a/Tests/TuistGeneratorTests/Linter/PackageLinterTests.swift b/Tests/TuistGeneratorTests/Linter/PackageLinterTests.swift index ed119f16240..e638ad7e897 100644 --- a/Tests/TuistGeneratorTests/Linter/PackageLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/PackageLinterTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting diff --git a/Tests/TuistGeneratorTests/Linter/ProjectLinterTests.swift b/Tests/TuistGeneratorTests/Linter/ProjectLinterTests.swift index dd75e01f7a1..78dad9dbff8 100644 --- a/Tests/TuistGeneratorTests/Linter/ProjectLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/ProjectLinterTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator @@ -39,19 +38,6 @@ final class ProjectLinterTests: XCTestCase { super.tearDown() } - func test_validate_when_there_are_duplicated_targets() throws { - let target = Target.test(name: "A") - let project = Project.test(targets: [target, target]) - let got = subject.lint(project) - XCTAssertTrue( - got - .contains(LintingIssue( - reason: "Targets A from project at \(project.path.pathString) have duplicates.", - severity: .error - )) - ) - } - func test_lint_valid_watchTargetBundleIdentifiers() throws { // Given let app = Target.test(name: "App", product: .app, bundleId: "app") diff --git a/Tests/TuistGeneratorTests/Linter/SchemeLinterTests.swift b/Tests/TuistGeneratorTests/Linter/SchemeLinterTests.swift index d0c28b94eb6..08900b93eb6 100644 --- a/Tests/TuistGeneratorTests/Linter/SchemeLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/SchemeLinterTests.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting @@ -115,7 +114,7 @@ class SchemeLinterTests: TuistTestCase { expandVariableFromTarget: nil, preActions: [], postActions: [], - diagnosticsOptions: [] + diagnosticsOptions: SchemeDiagnosticsOptions() ) ), ] diff --git a/Tests/TuistGeneratorTests/Linter/SettingsLinterTests.swift b/Tests/TuistGeneratorTests/Linter/SettingsLinterTests.swift index 80da003fff3..82fe6adbfb4 100644 --- a/Tests/TuistGeneratorTests/Linter/SettingsLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/SettingsLinterTests.swift @@ -1,11 +1,10 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator -@testable import TuistGraphTesting @testable import TuistSupportTesting final class SettingsLinterTests: TuistUnitTestCase { diff --git a/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift b/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift index 58c9a59935b..e4f911ef4cd 100644 --- a/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift @@ -1,11 +1,10 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistCoreTesting @testable import TuistGenerator -@testable import TuistGraphTesting @testable import TuistSupportTesting class StaticProductsGraphLinterTests: XCTestCase { @@ -26,7 +25,7 @@ class StaticProductsGraphLinterTests: XCTestCase { let path: AbsolutePath = "/project" let app = Target.test(name: "App") let framework = Target.test(name: "Framework", product: .framework) - let project = Project.test(path: "/tmp/app", name: "AppProject") + let project = Project.test(path: "/tmp/app", name: "AppProject", targets: [app, framework]) let package = Package.remote(url: "https://test.tuist.io", requirement: .branch("main")) let appDependency = GraphDependency.target(name: app.name, path: path) let frameworkDependency = GraphDependency.target(name: framework.name, path: path) @@ -40,10 +39,6 @@ class StaticProductsGraphLinterTests: XCTestCase { path: path, projects: [path: project], packages: [path: ["Package": package]], - targets: [path: [ - app.name: app, - framework.name: framework, - ]], dependencies: dependencies ) let config = Config.test() @@ -63,7 +58,7 @@ class StaticProductsGraphLinterTests: XCTestCase { let path: AbsolutePath = "/project" let app = Target.test(name: "App") let framework = Target.test(name: "Framework", product: .framework) - let project = Project.test(path: "/tmp/app", name: "AppProject") + let project = Project.test(path: "/tmp/app", name: "AppProject", targets: [app, framework]) let package = Package.remote(url: "https://test.tuist.io", requirement: .branch("main")) let appDependency = GraphDependency.target(name: app.name, path: path) let frameworkDependency = GraphDependency.target(name: framework.name, path: path) @@ -78,10 +73,6 @@ class StaticProductsGraphLinterTests: XCTestCase { path: path, projects: [path: project], packages: [path: ["Package": package]], - targets: [path: [ - app.name: app, - framework.name: framework, - ]], dependencies: dependencies ) let config = Config.test() @@ -116,10 +107,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - framework.name: framework, - ]], dependencies: dependencies ) let config = Config.test() @@ -154,11 +141,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - framework.name: framework, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -206,15 +188,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - ]], dependencies: dependencies ) let config = Config.test() @@ -229,6 +202,61 @@ class StaticProductsGraphLinterTests: XCTestCase { ]) } + /// Dependencies between XCFrameworks are preserved when replacing target nodes with binaries for Tuist Cache. + /// See this PR for more details: https://github.com/tuist/tuist/pull/6592 + func test_lint_whenStaticProductLinkedTwice_transitiveStaticXCFrameworks() throws { + // Given + let path: AbsolutePath = "/project" + let app = Target.test(name: "App") + let staticXCFrameworkA = GraphDependency.testXCFramework( + path: path.appending(component: "StaticXCFrameworkA"), + linking: .static + ) + let staticXCFrameworkB = GraphDependency.testXCFramework( + path: path.appending(component: "StaticXCFrameworkB"), + linking: .static + ) + let staticXCFrameworkC = GraphDependency.testXCFramework( + path: path.appending(component: "StaticXCFrameworkC"), + linking: .static + ) + let frameworkA = Target.test(name: "FrameworkA", product: .framework) + let frameworkB = Target.test(name: "FrameworkB", product: .framework) + let frameworkC = Target.test(name: "FrameworkC", product: .framework) + let project = Project + .test(targets: [app, frameworkA, frameworkB, frameworkC]) + + let appDependency = GraphDependency.target(name: app.name, path: path) + let frameworkADependency = GraphDependency.target(name: frameworkA.name, path: path) + let frameworkBDependency = GraphDependency.target(name: frameworkB.name, path: path) + let frameworkCDependency = GraphDependency.target(name: frameworkC.name, path: path) + + let dependencies: [GraphDependency: Set] = [ + appDependency: Set([staticXCFrameworkC, frameworkADependency]), + frameworkADependency: Set([frameworkBDependency]), + frameworkBDependency: Set([frameworkCDependency]), + frameworkCDependency: Set([staticXCFrameworkA]), + staticXCFrameworkA: Set([staticXCFrameworkB]), + staticXCFrameworkB: Set([staticXCFrameworkC]), + staticXCFrameworkC: Set([]), + ] + let graph = Graph.test( + path: path, + projects: [path: project], + dependencies: dependencies + ) + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let results = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertEqual(results, [ + warning(product: "StaticXCFrameworkC", type: "Xcframework", linkedBy: [appDependency, frameworkCDependency]), + ]) + } + func test_lint_whenStaticProductLinkedTwice_transitiveFrameworks() throws { // Given let path: AbsolutePath = "/project" @@ -262,15 +290,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - ]], dependencies: dependencies ) let config = Config.test() @@ -294,7 +313,7 @@ class StaticProductsGraphLinterTests: XCTestCase { let frameworkB = Target.test(name: "FrameworkB", product: .framework) let frameworkC = Target.test(name: "FrameworkC", product: .framework) let frameworkD = Target.test(name: "FrameworkD", product: .framework) - let project = Project.test(targets: [app, frameworkA, frameworkB, frameworkC, staticFrameworkA]) + let project = Project.test(targets: [app, frameworkA, frameworkB, frameworkC, frameworkD, staticFrameworkA]) let appDependency = GraphDependency.target(name: app.name, path: path) let staticFrameworkAdependency = GraphDependency.target(name: staticFrameworkA.name, path: path) @@ -314,14 +333,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - staticFrameworkA.name: staticFrameworkA, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - frameworkD.name: frameworkD, - ]], dependencies: dependencies ) let config = Config.test() @@ -357,11 +368,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - staticFramework.name: staticFramework, - framework.name: framework, - ]], dependencies: dependencies ) let config = Config.test() @@ -407,15 +413,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - ]], dependencies: dependencies ) let config = Config.test() @@ -448,11 +445,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - staticFramework.name: staticFramework, - frameworkTests.name: frameworkTests, - ]], dependencies: dependencies ) let config = Config.test() @@ -472,7 +464,7 @@ class StaticProductsGraphLinterTests: XCTestCase { let framework = Target.test(name: "Framework", product: .framework) let staticFramework = Target.test(name: "StaticFramework", product: .staticFramework) let frameworkTests = Target.test(name: "FrameworkTests", product: .unitTests) - let project = Project.test(targets: [app, staticFramework, frameworkTests]) + let project = Project.test(targets: [app, framework, staticFramework, frameworkTests]) let appDependency = GraphDependency.target(name: app.name, path: path) let frameworkDependency = GraphDependency.target(name: framework.name, path: path) @@ -488,12 +480,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - framework.name: framework, - staticFramework.name: staticFramework, - frameworkTests.name: frameworkTests, - ]], dependencies: dependencies ) let config = Config.test() @@ -528,11 +514,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appTestsTarget.name: appTestsTarget, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -613,21 +594,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appTests.name: appTests, - appUITests.name: appUITests, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - frameworkTests.name: frameworkTests, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - staticFrameworkATests.name: staticFrameworkATests, - staticFrameworkBTests.name: staticFrameworkBTests, - staticFrameworkCTests.name: staticFrameworkCTests, - ]], dependencies: dependencies ) let config = Config.test() @@ -660,11 +626,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - appClip.name: appClip, - appClipTestsTarget.name: appClipTestsTarget, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -729,17 +690,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appTests.name: appTests, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - frameworkTests.name: frameworkTests, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - ]], dependencies: dependencies ) let config = Config.test() @@ -782,13 +732,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appTests.name: appTests, - appUITests.name: appUITests, - frameworkA.name: frameworkA, - staticFrameworkA.name: staticFrameworkA, - ]], dependencies: dependencies ) let config = Config.test() @@ -827,12 +770,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appUITests.name: appUITests, - frameworkA.name: frameworkA, - staticFrameworkA.name: staticFrameworkA, - ]], dependencies: dependencies ) let config = Config.test() @@ -873,12 +810,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appUITests.name: appUITests, - frameworkA.name: frameworkA, - staticFrameworkA.name: staticFrameworkA, - ]], dependencies: dependencies ) let config = Config.test() @@ -918,12 +849,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appUITests.name: appUITests, - frameworkA.name: frameworkA, - staticFrameworkA.name: staticFrameworkA, - ]], dependencies: dependencies ) let config = Config.test() @@ -960,11 +885,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appExtension.name: appExtension, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -1001,12 +921,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appExtension.name: appExtension, - staticFramework.name: staticFramework, - framework.name: framework, - ]], dependencies: dependencies ) let config = Config.test() @@ -1042,11 +956,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appClip.name: appClip, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -1083,12 +992,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - appClip.name: appClip, - framework.name: framework, - staticFramework.name: staticFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -1126,11 +1029,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - watchApp.name: watchApp, - watchAppExtension.name: watchAppExtension, - ]], dependencies: dependencies ) let config = Config.test() @@ -1169,12 +1067,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - watchApp.name: watchApp, - watchAppExtension.name: watchAppExtension, - watchFramework.name: watchFramework, - ]], dependencies: dependencies ) let config = Config.test() @@ -1222,15 +1114,6 @@ class StaticProductsGraphLinterTests: XCTestCase { let graph = Graph.test( path: path, projects: [path: project], - targets: [path: [ - app.name: app, - staticFrameworkA.name: staticFrameworkA, - staticFrameworkB.name: staticFrameworkB, - staticFrameworkC.name: staticFrameworkC, - frameworkA.name: frameworkA, - frameworkB.name: frameworkB, - frameworkC.name: frameworkC, - ]], dependencies: dependencies ) let config = Config @@ -1271,12 +1154,6 @@ class StaticProductsGraphLinterTests: XCTestCase { 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() diff --git a/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift b/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift index 00d0d492ca4..008370851ae 100644 --- a/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting @@ -24,46 +23,91 @@ final class TargetLinterTests: TuistUnitTestCase { func test_lint_when_target_has_invalid_product_name() { let XCTAssertInvalidProductNameApp: (Target) -> Void = { target in - let got = self.subject.lint(target: target) + let got = self.subject.lint(target: target, options: .test()) let reason: String switch target.product { - case .app, .commandLineTool: + case .framework, .staticFramework: reason = - "Invalid product name '\(target.productName)'. This string must contain only alphanumeric (A-Z,a-z,0-9), period (.), and underscore (_) characters." + "Invalid product name '\(target.productName)'. This string must contain only alphanumeric (A-Z,a-z,0-9), and underscore (_) characters." default: reason = - "Invalid product name '\(target.productName)'. This string must contain only alphanumeric (A-Z,a-z,0-9), and underscore (_) characters." + "Invalid product name '\(target.productName)'. This string must contain only alphanumeric (A-Z,a-z,0-9), period (.), hyphen (-), and underscore (_) characters." } self.XCTContainsLintingIssue(got, LintingIssue(reason: reason, severity: .warning)) } let XCTAssertValidProductNameApp: (Target) -> Void = { target in - let got = self.subject.lint(target: target) + let got = self.subject.lint(target: target, options: .test()) XCTAssertNil(got.first(where: { $0.description.contains("Invalid product name") })) } XCTAssertValidProductNameApp(Target.test(product: .app, productName: "MyApp.iOS")) + XCTAssertValidProductNameApp(Target.test(product: .app, productName: "MyApp-iOS")) + XCTAssertValidProductNameApp(Target.test(product: .bundle, productName: "MyBundle.macOS")) + XCTAssertValidProductNameApp(Target.test(product: .bundle, productName: "MyBundle-macOS")) XCTAssertValidProductNameApp(Target.test(productName: "MyFramework_iOS")) XCTAssertValidProductNameApp(Target.test(productName: "MyFramework")) XCTAssertInvalidProductNameApp(Target.test(product: .framework, productName: "MyFramework.iOS")) - XCTAssertInvalidProductNameApp(Target.test(productName: "MyFramework-iOS")) + XCTAssertInvalidProductNameApp(Target.test(product: .framework, productName: "MyFramework-iOS")) XCTAssertInvalidProductNameApp(Target.test(productName: "ⅫFramework")) XCTAssertInvalidProductNameApp(Target.test(productName: "ؼFramework")) } + func test_lint_when_inconsistentProductNameBuildSettingAcrossConfigurations() { + // Given + let target = Target.test(settings: .test( + base: ["PRODUCT_NAME": "1"], + debug: .test(settings: ["PRODUCT_NAME": "2"]), + release: .test(settings: ["PRODUCT_NAME": "3"]) + )) + + // When + let got = subject.lint(target: target, options: .test()) + + // Then + XCTContainsLintingIssue( + got, + LintingIssue( + reason: "The target '\(target.name)' has a PRODUCT_NAME build setting that is different across configurations and might cause unpredictable behaviours.", + severity: .warning + ) + ) + } + + func test_lint_when_productNameBuildSettingWithVariables() { + // Given + let target = Target.test(settings: .test( + base: ["PRODUCT_NAME": "$VARIABLE"], + debug: .test(settings: ["PRODUCT_NAME": "$VARIABLE"]), + release: .test(settings: ["PRODUCT_NAME": "$VARIABLE"]) + )) + + // When + let got = subject.lint(target: target, options: .test()) + + // Then + XCTContainsLintingIssue( + got, + LintingIssue( + reason: "The target '\(target.name)' has a PRODUCT_NAME build setting containing variables that are resolved at build time, and might cause unpredictable behaviours.", + severity: .warning + ) + ) + } + func test_lint_when_target_has_invalid_bundle_identifier() { let XCTAssertInvalidBundleId: (String) -> Void = { bundleId in let target = Target.test(bundleId: bundleId) - let got = self.subject.lint(target: target) + let got = self.subject.lint(target: target, options: .test()) let reason = "Invalid bundle identifier '\(bundleId)'. This string must be a uniform type identifier (UTI) that contains only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) characters." self.XCTContainsLintingIssue(got, LintingIssue(reason: reason, severity: .error)) } let XCTAssertValidBundleId: (String) -> Void = { bundleId in let target = Target.test(bundleId: bundleId) - let got = self.subject.lint(target: target) + let got = self.subject.lint(target: target, options: .test()) XCTAssertNil(got.first(where: { $0.description.contains("Invalid bundle identifier") })) } @@ -76,7 +120,7 @@ final class TargetLinterTests: TuistUnitTestCase { func test_lint_when_target_no_source_files() { let target = Target.test(sources: []) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTContainsLintingIssue( got, @@ -88,7 +132,7 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test(sources: [], dependencies: [ TargetDependency.sdk(name: "libc++.tbd", status: .optional), ]) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTAssertEqual(0, got.count) } @@ -97,7 +141,7 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test(sources: [], scripts: [ TargetScript(name: "Test script", order: .post, script: .embedded("echo 'This is a test'")), ]) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTAssertEqual(0, got.count) } @@ -108,13 +152,15 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test( infoPlist: .file(path: infoPlistPath), - resources: [ - .file(path: infoPlistPath), - .file(path: googeServiceInfoPlistPath), - ] + resources: .init( + [ + .file(path: infoPlistPath), + .file(path: googeServiceInfoPlistPath), + ] + ) ) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTContainsLintingIssue( got, @@ -134,9 +180,9 @@ final class TargetLinterTests: TuistUnitTestCase { func test_lint_when_a_entitlements_file_is_being_copied() { let path = try! AbsolutePath(validating: "/App.entitlements") - let target = Target.test(resources: [.file(path: path)]) + let target = Target.test(resources: .init([.file(path: path)])) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTContainsLintingIssue( got, @@ -152,7 +198,7 @@ final class TargetLinterTests: TuistUnitTestCase { let path = temporaryPath.appending(component: "Info.plist") let target = Target.test(infoPlist: .file(path: path)) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTContainsLintingIssue( got, @@ -165,7 +211,7 @@ final class TargetLinterTests: TuistUnitTestCase { let path = temporaryPath.appending(component: "App.entitlements") let target = Target.test(entitlements: .file(path: path)) - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) XCTContainsLintingIssue( got, @@ -178,23 +224,128 @@ final class TargetLinterTests: TuistUnitTestCase { let path = temporaryPath.appending(component: "Image.png") let element = ResourceFileElement.file(path: path) - let staticLibrary = Target.test(product: .staticLibrary, resources: [element]) - let dynamicLibrary = Target.test(product: .dynamicLibrary, resources: [element]) + let staticLibrary = Target.test(product: .staticLibrary, resources: .init([element])) + let dynamicLibrary = Target.test(product: .dynamicLibrary, resources: .init([element])) + + let staticLibraryResult = subject.lint( + target: staticLibrary, + options: .test() + ) + XCTDoesNotContainLintingIssue( + staticLibraryResult, + LintingIssue( + reason: "Target \(staticLibrary.name) cannot contain resources. For \(staticLibrary.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", + severity: .error + ) + ) + + let dynamicLibraryResult = subject.lint( + target: dynamicLibrary, + options: .test() + ) + XCTDoesNotContainLintingIssue( + dynamicLibraryResult, + LintingIssue( + reason: "Target \(dynamicLibrary.name) cannot contain resources. For \(dynamicLibrary.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", + severity: .error + ) + ) + } - let staticResult = subject.lint(target: staticLibrary) + func test_lint_when_library_has_resources_with_disable_bundle_accessors() throws { + let temporaryPath = try temporaryPath() + let path = temporaryPath.appending(component: "Image.png") + let element = ResourceFileElement.file(path: path) + + let staticLibrary = Target.test(product: .staticLibrary, resources: .init([element])) + let dynamicLibrary = Target.test(product: .dynamicLibrary, resources: .init([element])) + + let staticLibraryResult = subject.lint( + target: staticLibrary, + options: .test(disableBundleAccessors: true) + ) XCTContainsLintingIssue( - staticResult, + staticLibraryResult, + LintingIssue( + reason: "Target \(staticLibrary.name) cannot contain resources. For \(staticLibrary.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", + severity: .error + ) + ) + + let dynamicLibraryResult = subject.lint( + target: dynamicLibrary, + options: .test(disableBundleAccessors: true) + ) + XCTContainsLintingIssue( + dynamicLibraryResult, + LintingIssue( + reason: "Target \(dynamicLibrary.name) cannot contain resources. For \(dynamicLibrary.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", + severity: .error + ) + ) + } + + func test_lint_when_framework_has_resources() throws { + let temporaryPath = try temporaryPath() + let path = temporaryPath.appending(component: "Image.png") + let element = ResourceFileElement.file(path: path) + + let staticFramework = Target.test(product: .staticFramework, resources: .init([element])) + let dynamicFramework = Target.test(product: .framework, resources: .init([element])) + + let staticFrameworkResult = subject.lint( + target: staticFramework, + options: .test() + ) + XCTDoesNotContainLintingIssue( + staticFrameworkResult, LintingIssue( - reason: "Target \(staticLibrary.name) cannot contain resources. static library targets do not support resources", + reason: "Target \(staticFramework.name) cannot contain resources. For \(staticFramework.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", severity: .error ) ) - let dynamicResult = subject.lint(target: dynamicLibrary) + let dynamicFrameworkResult = subject.lint( + target: dynamicFramework, + options: .test() + ) + XCTDoesNotContainLintingIssue( + dynamicFrameworkResult, + LintingIssue( + reason: "Target \(dynamicFramework.name) cannot contain resources. For \(dynamicFramework.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", + severity: .error + ) + ) + } + + func test_lint_when_framework_has_resources_with_disable_bundle_accessors() throws { + let temporaryPath = try temporaryPath() + let path = temporaryPath.appending(component: "Image.png") + let element = ResourceFileElement.file(path: path) + + let staticFramework = Target.test(product: .staticFramework, resources: .init([element])) + let dynamicFramework = Target.test(product: .framework, resources: .init([element])) + + let staticFrameworkResult = subject.lint( + target: staticFramework, + options: .test(disableBundleAccessors: true) + ) XCTContainsLintingIssue( - dynamicResult, + staticFrameworkResult, + LintingIssue( + reason: "Target \(staticFramework.name) cannot contain resources. For \(staticFramework.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", + severity: .error + ) + ) + + let dynamicFrameworkResult = subject.lint( + target: dynamicFramework, + options: .test(disableBundleAccessors: true) + ) + XCTDoesNotContainLintingIssue( + dynamicFrameworkResult, LintingIssue( - reason: "Target \(dynamicLibrary.name) cannot contain resources. dynamic library targets do not support resources", + reason: "Target \(dynamicFramework.name) cannot contain resources. For \(dynamicFramework.product) targets to support resources, 'Bundle Accessors' feature should be enabled.", severity: .error ) ) @@ -208,11 +359,11 @@ final class TargetLinterTests: TuistUnitTestCase { sources: [ SourceFile(path: "/path/to/some/source.swift"), ], - resources: [] + resources: .init([]) ) // When - let result = subject.lint(target: bundle) + let result = subject.lint(target: bundle, options: .test()) // Then XCTContainsLintingIssue( @@ -230,11 +381,11 @@ final class TargetLinterTests: TuistUnitTestCase { destinations: .macOS, product: .bundle, sources: [], - resources: [] + resources: .init([]) ) // When - let result = subject.lint(target: bundle) + let result = subject.lint(target: bundle, options: .test()) // Then XCTAssertTrue(result.isEmpty) @@ -245,13 +396,15 @@ final class TargetLinterTests: TuistUnitTestCase { let bundle = Target.empty( destinations: .iOS, product: .bundle, - resources: [ - .file(path: "/path/to/some/asset.png"), - ] + resources: .init( + [ + .file(path: "/path/to/some/asset.png"), + ] + ) ) // When - let result = subject.lint(target: bundle) + let result = subject.lint(target: bundle, options: .test()) // Then XCTAssertTrue(result.isEmpty) @@ -264,7 +417,7 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test(platform: .macOS, deploymentTarget: .macOS(version)) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) // Then XCTDoesNotContainLintingIssue( @@ -274,6 +427,42 @@ final class TargetLinterTests: TuistUnitTestCase { } } + func test_lint_when_visionos_iPad_designed_deployment_target_is_valid() { + let targets = [ + Target( + name: "visionOS", + destinations: [.appleVision], + product: .app, + productName: "visionOS", + bundleId: "io.tuist.visionOS", + deploymentTargets: .visionOS("1.0"), + filesGroup: .group(name: "Project") + ), + Target( + name: "iPadVision", + destinations: [.iPhone, .iPad, .appleVisionWithiPadDesign], + product: .app, + productName: "visionOS", + bundleId: "io.tuist.visionOS", + deploymentTargets: .init(iOS: "16.0", visionOS: "1.0"), + filesGroup: .group(name: "Project") + ), + ] + + for target in targets { + let got = subject.lint(target: target, options: .test()) + + // Then + XCTDoesNotContainLintingIssue( + got, + LintingIssue( + reason: "Found an inconsistency between target destinations `[XcodeGraph.Destination.appleVisionWithiPadDesign, XcodeGraph.Destination.iPad, XcodeGraph.Destination.iPhone]` and deployment target `visionOS`", + severity: .error + ) + ) + } + } + func test_lint_when_deployment_target_version_is_invalid() { let validVersions = ["tuist", "tuist9.0.1", "1.0tuist", "10_0", "1_1_3"] for version in validVersions { @@ -281,7 +470,7 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test(platform: .macOS, deploymentTarget: .macOS(version)) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) // Then XCTContainsLintingIssue(got, LintingIssue(reason: "The version of deployment target is incorrect", severity: .error)) @@ -300,7 +489,7 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test(platform: combinations.0, deploymentTarget: combinations.1) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) let expectedPlatform = try XCTUnwrap(combinations.1.configuredVersions.first?.platform.caseValue) // Then @@ -322,7 +511,7 @@ final class TargetLinterTests: TuistUnitTestCase { ] // When - let got = invalidTargets.flatMap { subject.lint(target: $0) } + let got = invalidTargets.flatMap { subject.lint(target: $0, options: .test()) } // Then let expectedIssues: [LintingIssue] = [ @@ -345,7 +534,7 @@ final class TargetLinterTests: TuistUnitTestCase { let target = Target.test(dependencies: .init(repeating: testDependency, count: 2)) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) // Then XCTContainsLintingIssue(got, .init( @@ -363,7 +552,7 @@ final class TargetLinterTests: TuistUnitTestCase { ]) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) // Then XCTContainsLintingIssue(got, .init( @@ -383,7 +572,7 @@ final class TargetLinterTests: TuistUnitTestCase { ]) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) // Then XCTContainsLintingIssue(got, .init( @@ -406,7 +595,7 @@ final class TargetLinterTests: TuistUnitTestCase { ) // When - let got = subject.lint(target: target) + let got = subject.lint(target: target, options: .test()) // Then XCTContainsLintingIssue(got, .init( @@ -414,4 +603,23 @@ final class TargetLinterTests: TuistUnitTestCase { severity: .warning )) } + + func test_lint_when_target_has_invalid_on_demand_resources_tags() throws { + // Given + let target = Target.empty( + onDemandResourcesTags: .init( + initialInstall: ["tag1", "tag2"], + prefetchOrder: ["tag2", "tag3"] + ) + ) + + // When + let got = subject.lint(target: target, options: .test()) + + // Then + XCTContainsLintingIssue(got, .init( + reason: "Prefetched Order Tag \"tag2\" is already assigned to Initial Install Tags category for the target \(target.name) and will be ignored by Xcode", + severity: .warning + )) + } } diff --git a/Tests/TuistGeneratorTests/Linter/TargetScriptLinterTests.swift b/Tests/TuistGeneratorTests/Linter/TargetScriptLinterTests.swift index b50f0c0cfe0..d2c44b7980b 100644 --- a/Tests/TuistGeneratorTests/Linter/TargetScriptLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/TargetScriptLinterTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting diff --git a/Tests/TuistGeneratorTests/ProjectMappers/AutogeneratedSchemesProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/AutogeneratedSchemesProjectMapperTests.swift index d5a10d53ab8..d3877c90b45 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/AutogeneratedSchemesProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/AutogeneratedSchemesProjectMapperTests.swift @@ -1,8 +1,7 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XCTest @testable import TuistGenerator @@ -233,7 +232,7 @@ final class AutogeneratedSchemesProjectMapperTests: TuistUnitTestCase { ), ] - XCTAssertEqual(got.schemes, expected) + XCTAssertBetterEqual(got.schemes, expected) } func test_grouping_notGrouped() throws { @@ -1465,7 +1464,7 @@ final class AutogeneratedSchemesProjectMapperTests: TuistUnitTestCase { testRegion: String? = nil, testScreenCaptureFormat: ScreenCaptureFormat? = nil, runActionOptions: RunActionOptions? = nil - ) -> TuistGraph.Scheme { + ) -> XcodeGraph.Scheme { Scheme( name: name, shared: true, @@ -1490,7 +1489,8 @@ final class AutogeneratedSchemesProjectMapperTests: TuistUnitTestCase { RunAction.test( executable: TargetReference(projectPath: projectPath, name: $0), arguments: runActionArguments, - options: runActionOptions ?? .init() + options: runActionOptions ?? .init(), + metalOptions: nil ) } ) diff --git a/Tests/TuistGeneratorTests/ProjectMappers/DeleteDerivedDirectoryProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/DeleteDerivedDirectoryProjectMapperTests.swift index 16f2ce23db0..ef5c4a25d5e 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/DeleteDerivedDirectoryProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/DeleteDerivedDirectoryProjectMapperTests.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @testable import TuistGenerator @@ -24,14 +23,19 @@ public final class DeleteDerivedDirectoryProjectMapperTests: TuistUnitTestCase { func test_map_returns_sideEffectsToDeleteDerivedDirectories() throws { // Given - let projectA = Project.test(path: "/projectA") + let projectPath = try temporaryPath() + let derivedDirectory = projectPath.appending(component: Constants.DerivedDirectory.name) + let projectA = Project.test(path: projectPath) + try fileHandler.createFolder(derivedDirectory) + try fileHandler.createFolder(derivedDirectory.appending(component: "InfoPlists")) + try fileHandler.touch(derivedDirectory.appending(component: "TargetA.modulemap")) // When let (_, sideEffects) = try subject.map(project: projectA) // Then XCTAssertEqual(sideEffects, [ - .directory(.init(path: projectA.path.appending(component: Constants.DerivedDirectory.name), state: .absent)), + .directory(.init(path: derivedDirectory.appending(component: "InfoPlists"), state: .absent)), ]) } } diff --git a/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift index 413aef83dfb..32ad0a6bb2e 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift @@ -1,7 +1,8 @@ import Foundation -import TuistGraph +import TuistCore import TuistSupport import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistGenerator @@ -33,7 +34,13 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { let dynamicLibraryB: Target = .test( name: "DynamicLibraryB", product: .dynamicLibrary, - productName: "DynamicLibraryB" + productName: "DynamicLibraryB", + settings: .test( + configurations: [ + .debug: .test(), + .release: .test(), + ] + ) ) let externalFrameworkC: Target = .test( name: "ExternalFrameworkC", @@ -62,15 +69,6 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { isExternal: true ), ], - targets: [ - projectAPath: [ - "FrameworkA": frameworkA, - "DynamicLibraryB": dynamicLibraryB, - ], - externalProjectBPath: [ - "ExternalFrameworkC": externalFrameworkC, - ], - ], dependencies: [ .target(name: "FrameworkA", path: projectAPath): [ .target(name: "DynamicLibraryB", path: projectAPath), @@ -80,7 +78,7 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { ) // When - let got = try await subject.map(graph: graph) + let got = try await subject.map(graph: graph, environment: MapperEnvironment()) let copyScript = """ if [[ -d "$FILE" && ! -d "$DESTINATION_FILE" ]]; then ln -s "$FILE" "$DESTINATION_FILE" @@ -92,8 +90,10 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { """ // Then + let gotAProject = try XCTUnwrap(got.0.projects[projectAPath]) + let gotATargets = Array(gotAProject.targets.values).sorted() XCTAssertEqual( - got.0.projects[projectAPath]?.targets[0], + gotATargets[0], .test( name: "App", product: .app, @@ -102,7 +102,7 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { ] ) ) - let gotFrameworkA = try XCTUnwrap(got.0.projects[projectAPath]?.targets[1]) + let gotFrameworkA = try XCTUnwrap(gotATargets[2]) XCTAssertEqual( gotFrameworkA.name, "FrameworkA" @@ -163,8 +163,9 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { .project(target: "ExternalFrameworkC", path: externalProjectBPath), ] ) + XCTAssertEqual( - got.0.projects[projectAPath]?.targets[2], + gotATargets[1], .test( name: "DynamicLibraryB", product: .dynamicLibrary, @@ -172,6 +173,10 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { baseDebug: [ "BUILT_PRODUCTS_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", "TARGET_BUILD_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + ], + configurations: [ + .debug: .test(), + .release: .test(), ] ), scripts: [ @@ -196,8 +201,10 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { ] ) ) + let gotExternalBProject = try XCTUnwrap(got.0.projects[externalProjectBPath]) + let gotExternalBTargets = Array(gotExternalBProject.targets.values) XCTAssertEqual( - got.0.projects[externalProjectBPath]?.targets, + gotExternalBTargets, [ .test( name: "ExternalFrameworkC", @@ -236,4 +243,68 @@ final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { ] ) } + + func test_enabling_testing_search_paths() 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: [ + .project(target: "ExternalFrameworkB", path: externalProjectBPath), + ] + ) + + let externalFrameworkB: Target = .test( + name: "ExternalFrameworkB", + product: .staticFramework, + productName: "ExternalFrameworkB", + settings: .test(base: ["ENABLE_TESTING_SEARCH_PATHS": .string("YES")]) + ) + + let graph = Graph.test( + projects: [ + projectAPath: .test( + targets: [ + frameworkA, + ] + ), + externalProjectBPath: .test( + targets: [ + externalFrameworkB, + ], + isExternal: true + ), + ], + dependencies: [ + .target(name: "FrameworkA", path: projectAPath): [ + .target(name: "ExternalFrameworkB", path: externalProjectBPath), + ], + ] + ) + + // When + let got = try await subject.map(graph: graph, environment: MapperEnvironment()) + + // Then + let gotAProject = try XCTUnwrap(got.0.projects[projectAPath]) + let gotATargets = Array(gotAProject.targets.values).sorted() + let gotFrameworkA = try XCTUnwrap(gotATargets[0]) + XCTAssertEqual( + gotFrameworkA.name, + "FrameworkA" + ) + XCTAssertEqual( + gotFrameworkA.product, + .framework + ) + + // ENABLE_TESTING_SEARCH_PATHS is propagated from ExternalFrameworkB + XCTAssertEqual( + gotFrameworkA.settings?.baseDebug["ENABLE_TESTING_SEARCH_PATHS"], + .string("YES") + ) + } } diff --git a/Tests/TuistGeneratorTests/ProjectMappers/GenerateInfoPlistProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/GenerateInfoPlistProjectMapperTests.swift index 78f483afda6..72113aa6624 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/GenerateInfoPlistProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/GenerateInfoPlistProjectMapperTests.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @testable import TuistGenerator @@ -95,7 +94,7 @@ public final class GenerateInfoPlistProjectMapperTests: TuistUnitTestCase { file: StaticString = #file, line: UInt = #line ) { - XCTAssertNotNil(project.targets.first(where: { (target: Target) in + XCTAssertNotNil(project.targets.values.first(where: { (target: Target) in target.infoPlist?.path == project.path .appending(component: Constants.DerivedDirectory.name) .appending(component: Constants.DerivedDirectory.infoPlists) diff --git a/Tests/TuistGeneratorTests/ProjectMappers/IDETemplateMacrosMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/IDETemplateMacrosMapperTests.swift index 4a6fae33b0a..a1fa6eafb62 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/IDETemplateMacrosMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/IDETemplateMacrosMapperTests.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistGenerator -import TuistGraph import TuistSupportTesting +import XcodeGraph import XCTest final class IDETemplateMacrosMapperTests: XCTestCase { diff --git a/Tests/TuistGeneratorTests/ProjectMappers/LastUpgradeVersionWorkspaceMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/LastUpgradeVersionWorkspaceMapperTests.swift index 0d4d5ab9bd8..e59d4493f80 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/LastUpgradeVersionWorkspaceMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/LastUpgradeVersionWorkspaceMapperTests.swift @@ -1,6 +1,6 @@ import TuistCore import TuistGenerator -import TuistGraph +import XcodeGraph import XCTest @testable import TuistSupportTesting diff --git a/Tests/TuistGeneratorTests/ProjectMappers/ModuleMapMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/ModuleMapMapperTests.swift index a2e62330f33..f6a9121e2fc 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/ModuleMapMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/ModuleMapMapperTests.swift @@ -1,6 +1,6 @@ import TuistCore import TuistGenerator -import TuistGraph +import XcodeGraph import XCTest @testable import TuistSupportTesting @@ -47,6 +47,7 @@ final class ModuleMapMapperTests: TuistUnitTestCase { name: "B1", settings: .test(base: [ "MODULEMAP_FILE": .string(projectBPath.appending(components: "B1", "B1.module").pathString), + "HEADER_SEARCH_PATHS": .array(["$(SRCROOT)/B1/include"]), ]), dependencies: [ .target(name: "B2"), @@ -56,6 +57,7 @@ final class ModuleMapMapperTests: TuistUnitTestCase { name: "B2", settings: .test(base: [ "MODULEMAP_FILE": .string(projectBPath.appending(components: "B2", "B2.module").pathString), + "HEADER_SEARCH_PATHS": .array(["$(SRCROOT)/B2/include"]), ]) ) let projectB = Project.test( @@ -68,14 +70,23 @@ final class ModuleMapMapperTests: TuistUnitTestCase { ) // When - let (gotWorkspaceWithProjects, gotSideEffects) = try subject.map( - workspace: WorkspaceWithProjects( + let (gotGraph, gotSideEffects, _) = try subject.map( + graph: .test( workspace: workspace, projects: [ - projectA, - projectB, + projectAPath: projectA, + projectBPath: projectB, + ], + dependencies: [ + .target(name: targetA.name, path: projectAPath): [ + .target(name: targetB1.name, path: projectBPath), + ], + .target(name: targetB1.name, path: projectBPath): [ + .target(name: targetB2.name, path: projectBPath), + ], ] - ) + ), + environment: MapperEnvironment() ) // Then @@ -94,6 +105,7 @@ final class ModuleMapMapperTests: TuistUnitTestCase { "-Xcc", "-fmodule-map-file=$(SRCROOT)/../B/B2/B2.module", ]), + "HEADER_SEARCH_PATHS": .array(["$(inherited)", "$(SRCROOT)/../B/B1/include", "$(SRCROOT)/../B/B2/include"]), ]), dependencies: [ .project(target: "B1", path: projectBPath), @@ -112,13 +124,19 @@ final class ModuleMapMapperTests: TuistUnitTestCase { settings: .test(base: [ "OTHER_CFLAGS": .array(["$(inherited)", "-fmodule-map-file=$(SRCROOT)/B2/B2.module"]), "OTHER_SWIFT_FLAGS": .array(["$(inherited)", "-Xcc", "-fmodule-map-file=$(SRCROOT)/B2/B2.module"]), + "HEADER_SEARCH_PATHS": .array(["$(SRCROOT)/B1/include", "$(SRCROOT)/B2/include"]), ]), dependencies: [ .target(name: "B2"), ] ) let mappedTargetB2 = Target.test( - name: "B2" + name: "B2", + settings: .test( + base: [ + "HEADER_SEARCH_PATHS": .array(["$(SRCROOT)/B2/include"]), + ] + ) ) let mappedProjectB = Project.test( path: projectBPath, @@ -129,15 +147,23 @@ final class ModuleMapMapperTests: TuistUnitTestCase { ] ) - XCTAssertEqual( - gotWorkspaceWithProjects, - WorkspaceWithProjects( + XCTAssertBetterEqual( + Graph.test( workspace: workspace, projects: [ - mappedProjectA, - mappedProjectB, + projectAPath: mappedProjectA, + projectBPath: mappedProjectB, + ], + dependencies: [ + .target(name: targetA.name, path: projectAPath): [ + .target(name: targetB1.name, path: projectBPath), + ], + .target(name: targetB1.name, path: projectBPath): [ + .target(name: targetB2.name, path: projectBPath), + ], ] - ) + ), + gotGraph ) XCTAssertEqual(gotSideEffects, []) } @@ -167,6 +193,7 @@ final class ModuleMapMapperTests: TuistUnitTestCase { name: "B", settings: .test(base: [ "MODULEMAP_FILE": .string(projectBPath.appending(components: "B", "B.module").pathString), + "HEADER_SEARCH_PATHS": .array(["$(SRCROOT)/B/include"]), ]) ) let projectB = Project.test( @@ -178,14 +205,20 @@ final class ModuleMapMapperTests: TuistUnitTestCase { ) // When - let (gotWorkspaceWithProjects, gotSideEffects) = try subject.map( - workspace: WorkspaceWithProjects( + let (gotGraph, gotSideEffects, _) = try subject.map( + graph: .test( workspace: workspace, projects: [ - projectA, - projectB, + projectAPath: projectA, + projectBPath: projectB, + ], + dependencies: [ + .target(name: targetA.name, path: projectAPath): [ + .target(name: targetB.name, path: projectBPath), + ], ] - ) + ), + environment: MapperEnvironment() ) // Then @@ -202,6 +235,7 @@ final class ModuleMapMapperTests: TuistUnitTestCase { "-Xcc", "-fmodule-map-file=$(SRCROOT)/../B/B/B.module", ]), + "HEADER_SEARCH_PATHS": .array(["$(inherited)", "$(SRCROOT)/../B/B/include"]), ], configurations: [:], defaultSettings: .recommended @@ -220,7 +254,11 @@ final class ModuleMapMapperTests: TuistUnitTestCase { let mappedTargetB = Target.test( name: "B", - settings: .test(base: [:]) + settings: .test( + base: [ + "HEADER_SEARCH_PATHS": .array(["$(SRCROOT)/B/include"]), + ] + ) ) let mappedProjectB = Project.test( path: projectBPath, @@ -230,15 +268,20 @@ final class ModuleMapMapperTests: TuistUnitTestCase { ] ) - XCTAssertEqual( - gotWorkspaceWithProjects, - WorkspaceWithProjects( + XCTAssertBetterEqual( + Graph.test( workspace: workspace, projects: [ - mappedProjectA, - mappedProjectB, + projectAPath: mappedProjectA, + projectBPath: mappedProjectB, + ], + dependencies: [ + .target(name: projectA.name, path: projectAPath): [ + .target(name: projectB.name, path: projectBPath), + ], ] - ) + ), + gotGraph ) XCTAssertEqual(gotSideEffects, []) } diff --git a/Tests/TuistGeneratorTests/ProjectMappers/ResourcesProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/ResourcesProjectMapperTests.swift index b2a0f7b79ca..ca4be38bcd2 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/ResourcesProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/ResourcesProjectMapperTests.swift @@ -1,14 +1,13 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistSupport +import TuistSupportTesting +import XcodeGraph import XCTest -@testable import TuistCoreTesting @testable import TuistGenerator -@testable import TuistSupport -@testable import TuistSupportTesting // Bundle name is irrelevant if the target supports resources. private let irrelevantBundleName = "" @@ -16,12 +15,16 @@ private let irrelevantBundleName = "" final class ResourcesProjectMapperTests: TuistUnitTestCase { var project: Project! var subject: ResourcesProjectMapper! - var contentHasher: MockContentHasher! + var contentHasher: MockContentHashing! override func setUp() { super.setUp() - contentHasher = MockContentHasher() + contentHasher = .init() subject = ResourcesProjectMapper(contentHasher: contentHasher) + + given(contentHasher) + .hash(Parameter.any) + .willProduce { String(data: $0, encoding: .utf8)! } } override func tearDown() { @@ -33,7 +36,7 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { func test_map_when_a_target_that_has_resources_and_doesnt_supports_them() throws { // Given let resources: [ResourceFileElement] = [.file(path: "/image.png")] - let target = Target.test(product: .staticLibrary, resources: resources) + let target = Target.test(product: .staticLibrary, sources: ["/Absolute/File.swift"], resources: .init(resources)) project = Project.test(targets: [target]) // Got @@ -51,42 +54,56 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { .appending(component: Constants.DerivedDirectory.sources) .appending(component: "TuistBundle+\(target.name).swift") let expectedContents = ResourcesProjectMapper - .fileContent(targetName: target.name, bundleName: "\(project.name)_\(target.name)", target: target) + .fileContent(targetName: target.name, bundleName: "\(project.name)_\(target.name)", target: target, in: project) XCTAssertEqual(file.path, expectedPath) XCTAssertEqual(file.contents, expectedContents.data(using: .utf8)) // Then: Targets XCTAssertEqual(gotProject.targets.count, 2) - let gotTarget = try XCTUnwrap(gotProject.targets.first) + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().last) XCTAssertEqual(gotTarget.name, target.name) XCTAssertEqual(gotTarget.product, target.product) - XCTAssertEqual(gotTarget.resources.count, 0) - XCTAssertEqual(gotTarget.sources.count, 1) - XCTAssertEqual(gotTarget.sources.first?.path, expectedPath) - XCTAssertNotNil(gotTarget.sources.first?.contentHash) + XCTAssertEqual(gotTarget.resources.resources.count, 0) + XCTAssertEqual(gotTarget.sources.count, 2) + XCTAssertEqual(gotTarget.sources.last?.path, expectedPath) + XCTAssertNotNil(gotTarget.sources.last?.contentHash) XCTAssertEqual(gotTarget.dependencies.count, 1) XCTAssertEqual( gotTarget.dependencies.first, TargetDependency.target(name: "\(project.name)_\(target.name)", condition: .when([.ios])) ) - let resourcesTarget = try XCTUnwrap(gotProject.targets.last) + let resourcesTarget = try XCTUnwrap(gotProject.targets.values.sorted().first) XCTAssertEqual(resourcesTarget.name, "\(project.name)_\(target.name)") XCTAssertEqual(resourcesTarget.product, .bundle) XCTAssertEqual(resourcesTarget.destinations, target.destinations) XCTAssertEqual(resourcesTarget.bundleId, "\(target.bundleId).resources") XCTAssertEqual(resourcesTarget.deploymentTargets, target.deploymentTargets) XCTAssertEqual(resourcesTarget.filesGroup, target.filesGroup) - XCTAssertEqual(resourcesTarget.resources, resources) + XCTAssertEqual(resourcesTarget.resources.resources, resources) XCTAssertEqual(resourcesTarget.settings?.base, [ "CODE_SIGNING_ALLOWED": "NO", ]) } + func test_map_when_an_external_objc_target_that_has_resources_and_supports_them() throws { + // Given + let resources: [ResourceFileElement] = [.file(path: "/image.png")] + let target = Target.test(product: .framework, sources: ["/Absolute/File.m"], resources: .init(resources)) + project = Project.test(targets: [target], isExternal: true) + + // Got + let (gotProject, gotSideEffects) = try subject.map(project: project) + + // Then + XCTAssertEmpty(gotSideEffects) + XCTAssertEqual(project, gotProject) + } + func testMap_whenDisableBundleAccessorsIsTrue_doesNotGenerateAccessors() throws { // Given let resources: [ResourceFileElement] = [.file(path: "/image.png")] - let target = Target.test(product: .staticLibrary, resources: resources) + let target = Target.test(product: .staticLibrary, resources: .init(resources)) project = Project.test( options: .test( disableBundleAccessors: true @@ -102,12 +119,48 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { XCTAssertEqual(gotSideEffects, []) } + func test_map_when_no_sources() throws { + // Given + let resources: [ResourceFileElement] = [.file(path: "/image.png")] + let target = Target.test(product: .staticLibrary, sources: [], resources: .init(resources)) + project = Project.test( + targets: [target] + ) + + // Got + let (gotProject, gotSideEffects) = try subject.map(project: project) + + // Then: Side effects + XCTAssertEqual(gotSideEffects, []) + XCTAssertEqual(gotProject.targets.count, 2) + + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().last) + XCTAssertEqual(gotTarget.name, target.name) + XCTAssertEqual(gotTarget.product, target.product) + XCTAssertEqual(gotTarget.resources.resources.count, 0) + XCTAssertEqual(gotTarget.sources.count, 0) + XCTAssertEqual(gotTarget.dependencies.count, 1) + XCTAssertEqual( + gotTarget.dependencies.first, + TargetDependency.target(name: "\(project.name)_\(target.name)", condition: .when([.ios])) + ) + + let resourcesTarget = try XCTUnwrap(gotProject.targets.values.sorted().first) + XCTAssertEqual(resourcesTarget.name, "\(project.name)_\(target.name)") + XCTAssertEqual(resourcesTarget.product, .bundle) + XCTAssertEqual(resourcesTarget.destinations, target.destinations) + XCTAssertEqual(resourcesTarget.bundleId, "\(target.bundleId).resources") + XCTAssertEqual(resourcesTarget.deploymentTargets, target.deploymentTargets) + XCTAssertEqual(resourcesTarget.filesGroup, target.filesGroup) + XCTAssertEqual(resourcesTarget.resources.resources, resources) + } + func test_map_when_a_target_that_has_core_data_models_and_doesnt_supports_them() throws { // Given let coreDataModels: [CoreDataModel] = [CoreDataModel(path: "/data.xcdatamodeld", versions: ["/data.xcdatamodeld"], currentVersion: "1")] - let target = Target.test(product: .staticLibrary, coreDataModels: coreDataModels) + let target = Target.test(product: .staticLibrary, sources: ["/Absolute/File.swift"], coreDataModels: coreDataModels) project = Project.test(targets: [target]) // Got @@ -125,26 +178,26 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { .appending(component: Constants.DerivedDirectory.sources) .appending(component: "TuistBundle+\(target.name).swift") let expectedContents = ResourcesProjectMapper - .fileContent(targetName: target.name, bundleName: "\(project.name)_\(target.name)", target: target) + .fileContent(targetName: target.name, bundleName: "\(project.name)_\(target.name)", target: target, in: project) XCTAssertEqual(file.path, expectedPath) XCTAssertEqual(file.contents, expectedContents.data(using: .utf8)) // Then: Targets XCTAssertEqual(gotProject.targets.count, 2) - let gotTarget = try XCTUnwrap(gotProject.targets.first) + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().last) XCTAssertEqual(gotTarget.name, target.name) XCTAssertEqual(gotTarget.product, target.product) - XCTAssertEqual(gotTarget.resources.count, 0) - XCTAssertEqual(gotTarget.sources.count, 1) - XCTAssertEqual(gotTarget.sources.first?.path, expectedPath) - XCTAssertNotNil(gotTarget.sources.first?.contentHash) + XCTAssertEqual(gotTarget.resources.resources.count, 0) + XCTAssertEqual(gotTarget.sources.count, 2) + XCTAssertEqual(gotTarget.sources.last?.path, expectedPath) + XCTAssertNotNil(gotTarget.sources.last?.contentHash) XCTAssertEqual(gotTarget.dependencies.count, 1) XCTAssertEqual( gotTarget.dependencies.first, TargetDependency.target(name: "\(project.name)_\(target.name)", condition: .when([.ios])) ) - let resourcesTarget = try XCTUnwrap(gotProject.targets.last) + let resourcesTarget = try XCTUnwrap(gotProject.targets.values.sorted().first) XCTAssertEqual(resourcesTarget.name, "\(project.name)_\(target.name)") XCTAssertEqual(resourcesTarget.product, .bundle) XCTAssertEqual(resourcesTarget.destinations, target.destinations) @@ -157,7 +210,7 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { func test_map_when_a_target_that_has_resources_and_supports_them() throws { // Given let resources: [ResourceFileElement] = [.file(path: "/image.png")] - let target = Target.test(product: .framework, resources: resources) + let target = Target.test(product: .framework, sources: ["/Absolute/File.swift"], resources: .init(resources)) project = Project.test(targets: [target]) // Got @@ -175,19 +228,19 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { .appending(component: Constants.DerivedDirectory.sources) .appending(component: "TuistBundle+\(target.name).swift") let expectedContents = ResourcesProjectMapper - .fileContent(targetName: target.name, bundleName: irrelevantBundleName, target: target) + .fileContent(targetName: target.name, bundleName: irrelevantBundleName, target: target, in: project) XCTAssertEqual(file.path, expectedPath) XCTAssertEqual(file.contents, expectedContents.data(using: .utf8)) // Then: Targets XCTAssertEqual(gotProject.targets.count, 1) - let gotTarget = try XCTUnwrap(gotProject.targets.first) + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().first) XCTAssertEqual(gotTarget.name, target.name) XCTAssertEqual(gotTarget.product, target.product) - XCTAssertEqual(gotTarget.resources, resources) - XCTAssertEqual(gotTarget.sources.count, 1) - XCTAssertEqual(gotTarget.sources.first?.path, expectedPath) - XCTAssertNotNil(gotTarget.sources.first?.contentHash) + XCTAssertEqual(gotTarget.resources.resources, resources) + XCTAssertEqual(gotTarget.sources.count, 2) + XCTAssertEqual(gotTarget.sources.last?.path, expectedPath) + XCTAssertNotNil(gotTarget.sources.last?.contentHash) XCTAssertEqual(gotTarget.dependencies.count, 0) } @@ -195,7 +248,7 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { // Given let coreDataModels: [CoreDataModel] = [CoreDataModel(path: "/data.xcdatamodeld", versions: ["/data.xcdatamodeld"], currentVersion: "1")] - let target = Target.test(product: .framework, coreDataModels: coreDataModels) + let target = Target.test(product: .framework, sources: ["/Absolute/File.swift"], coreDataModels: coreDataModels) project = Project.test(targets: [target]) // Got @@ -213,33 +266,33 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { .appending(component: Constants.DerivedDirectory.sources) .appending(component: "TuistBundle+\(target.name).swift") let expectedContents = ResourcesProjectMapper - .fileContent(targetName: target.name, bundleName: irrelevantBundleName, target: target) + .fileContent(targetName: target.name, bundleName: irrelevantBundleName, target: target, in: project) XCTAssertEqual(file.path, expectedPath) XCTAssertEqual(file.contents, expectedContents.data(using: .utf8)) // Then: Targets XCTAssertEqual(gotProject.targets.count, 1) - let gotTarget = try XCTUnwrap(gotProject.targets.first) + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().first) XCTAssertEqual(gotTarget.name, target.name) XCTAssertEqual(gotTarget.product, target.product) XCTAssertEqual(gotTarget.coreDataModels, coreDataModels) - XCTAssertEqual(gotTarget.sources.count, 1) - XCTAssertEqual(gotTarget.sources.first?.path, expectedPath) - XCTAssertNotNil(gotTarget.sources.first?.contentHash) + XCTAssertEqual(gotTarget.sources.count, 2) + XCTAssertEqual(gotTarget.sources.last?.path, expectedPath) + XCTAssertNotNil(gotTarget.sources.last?.contentHash) XCTAssertEqual(gotTarget.dependencies.count, 0) } func test_map_when_a_target_has_no_resources() throws { // Given let resources: [ResourceFileElement] = [] - let target = Target.test(product: .framework, resources: resources) + let target = Target.test(product: .framework, resources: .init(resources)) project = Project.test(targets: [target]) // Got let (gotProject, gotSideEffects) = try subject.map(project: project) // Then - XCTAssertEqual(gotProject.targets, [target]) + XCTAssertEqual(Array(gotProject.targets.values), [target]) XCTAssertEmpty(gotSideEffects) } @@ -249,21 +302,26 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { .file(path: "/Some/ResourceA"), .file(path: "/Some/ResourceB"), ] - let target = Target.test(product: .bundle, resources: resources) + let target = Target.test(product: .bundle, resources: .init(resources)) project = Project.test(targets: [target]) // Got let (gotProject, gotSideEffects) = try subject.map(project: project) // Then - XCTAssertEqual(gotProject.targets, [target]) + XCTAssertEqual(Array(gotProject.targets.values), [target]) XCTAssertEmpty(gotSideEffects) } func test_map_when_a_target_that_has_name_with_hyphen() throws { // Given let resources: [ResourceFileElement] = [.file(path: "/image.png")] - let target = Target.test(name: "test-tuist", product: .staticLibrary, resources: resources) + let target = Target.test( + name: "test-tuist", + product: .staticLibrary, + sources: ["/Absolute/File.swift"], + resources: .init(resources) + ) project = Project.test(targets: [target]) // Got @@ -281,8 +339,206 @@ final class ResourcesProjectMapperTests: TuistUnitTestCase { .appending(component: Constants.DerivedDirectory.sources) .appending(component: "TuistBundle+\(target.name.camelized.uppercasingFirst).swift") let expectedContents = ResourcesProjectMapper - .fileContent(targetName: target.name, bundleName: "\(project.name)_test_tuist", target: target) + .fileContent(targetName: target.name, bundleName: "\(project.name)_test_tuist", target: target, in: project) + XCTAssertEqual(file.path, expectedPath) + XCTAssertEqual(file.contents, expectedContents.data(using: .utf8)) + } + + func test_map_when_a_project_is_external_target_has_swift_source_but_no_resource_files() throws { + // Given + let sources: [SourceFile] = ["/ViewController.swift"] + let resources: [ResourceFileElement] = [] + let target = Target.test(product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), targets: [target], isExternal: true) + + // Got + let (gotProject, gotSideEffects) = try subject.map(project: project) + + // Then + XCTAssertEqual(Array(gotProject.targets.values), [target]) + XCTAssertEmpty(gotSideEffects) + } + + func test_map_when_a_project_is_not_external_target_has_swift_source_but_no_resource_files() throws { + // Given + let sources: [SourceFile] = ["/ViewController.swift"] + let resources: [ResourceFileElement] = [] + let target = Target.test(product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), targets: [target], isExternal: false) + + // Got + let (gotProject, gotSideEffects) = try subject.map(project: project) + + // Then + XCTAssertEqual(Array(gotProject.targets.values), [target]) + XCTAssertEmpty(gotSideEffects) + } + + func test_map_when_a_project_is_external_target_has_swift_source_and_resource_files() throws { + // Given + let sources: [SourceFile] = ["/ViewController.swift"] + let resources: [ResourceFileElement] = [.file(path: "/AbsolutePath/Project/Resources/image.png")] + let target = Target.test(product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), targets: [target], isExternal: true) + + // Got + let (_, gotSideEffects) = try subject.map(project: project) + + // Then: Side effects + try verify(gotSideEffects, for: target, in: project, by: "\(project.name)_\(target.name)") + } + + func test_map_when_a_project_is_not_external_target_has_swift_source_and_resource_files() throws { + // Given + let sources: [SourceFile] = ["/ViewController.swift"] + let resources: [ResourceFileElement] = [.file(path: "/AbsolutePath/Project/Resources/image.png")] + let target = Target.test(product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), targets: [target], isExternal: false) + + // Got + let (_, gotSideEffects) = try subject.map(project: project) + + // Then: Side effects + try verify(gotSideEffects, for: target, in: project, by: "\(project.name)_\(target.name)") + } + + func test_map_when_a_project_is_external_target_has_objc_source_files() throws { + // Given + let sources: [SourceFile] = ["/ViewController.m"] + let resources: [ResourceFileElement] = [.file(path: "/AbsolutePath/Project/Resources/image.png")] + let target = Target.test(product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), targets: [target], isExternal: true) + + // Got + let (gotProject, gotSideEffects) = try subject.map(project: project) + + // Then + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().last) + verifyObjcBundleAccessor( + for: target, + gotTarget: gotTarget, + gotSideEffects: gotSideEffects + ) + } + + func test_map_when_a_project_is_not_external_target_has_objc_source_files() throws { + // Given + let sources: [SourceFile] = ["/ViewController.m"] + let resources: [ResourceFileElement] = [.file(path: "/AbsolutePath/Project/Resources/image.png")] + let target = Target.test(product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), targets: [target], isExternal: false) + + // Got + let (gotProject, gotSideEffects) = try subject.map(project: project) + + // Then + let gotTarget = try XCTUnwrap(gotProject.targets.values.sorted().last) + XCTAssertEqual( + gotTarget.settings?.base["GCC_PREFIX_HEADER"], + nil + ) + XCTAssertEqual(gotTarget.sources.count, 1) + XCTAssertEqual(gotSideEffects.count, 0) + } + + func test_map_when_project_name_has_dashes_in_it_bundle_name_include_dash_for_project_name_and_underscore_for_target_name( + ) throws { + // Given + let projectName = "sdk-with-dash" + let targetName = "target-with-dash" + let expectedBundleName = "sdk-with-dash_target_with_dash" + let sources: [SourceFile] = ["/ViewController.m", "/ViewController2.swift"] + let resources: [ResourceFileElement] = [.file(path: "/AbsolutePath/Project/Resources/image.png")] + let target = Target.test(name: targetName, product: .staticLibrary, sources: sources, resources: .init(resources)) + project = Project.test(path: try AbsolutePath(validating: "/AbsolutePath/Project"), name: projectName, targets: [target]) + + // Got + let (gotProject, _) = try subject.map(project: project) + let bundleTarget = try XCTUnwrap(gotProject.targets.values.sorted().first(where: { $0.product == .bundle })) + + // Then + XCTAssertEqual(expectedBundleName, bundleTarget.name) + XCTAssertEqual(expectedBundleName, bundleTarget.productName) + XCTAssertEqual(2, gotProject.targets.count) // One code target, one bundle target + } + + // MARK: - Verifiers + + private func verify( + _ gotSideEffects: [SideEffectDescriptor], + for target: Target, + in project: Project, + by bundleName: String + ) throws { + XCTAssertEqual(gotSideEffects.count, 1) + let sideEffect = try XCTUnwrap(gotSideEffects.first) + guard case let SideEffectDescriptor.file(file) = sideEffect else { + XCTFail("Expected file descriptor") + return + } + let expectedPath = expectedTuistBundleSwiftFilePath(for: target) + let expectedContents = ResourcesProjectMapper + .fileContent( + targetName: target.name, + bundleName: bundleName, + target: target, + in: project + ) XCTAssertEqual(file.path, expectedPath) XCTAssertEqual(file.contents, expectedContents.data(using: .utf8)) } + + private func verifyObjcBundleAccessor( + for target: Target, + gotTarget: Target, + gotSideEffects: [SideEffectDescriptor] + ) { + XCTAssertEqual( + gotTarget.settings?.base["GCC_PREFIX_HEADER"], + .string( + "$(SRCROOT)/\(Constants.DerivedDirectory.name)/\(Constants.DerivedDirectory.sources)/TuistBundle+\(target.name).h" + ) + ) + XCTAssertEqual(gotTarget.sources.count, 2) + XCTAssertEqual(gotSideEffects.count, 2) + let generatedFiles = gotSideEffects.compactMap { + if case let .file(file) = $0 { + return file + } else { + return nil + } + } + + let expectedBasePath = project.derivedDirectoryPath(for: target) + .appending(component: Constants.DerivedDirectory.sources) + XCTAssertEqual( + generatedFiles, + [ + FileDescriptor( + path: expectedBasePath.appending(component: "TuistBundle+\(target.name).h"), + contents: ResourcesProjectMapper + .objcHeaderFileContent(targetName: target.name) + .data(using: .utf8) + ), + FileDescriptor( + path: expectedBasePath.appending(component: "TuistBundle+\(target.name).m"), + contents: ResourcesProjectMapper + .objcImplementationFileContent( + targetName: target.name, + bundleName: "\(project.name)_\(target.name)" + ) + .data(using: .utf8) + ), + ] + ) + } + + // MARK: - Helpers + + private func expectedTuistBundleSwiftFilePath(for target: Target) -> AbsolutePath { + project.path + .appending(component: Constants.DerivedDirectory.name) + .appending(component: Constants.DerivedDirectory.sources) + .appending(component: "TuistBundle+\(target.name.camelized.uppercasingFirst).swift") + } } diff --git a/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift index ee5fbd3e050..cd5c5250f08 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @@ -58,6 +57,11 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { let otfFont = targetAPath.appending(component: "otfFont.otf") let ttcFont = targetAPath.appending(component: "ttcFont.ttc") let lottieFile = targetAPath.appending(component: "LottieAnimation.lottie") + let coreDataModelFolder = targetAPath.appending(component: "CoreDataModel.xcdatamodeld") + let coreDataModelVersionFile = targetAPath.appending( + components: "CoreDataModel.xcdatamodeld", + "CoreDataModel.xcdatamodel" + ) try fileHandler.createFolder(aAssets) try fileHandler.touch(aAsset) @@ -65,6 +69,7 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { try fileHandler.touch(frenchStringsDict) try fileHandler.touch(englishStrings) try fileHandler.touch(englishStringsDict) + try fileHandler.touch(coreDataModelVersionFile) try fileHandler.write("a", path: frenchStrings, atomically: true) try fileHandler.write("a", path: frenchStringsDict, atomically: true) try fileHandler.write("a", path: englishStrings, atomically: true) @@ -79,21 +84,36 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { try fileHandler.write("a", path: lottieFile, atomically: true) let stringsTemplatePath = projectPath.appending(component: "Strings.stencil") try fileHandler.write("strings template", path: stringsTemplatePath, atomically: true) + let coreDataTemplatePath = projectPath.appending(component: "CoreData.stencil") + try fileHandler.write("core data template", path: coreDataTemplatePath, atomically: true) + try fileHandler.createFolder(coreDataModelFolder) + try fileHandler.write("a", path: coreDataModelVersionFile, atomically: true) let targetA = Target.test( name: "TargetA", - resources: [ - .folderReference(path: aAssets), - .file(path: frenchStrings), - .file(path: frenchStringsDict), - .file(path: englishStrings), - .file(path: englishStringsDict), - .file(path: emptyPlist), - .file(path: environmentPlist), - .file(path: ttfFont), - .file(path: otfFont), - .file(path: ttcFont), - .file(path: lottieFile), + resources: .init( + [ + .folderReference(path: aAssets), + .file(path: frenchStrings), + .file(path: frenchStringsDict), + .file(path: englishStrings), + .file(path: englishStringsDict), + .file(path: emptyPlist), + .file(path: environmentPlist), + .file(path: ttfFont), + .file(path: otfFont), + .file(path: ttcFont), + .file(path: lottieFile), + ] + ), + coreDataModels: [ + CoreDataModel( + path: coreDataModelFolder, + versions: [ + coreDataModelVersionFile, + ], + currentVersion: "CoreDataModel" + ), ] ) @@ -153,6 +173,17 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { extensions: ["lottie"], template: .file(lottieTemplatePath) ), + .init( + parser: .coreData, + parserOptions: [ + "stringValue": "test", + "intValue": 999, + "boolValue": true, + "doubleValue": 1.0, + ], + extensions: ["xcdatamodeld"], + template: .file(coreDataTemplatePath) + ), ] let project = Project.test( @@ -205,6 +236,12 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { contents: "TargetA/LottieAnimation.lottie".data(using: .utf8) ) ), + .file( + FileDescriptor( + path: derivedSourcesPath.appending(component: "TuistCoreData+TargetA.swift"), + contents: "TargetA/CoreDataModel.xcdatamodeld".data(using: .utf8) + ) + ), ] ) XCTAssertEqual( @@ -248,8 +285,15 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { compilerFlags: nil, contentHash: try contentHasher.hash("TargetA/LottieAnimation.lottie".data(using: .utf8)!) ), + SourceFile( + path: derivedSourcesPath + .appending(component: "TuistCoreData+TargetA.swift"), + compilerFlags: nil, + contentHash: try contentHasher.hash("TargetA/CoreDataModel.xcdatamodeld".data(using: .utf8)!) + ), ], - resources: targetA.resources + resources: targetA.resources, + coreDataModels: targetA.coreDataModels ), ], resourceSynthesizers: resourceSynthesizers @@ -263,6 +307,7 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { SynthesizedResourceInterfaceTemplates.plistsTemplate, SynthesizedResourceInterfaceTemplates.fontsTemplate, "lottie template", + "core data template", ] ) [ @@ -271,6 +316,7 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { ResourceSynthesizer.Parser.plists, ResourceSynthesizer.Parser.fonts, ResourceSynthesizer.Parser.json, + ResourceSynthesizer.Parser.coreData, ].forEach { parser in XCTAssertEqual( parserOptionsStrings[parser], @@ -307,6 +353,11 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { let otfFont = targetAPath.appending(component: "otfFont.otf") let ttcFont = targetAPath.appending(component: "ttcFont.ttc") let lottieFile = targetAPath.appending(component: "LottieAnimation.lottie") + let coreDataModelFolder = targetAPath.appending(component: "CoreDataModel.xcdatamodeld") + let coreDataModelVersionFile = targetAPath.appending( + components: "CoreDataModel.xcdatamodeld", + "CoreDataModel.xcdatamodel" + ) try fileHandler.createFolder(aAssets) try fileHandler.touch(aAsset) @@ -328,22 +379,28 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { try fileHandler.write("a", path: lottieFile, atomically: true) let stringsTemplatePath = projectPath.appending(component: "Strings.stencil") try fileHandler.write("strings template", path: stringsTemplatePath, atomically: true) + let coreDataTemplatePath = projectPath.appending(component: "CoreData.stencil") + try fileHandler.write("core data template", path: coreDataTemplatePath, atomically: true) + try fileHandler.createFolder(coreDataModelFolder) + try fileHandler.write("a", path: coreDataModelVersionFile, atomically: true) let targetA = Target.test( name: "TargetA", - resources: [ - .folderReference(path: aAssets), - .file(path: frenchStrings), - .file(path: frenchStringsDict), - .file(path: englishStrings), - .file(path: englishStringsDict), - .file(path: emptyPlist), - .file(path: environmentPlist), - .file(path: ttfFont), - .file(path: otfFont), - .file(path: ttcFont), - .file(path: lottieFile), - ] + resources: .init( + [ + .folderReference(path: aAssets), + .file(path: frenchStrings), + .file(path: frenchStringsDict), + .file(path: englishStrings), + .file(path: englishStringsDict), + .file(path: emptyPlist), + .file(path: environmentPlist), + .file(path: ttfFont), + .file(path: otfFont), + .file(path: ttcFont), + .file(path: lottieFile), + ] + ) ) let resourceSynthesizers: [ResourceSynthesizer] = [ @@ -377,6 +434,17 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { extensions: ["lottie"], template: .file(lottieTemplatePath) ), + .init( + parser: .coreData, + parserOptions: [ + "stringValue": "test", + "intValue": 999, + "boolValue": true, + "doubleValue": 1.0, + ], + extensions: ["xcdatamodeld"], + template: .file(coreDataTemplatePath) + ), ] let project = Project.test( @@ -418,9 +486,11 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { targets: [ .test( name: "TargetA", - resources: [ - .file(path: ttfFont), - ] + resources: .init( + [ + .file(path: ttfFont), + ] + ) ), ], resourceSynthesizers: makeResourceSynthesizers() @@ -455,9 +525,11 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { targets: [ .test( name: "TargetA", - resources: [ - .file(path: ttfFont), - ] + resources: .init( + [ + .file(path: ttfFont), + ] + ) ), ], resourceSynthesizers: makeResourceSynthesizers() diff --git a/Tests/TuistGeneratorTests/ProjectMappers/TargetActionDisableShowEnvVarsProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/TargetActionDisableShowEnvVarsProjectMapperTests.swift index 19a4c7771c5..73ff03cf7d4 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/TargetActionDisableShowEnvVarsProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/TargetActionDisableShowEnvVarsProjectMapperTests.swift @@ -1,6 +1,5 @@ import TuistCore -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting @@ -17,16 +16,17 @@ final class TargetActionDisableShowEnvVarsProjectMapperTests: TuistUnitTestCase // When let (updatedProject, _) = try subject.map(project: project) + let updatedTargets = updatedProject.targets.values.sorted() // Then - XCTAssertTrue(project.targets[0].scripts[0].showEnvVarsInLog) - XCTAssertTrue(project.targets[0].scripts[1].showEnvVarsInLog) - XCTAssertTrue(project.targets[1].scripts[0].showEnvVarsInLog) - XCTAssertTrue(project.targets[1].scripts[1].showEnvVarsInLog) - XCTAssertFalse(updatedProject.targets[0].scripts[0].showEnvVarsInLog) - XCTAssertFalse(updatedProject.targets[0].scripts[1].showEnvVarsInLog) - XCTAssertFalse(updatedProject.targets[1].scripts[0].showEnvVarsInLog) - XCTAssertFalse(updatedProject.targets[1].scripts[1].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[1].scripts[0].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[1].scripts[1].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[0].scripts[0].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[0].scripts[1].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[1].scripts[0].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[1].scripts[1].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[0].scripts[0].showEnvVarsInLog) + XCTAssertFalse(updatedTargets[0].scripts[1].showEnvVarsInLog) } func test_map_environmentLoggingEnables() throws { @@ -40,15 +40,16 @@ final class TargetActionDisableShowEnvVarsProjectMapperTests: TuistUnitTestCase // When let (updatedProject, _) = try subject.map(project: project) + let updatedTargets = updatedProject.targets.values.sorted() // Then - XCTAssertTrue(project.targets[0].scripts[0].showEnvVarsInLog) - XCTAssertTrue(project.targets[0].scripts[1].showEnvVarsInLog) - XCTAssertTrue(project.targets[1].scripts[0].showEnvVarsInLog) - XCTAssertTrue(project.targets[1].scripts[1].showEnvVarsInLog) - XCTAssertTrue(updatedProject.targets[0].scripts[0].showEnvVarsInLog) - XCTAssertTrue(updatedProject.targets[0].scripts[1].showEnvVarsInLog) - XCTAssertTrue(updatedProject.targets[1].scripts[0].showEnvVarsInLog) - XCTAssertTrue(updatedProject.targets[1].scripts[1].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[0].scripts[0].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[0].scripts[1].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[1].scripts[0].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[1].scripts[1].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[0].scripts[0].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[0].scripts[1].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[1].scripts[0].showEnvVarsInLog) + XCTAssertTrue(updatedTargets[1].scripts[1].showEnvVarsInLog) } } diff --git a/Tests/TuistGeneratorTests/Settings/DefaultSettingsProviderTests.swift b/Tests/TuistGeneratorTests/Settings/DefaultSettingsProviderTests.swift index 84dc6c2d94e..d35f405dcc5 100644 --- a/Tests/TuistGeneratorTests/Settings/DefaultSettingsProviderTests.swift +++ b/Tests/TuistGeneratorTests/Settings/DefaultSettingsProviderTests.swift @@ -1,8 +1,8 @@ +import MockableTest import struct TSCUtility.Version import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting +import XcodeGraph import XCTest @testable import TuistGenerator @testable import TuistSupportTesting @@ -56,7 +56,7 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { private let appTargetEssentialDebugSettings: [String: SettingValue] = [ "SDKROOT": "iphoneos", "LD_RUNPATH_SEARCH_PATHS": ["$(inherited)", "@executable_path/Frameworks"], - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": .array(["$(inherited)", "DEBUG"]), "CODE_SIGN_IDENTITY": "iPhone Developer", "SWIFT_OPTIMIZATION_LEVEL": "-Onone", "TARGETED_DEVICE_FAMILY": "1,2", @@ -75,7 +75,7 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ] private let frameworkTargetEssentialDebugSettings: [String: SettingValue] = [ - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": .array(["$(inherited)", "DEBUG"]), "SKIP_INSTALL": "YES", "CODE_SIGN_IDENTITY": "", "VERSIONING_SYSTEM": "apple-generic", @@ -116,7 +116,7 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { private let testTargetEssentialDebugSettings: [String: SettingValue] = [ "SDKROOT": "iphoneos", "LD_RUNPATH_SEARCH_PATHS": ["$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks"], - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": .array(["$(inherited)", "DEBUG"]), "CODE_SIGN_IDENTITY": "iPhone Developer", "SWIFT_OPTIMIZATION_LEVEL": "-Onone", "TARGETED_DEVICE_FAMILY": "1,2", @@ -124,7 +124,7 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ] private let multiplatformFrameworkTargetEssentialDebugSettings: [String: SettingValue] = [ - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": .array(["$(inherited)", "DEBUG"]), "SKIP_INSTALL": "YES", "VERSIONING_SYSTEM": "apple-generic", "DYLIB_CURRENT_VERSION": "1", @@ -163,6 +163,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -184,6 +188,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -204,6 +212,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -220,6 +232,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .dynamicLibrary, mergeable: true) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -238,6 +254,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .app) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -255,6 +275,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .app, mergedBinaryType: .automatic) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -272,6 +296,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .app, mergedBinaryType: .manual(mergeableDependencies: Set(["Sample"]))) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -290,6 +318,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .app, mergedBinaryType: .manual(mergeableDependencies: Set(["Sample"]))) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -313,6 +345,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .app, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -335,6 +371,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .framework, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -357,6 +397,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .framework, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -378,6 +422,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -400,6 +448,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -421,6 +473,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -441,6 +497,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test(settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.projectSettings( project: project, @@ -461,7 +521,9 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let project = Project.test() let target = Target.test(settings: settings) - xcodeController.selectedVersionStub = .success(Version(11, 0, 0)) + given(xcodeController) + .selectedVersion() + .willReturn(Version(11, 0, 0)) // When let got = try subject.targetSettings( @@ -480,6 +542,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test(settings: .test(defaultSettings: .essential)) let target = Target.test(settings: nil) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -501,7 +567,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let target = Target.test(settings: settings) let project = Project.test() - xcodeController.selectedVersionStub = .success(Version(10, 0, 0)) + + given(xcodeController) + .selectedVersion() + .willReturn(Version(10, 0, 0)) // When let got = try subject.targetSettings( @@ -524,7 +593,9 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let target = Target.test(settings: settings) let project = Project.test() - xcodeController.selectedVersionStub = .success(Version(11, 0, 0)) + given(xcodeController) + .selectedVersion() + .willReturn(Version(11, 0, 0)) // When let got = try subject.targetSettings( @@ -548,6 +619,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let target = Target.test(product: .app, settings: settings) let project = Project.test() + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -576,6 +651,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) ) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -598,6 +677,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let target = Target.test(product: .app, settings: settings) let project = Project.test() + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -641,7 +724,9 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { ) let target = Target.test(product: .app, settings: settings) let project = Project.test() - xcodeController.selectedVersionStub = .success(Version(11, 0, 0)) + given(xcodeController) + .selectedVersion() + .willReturn(Version(11, 0, 0)) // When let got = try subject.targetSettings( @@ -666,6 +751,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .framework, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -689,6 +778,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .framework, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -712,6 +805,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .framework, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -756,6 +853,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .unitTests, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -778,6 +879,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .uiTests, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -800,6 +905,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .unitTests, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -822,6 +931,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { let project = Project.test() let target = Target.test(product: .uiTests, settings: settings) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -848,6 +961,10 @@ final class DefaultSettingsProvider_iOSTests: TuistUnitTestCase { settings: settings ) + given(xcodeController) + .selectedVersion() + .willReturn(Version(15, 0, 0)) + // When let got = try subject.targetSettings( target: target, @@ -864,7 +981,7 @@ final class DefaultSettingsProvider_MacosTests: TuistUnitTestCase { private var subject: DefaultSettingsProvider! private let macroTargetEssentialDebugSettings: [String: SettingValue] = [ - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": .array(["$(inherited)", "DEBUG"]), "SKIP_INSTALL": "YES", "SWIFT_OPTIMIZATION_LEVEL": "-Onone", "SWIFT_VERSION": "5.0", diff --git a/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift b/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift index 4a98dc4177b..a5854ba7179 100644 --- a/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift b/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift index 14787089fdb..7c72ffbdf3d 100644 --- a/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift +++ b/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore @testable import TuistGenerator diff --git a/Tests/TuistGeneratorTests/Utils/Mocks/MockSynthesizedResourceInterfaceGenerator.swift b/Tests/TuistGeneratorTests/Utils/Mocks/MockSynthesizedResourceInterfaceGenerator.swift index 8fb0ef6a755..57938d74fdc 100644 --- a/Tests/TuistGeneratorTests/Utils/Mocks/MockSynthesizedResourceInterfaceGenerator.swift +++ b/Tests/TuistGeneratorTests/Utils/Mocks/MockSynthesizedResourceInterfaceGenerator.swift @@ -1,5 +1,5 @@ -import TSCBasic -import TuistGraph +import Path +import XcodeGraph @testable import TuistGenerator final class MockSynthesizedResourceInterfaceGenerator: SynthesizedResourceInterfacesGenerating { diff --git a/Tests/TuistGeneratorTests/Utils/SettingsHelperTests.swift b/Tests/TuistGeneratorTests/Utils/SettingsHelperTests.swift index dc5c8a1243c..8ca23605e8b 100644 --- a/Tests/TuistGeneratorTests/Utils/SettingsHelperTests.swift +++ b/Tests/TuistGeneratorTests/Utils/SettingsHelperTests.swift @@ -1,7 +1,7 @@ import Foundation import TuistCore import TuistCoreTesting -import TuistGraph +import XcodeGraph import XcodeProj import XCTest @testable import TuistGenerator diff --git a/Tests/TuistGraphTests/DependenciesGraph/DependenciesGraphTests.swift b/Tests/TuistGraphTests/DependenciesGraph/DependenciesGraphTests.swift deleted file mode 100644 index bd398e6055f..00000000000 --- a/Tests/TuistGraphTests/DependenciesGraph/DependenciesGraphTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import TuistSupport -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class DependenciesGraphTests: TuistUnitTestCase { - func test_codable_xcframework() { - // Given - let subject = DependenciesGraph.test() - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift b/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift deleted file mode 100644 index 9cc2e18b7f0..00000000000 --- a/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class GraphDependencyTests: TuistUnitTestCase { - func test_codable_target() { - // Given - let subject = GraphDependency.testTarget() - - // Then - XCTAssertCodable(subject) - } - - func test_codable_framework() { - // Given - let subject = GraphDependency.testFramework() - - // 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/Graph/GraphTargetTests.swift b/Tests/TuistGraphTests/Graph/GraphTargetTests.swift deleted file mode 100644 index 743f48a80b2..00000000000 --- a/Tests/TuistGraphTests/Graph/GraphTargetTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class GraphTargetTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = GraphTarget.test() - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Graph/GraphTests.swift b/Tests/TuistGraphTests/Graph/GraphTests.swift deleted file mode 100644 index 8ba1154730e..00000000000 --- a/Tests/TuistGraphTests/Graph/GraphTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class GraphTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = Graph.test(name: "name", path: "/path/to") - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/AnalyzeActionTests.swift b/Tests/TuistGraphTests/Models/AnalyzeActionTests.swift deleted file mode 100644 index 5bb2c79c0fd..00000000000 --- a/Tests/TuistGraphTests/Models/AnalyzeActionTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class AnalyzeActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = AnalyzeAction(configurationName: "name") - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/ArchiveActionTests.swift b/Tests/TuistGraphTests/Models/ArchiveActionTests.swift deleted file mode 100644 index 37235a281ba..00000000000 --- a/Tests/TuistGraphTests/Models/ArchiveActionTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ArchiveActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = ArchiveAction( - configurationName: "name", - revealArchiveInOrganizer: true, - customArchiveName: "archiveName", - preActions: [ - .init( - title: "preActionTitle", - scriptText: "text", - target: nil, - shellPath: nil, - showEnvVarsInLog: false - ), - ], - postActions: [ - .init( - title: "postActionTitle", - scriptText: "text", - target: nil, - shellPath: nil, - showEnvVarsInLog: true - ), - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/ArgumentsTests.swift b/Tests/TuistGraphTests/Models/ArgumentsTests.swift deleted file mode 100644 index 11a4de86850..00000000000 --- a/Tests/TuistGraphTests/Models/ArgumentsTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ArgumentsTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = Arguments( - environmentVariables: [ - "key": EnvironmentVariable(value: "value", isEnabled: true), - ], - launchArguments: [ - .init( - name: "name", - isEnabled: true - ), - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/BuildActionTests.swift b/Tests/TuistGraphTests/Models/BuildActionTests.swift deleted file mode 100644 index 32a4b3b67c2..00000000000 --- a/Tests/TuistGraphTests/Models/BuildActionTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class BuildActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = BuildAction( - targets: [ - .init( - projectPath: "/path/to/project", - name: "name" - ), - ], - preActions: [ - .init( - title: "preActionTitle", - scriptText: "text", - target: nil, - shellPath: nil, - showEnvVarsInLog: true - ), - ], - postActions: [ - .init( - title: "postActionTitle", - scriptText: "text", - target: nil, - shellPath: nil, - showEnvVarsInLog: false - ), - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/BuildConfigurationTests.swift b/Tests/TuistGraphTests/Models/BuildConfigurationTests.swift deleted file mode 100644 index c9325af837e..00000000000 --- a/Tests/TuistGraphTests/Models/BuildConfigurationTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import TuistCore -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class BuildConfigurationTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = BuildConfiguration( - name: "Debug", - variant: .debug - ) - - // Then - XCTAssertCodable(subject) - } - - func test_name_returnsTheRightValue_whenDebug() { - XCTAssertEqual(BuildConfiguration.debug.name, "Debug") - } - - func test_name_returnsTheRightValue_whenRelease() { - XCTAssertEqual(BuildConfiguration.release.name, "Release") - } - - func test_xcodeValue_returnsTheRightValue_whenDebug() { - XCTAssertEqual(BuildConfiguration.debug.xcodeValue, "Debug") - } - - func test_xcodeValue_returnsTheRightValue_whenRelease() { - XCTAssertEqual(BuildConfiguration.release.xcodeValue, "Release") - } - - func test_hashValue() { - XCTAssertEqual( - BuildConfiguration(name: "Debug", variant: .debug).hashValue, - BuildConfiguration(name: "Debug", variant: .debug).hashValue - ) - XCTAssertEqual( - BuildConfiguration(name: "Debug", variant: .debug).hashValue, - BuildConfiguration.debug.hashValue - ) - XCTAssertEqual( - BuildConfiguration(name: "debug", variant: .debug).hashValue, - BuildConfiguration.debug.hashValue - ) - XCTAssertNotEqual( - BuildConfiguration(name: "Debug", variant: .debug).hashValue, - BuildConfiguration.release.hashValue - ) - XCTAssertNotEqual( - BuildConfiguration(name: "debug", variant: .debug).hashValue, - BuildConfiguration.release.hashValue - ) - } -} diff --git a/Tests/TuistGraphTests/Models/BuildRule.CompilerSpecTests.swift b/Tests/TuistGraphTests/Models/BuildRule.CompilerSpecTests.swift deleted file mode 100644 index 192d4ebe0a5..00000000000 --- a/Tests/TuistGraphTests/Models/BuildRule.CompilerSpecTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class BuildRuleCompilerSpecTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = BuildRule.CompilerSpec.customScript - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/BuildRule.FileTypeTests.swift b/Tests/TuistGraphTests/Models/BuildRule.FileTypeTests.swift deleted file mode 100644 index 0e6e9a18fe7..00000000000 --- a/Tests/TuistGraphTests/Models/BuildRule.FileTypeTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class BuildRuleFileTypeTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = BuildRule.FileType.sourceFilesWithNamesMatching - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/BuildRuleTests.swift b/Tests/TuistGraphTests/Models/BuildRuleTests.swift deleted file mode 100644 index e8cf0a5d1d7..00000000000 --- a/Tests/TuistGraphTests/Models/BuildRuleTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class BuildRuleTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = BuildRule( - compilerSpec: .unifdef, - fileType: .sourceFilesWithNamesMatching - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/CompatibleXcodeVersionsTests.swift b/Tests/TuistGraphTests/Models/CompatibleXcodeVersionsTests.swift deleted file mode 100644 index bbfe7a8f409..00000000000 --- a/Tests/TuistGraphTests/Models/CompatibleXcodeVersionsTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class CompatibleXcodeVersionsTests: XCTestCase { - func test_isCompatible_when_all() { - // Given - let subject = CompatibleXcodeVersions.all - - // Then - XCTAssertTrue(subject.isCompatible(versionString: "1")) - XCTAssertTrue(subject.isCompatible(versionString: "5.5")) - XCTAssertTrue(subject.isCompatible(versionString: "15.10.10")) - } - - func test_isCompatible_when_list() { - // Given - let subject = CompatibleXcodeVersions.list([.upToNextMajor("13.2.2"), .upToNextMinor("1"), "12.5.1"]) - - // Then - XCTAssertTrue(subject.isCompatible(versionString: "12.5.1")) - XCTAssertFalse(subject.isCompatible(versionString: "12.5.2")) - XCTAssertFalse(subject.isCompatible(versionString: "13.2.1")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2.2")) - XCTAssertTrue(subject.isCompatible(versionString: "13.3.6")) - XCTAssertFalse(subject.isCompatible(versionString: "14.2.0")) - XCTAssertTrue(subject.isCompatible(versionString: "1.0.0")) - XCTAssertTrue(subject.isCompatible(versionString: "1.0.5")) - XCTAssertFalse(subject.isCompatible(versionString: "2.0.0")) - } - - func test_isCompatible_when_exact() { - // Given - let subject = CompatibleXcodeVersions.exact("13.2") - - // Then - XCTAssertTrue(subject.isCompatible(versionString: "13.2")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2.0")) - XCTAssertFalse(subject.isCompatible(versionString: "13.2.2")) - XCTAssertFalse(subject.isCompatible(versionString: "13.3.0")) - XCTAssertFalse(subject.isCompatible(versionString: "14.2.0")) - } - - func test_isCompatible_when_upToNextMajor() { - // Given - let subject = CompatibleXcodeVersions.upToNextMajor("13.2") - - // Then - XCTAssertFalse(subject.isCompatible(versionString: "12.3.0")) - XCTAssertFalse(subject.isCompatible(versionString: "13.0.0")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2.0")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2.2")) - XCTAssertTrue(subject.isCompatible(versionString: "13.3.0")) - XCTAssertFalse(subject.isCompatible(versionString: "14.0.0")) - XCTAssertFalse(subject.isCompatible(versionString: "14.2.0")) - } - - func test_isCompatible_when_upToNextMinor() { - // Given - let subject = CompatibleXcodeVersions.upToNextMinor("13.2") - - // Then - XCTAssertFalse(subject.isCompatible(versionString: "12.2.0")) - XCTAssertFalse(subject.isCompatible(versionString: "13.0.0")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2.0")) - XCTAssertTrue(subject.isCompatible(versionString: "13.2.2")) - XCTAssertFalse(subject.isCompatible(versionString: "13.3.0")) - XCTAssertFalse(subject.isCompatible(versionString: "14.2.0")) - } - - func test_description() { - XCTAssertTrue("\(CompatibleXcodeVersions.all)" == "all") - XCTAssertTrue("\(CompatibleXcodeVersions.exact("1.2"))" == "1.2.0") - XCTAssertTrue("\(CompatibleXcodeVersions.upToNextMajor("1.2.3"))" == "1.2.3..<2.0.0") - XCTAssertTrue("\(CompatibleXcodeVersions.upToNextMinor("1.2.3"))" == "1.2.3..<1.3.0") - - let versionsList = CompatibleXcodeVersions.list([.upToNextMajor("13.2.2"), .upToNextMinor("1"), "12.5.1"]) - XCTAssertTrue("\(versionsList)" == "13.2.2..<14.0.0 or 1.0.0..<1.1.0 or 12.5.1") - } -} diff --git a/Tests/TuistGraphTests/Models/CopyFilesActionTests.swift b/Tests/TuistGraphTests/Models/CopyFilesActionTests.swift deleted file mode 100644 index 8ac858d25df..00000000000 --- a/Tests/TuistGraphTests/Models/CopyFilesActionTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class CopyFilesActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = CopyFilesAction( - name: "name", - destination: .frameworks, - subpath: "subpath", - files: [ - .file(path: "/path/to/file"), - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/CoreDataModelTests.swift b/Tests/TuistGraphTests/Models/CoreDataModelTests.swift deleted file mode 100644 index d8f3a27411f..00000000000 --- a/Tests/TuistGraphTests/Models/CoreDataModelTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class CoreDataModelTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = CoreDataModel( - path: "/path/to/model", - versions: [ - "/path/to/version", - ], - currentVersion: "1.1.1" - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/Dependencies/CarthageDependenciesTests.swift b/Tests/TuistGraphTests/Models/Dependencies/CarthageDependenciesTests.swift deleted file mode 100644 index 3cbbd2195fd..00000000000 --- a/Tests/TuistGraphTests/Models/Dependencies/CarthageDependenciesTests.swift +++ /dev/null @@ -1,160 +0,0 @@ -import Foundation -import XCTest -@testable import TuistGraph -@testable import TuistSupportTesting - -final class CarthageDependenciesTests: TuistUnitTestCase { - func test_cartfileValue_singleDependency() { - // Given - let carthageDependencies: CarthageDependencies = .init( - [ - .github(path: "Dependency/Dependency", requirement: .exact("1.1.1")), - ] - ) - let expected = """ - github "Dependency/Dependency" == 1.1.1 - """ - - // When - let got = carthageDependencies.cartfileValue() - - // Then - XCTAssertEqual(got, expected) - } - - func test_cartfileValue_multipleDependencies() { - // Given - let carthageDependencies: CarthageDependencies = .init( - [ - .github(path: "Dependency/Dependency", requirement: .exact("2.1.1")), - .github(path: "XYZ/Foo", requirement: .revision("revision")), - .git(path: "Foo/Bar", requirement: .atLeast("1.0.1")), - .github(path: "Qwerty/bar", requirement: .branch("develop")), - .github(path: "XYZ/Bar", requirement: .upToNext("1.1.1")), - .binary(path: "https://my.domain.com/release/MyFramework.json", requirement: .upToNext("1.0.1")), - .binary(path: "file:///some/local/path/MyFramework.json", requirement: .atLeast("1.1.0")), - ] - ) - let expected = """ - github "Dependency/Dependency" == 2.1.1 - github "XYZ/Foo" "revision" - git "Foo/Bar" >= 1.0.1 - github "Qwerty/bar" "develop" - github "XYZ/Bar" ~> 1.1.1 - binary "https://my.domain.com/release/MyFramework.json" ~> 1.0.1 - binary "file:///some/local/path/MyFramework.json" >= 1.1.0 - """ - - // When - let got = carthageDependencies.cartfileValue() - - // Then - XCTAssertEqual(got, expected) - } - - // MARK: - CarthageDependency.Dependency tests - - func test_dependency_cartfileValue_github() { - // Given - let origin: CarthageDependencies.Dependency = .github(path: "Alamofire/Alamofire", requirement: .exact("1.2.3")) - let expected = #"github "Alamofire/Alamofire" == 1.2.3"# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - func test_dependency_cartfileValue_git() { - // Given - let origin: CarthageDependencies.Dependency = .git( - path: "https://enterprise.local/desktop/git-error-translations2.git", - requirement: .atLeast("5.4.3") - ) - let expected = #"git "https://enterprise.local/desktop/git-error-translations2.git" >= 5.4.3"# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - func test_dependency_cartfileValue_binary() { - // Given - let origin: CarthageDependencies.Dependency = .binary( - path: "file:///some/local/path/MyFramework.json", - requirement: .upToNext("5.0.0") - ) - let expected = #"binary "file:///some/local/path/MyFramework.json" ~> 5.0.0"# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - // MARK: - CarthageDependencies.Requirement tests - - func test_requirement_cartfileValue_exact() { - // Given - let origin: CarthageDependencies.Requirement = .exact("1.2.3") - let expected = #"== 1.2.3"# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - func test_requirement_cartfileValue_upToNext() { - // Given - let origin: CarthageDependencies.Requirement = .upToNext("3.2.3") - let expected = #"~> 3.2.3"# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - func test_requirement_cartfileValue_atLeast() { - // Given - let origin: CarthageDependencies.Requirement = .atLeast("1.2.1") - let expected = #">= 1.2.1"# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - func test_requirement_cartfileValue_branch() { - // Given - let origin: CarthageDependencies.Requirement = .branch("develop") - let expected = #""develop""# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } - - func test_requirement_cartfileValue_revision() { - // Given - let origin: CarthageDependencies.Requirement = .revision("1234567898765432qwerty") - let expected = #""1234567898765432qwerty""# - - // When - let got = origin.cartfileValue - - // Then - XCTAssertEqual(got, expected) - } -} diff --git a/Tests/TuistGraphTests/Models/DeploymentDeviceTests.swift b/Tests/TuistGraphTests/Models/DeploymentDeviceTests.swift deleted file mode 100644 index 07ba3e48688..00000000000 --- a/Tests/TuistGraphTests/Models/DeploymentDeviceTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class DeploymentDeviceTests: TuistUnitTestCase { - func test_codable_iphone() { - // Given - let subject = DeploymentDevice.iphone - - // Then - XCTAssertCodable(subject) - } - - func test_codable_mac() { - // Given - let subject = DeploymentDevice.mac - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/DeploymentTargetTests.swift b/Tests/TuistGraphTests/Models/DeploymentTargetTests.swift deleted file mode 100644 index 7b45aae2e9c..00000000000 --- a/Tests/TuistGraphTests/Models/DeploymentTargetTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class DeploymentTargetTests: TuistUnitTestCase { - func test_codable_iOS() { - // Given - let subject = DeploymentTargets.iOS("12.1") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_macOS() { - // Given - let subject = DeploymentTargets.macOS("10.6") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_watchOS() { - // Given - let subject = DeploymentTargets.watchOS("9.3") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_tvOS() { - // Given - let subject = DeploymentTargets.tvOS("13.2.1") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_many_OS() { - // Given - let subject = DeploymentTargets(iOS: "12.1", macOS: "10.6", watchOS: "9.3", tvOS: "13.2.1") - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/ExecutionActionTests.swift b/Tests/TuistGraphTests/Models/ExecutionActionTests.swift deleted file mode 100644 index 4133294200b..00000000000 --- a/Tests/TuistGraphTests/Models/ExecutionActionTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ExecutionActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = ExecutionAction( - title: "title", - scriptText: "text", - target: .init( - projectPath: "/path/to/project", - name: "name" - ), - shellPath: nil, - showEnvVarsInLog: false - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/FileElementTests.swift b/Tests/TuistGraphTests/Models/FileElementTests.swift deleted file mode 100644 index 966518bd56f..00000000000 --- a/Tests/TuistGraphTests/Models/FileElementTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class FileElementTests: TuistUnitTestCase { - func test_codable_file() { - // Given - let subject = FileElement.file(path: "/path/to/file") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_folderReference() { - // Given - let subject = FileElement.folderReference(path: "/folder/reference") - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/HeadersTests.swift b/Tests/TuistGraphTests/Models/HeadersTests.swift deleted file mode 100644 index 8a9b015970b..00000000000 --- a/Tests/TuistGraphTests/Models/HeadersTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class HeadersTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = Headers( - public: [ - "/path/to/public/header", - ], - private: [ - "/path/to/private/header", - ], - project: [ - "/path/to/project/header", - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/IDETemplateMacrosTests.swift b/Tests/TuistGraphTests/Models/IDETemplateMacrosTests.swift deleted file mode 100644 index eb44c9878fc..00000000000 --- a/Tests/TuistGraphTests/Models/IDETemplateMacrosTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -import TuistGraph -import XCTest - -final class IDETemplateMacrosTests: XCTestCase { - func test_removing_leading_comment_slashes() { - // Given - let fileHeader = "// Some template" - let templateMacros = IDETemplateMacros(fileHeader: fileHeader) - - // Then - XCTAssertEqual(" Some template", templateMacros.fileHeader) - } - - func test_space_preservation_if_leading_comment_slashes_are_present() { - // Given - let fileHeader = "//Some template" - let templateMacros = IDETemplateMacros(fileHeader: fileHeader) - - // Then - XCTAssertEqual("Some template", templateMacros.fileHeader) - } - - func test_removing_trailing_newline() { - // Given - let fileHeader = "Some template\n" - let templateMacros = IDETemplateMacros(fileHeader: fileHeader) - - // Then - XCTAssertEqual(" Some template", templateMacros.fileHeader) - } - - func test_inserting_leading_space() { - // Given - let fileHeader = "Some template" - let templateMacros = IDETemplateMacros(fileHeader: fileHeader) - - // Then - XCTAssertEqual(" Some template", templateMacros.fileHeader) - } - - func test_not_inserting_leading_space_if_already_present() { - // Given - let fileHeader = " Some template" - let templateMacros = IDETemplateMacros(fileHeader: fileHeader) - - // Then - XCTAssertEqual(" Some template", templateMacros.fileHeader) - } - - func test_not_inserting_leading_space_if_starting_with_newline() { - // Given - let fileHeader = "\nSome template" - let templateMacros = IDETemplateMacros(fileHeader: fileHeader) - - // Then - XCTAssertEqual("\nSome template", templateMacros.fileHeader) - } -} diff --git a/Tests/TuistGraphTests/Models/PackageTests.swift b/Tests/TuistGraphTests/Models/PackageTests.swift deleted file mode 100644 index ccf145c7413..00000000000 --- a/Tests/TuistGraphTests/Models/PackageTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class PackageTests: TuistUnitTestCase { - func test_codable_local() { - // Given - let subject = Package.local(path: "/path/to/package") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_remote() { - // Given - let subject = Package.remote( - url: "/url/to/package", - requirement: .branch("branch") - ) - - // Then - XCTAssertCodable(subject) - } - - func test_is_remote_local() { - // Given - let subject = Package.local(path: "/path/to/package") - - // Then - XCTAssertFalse(subject.isRemote) - } - - func test_is_remote_remote() { - // Given - let subject = Package.remote( - url: "/url/to/package", - requirement: .branch("branch") - ) - - // Then - XCTAssertTrue(subject.isRemote) - } -} diff --git a/Tests/TuistGraphTests/Models/PlatformFilterTests.swift b/Tests/TuistGraphTests/Models/PlatformFilterTests.swift deleted file mode 100644 index 59faa4fcddc..00000000000 --- a/Tests/TuistGraphTests/Models/PlatformFilterTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class PlatformFilterTests: TuistUnitTestCase { - func test_xcodeprojValue() { - XCTAssertEqual(PlatformFilter.catalyst.xcodeprojValue, "maccatalyst") - XCTAssertEqual(PlatformFilter.ios.xcodeprojValue, "ios") - XCTAssertEqual(PlatformFilter.driverkit.xcodeprojValue, "driverkit") - XCTAssertEqual(PlatformFilter.macos.xcodeprojValue, "macos") - XCTAssertEqual(PlatformFilter.tvos.xcodeprojValue, "tvos") - XCTAssertEqual(PlatformFilter.watchos.xcodeprojValue, "watchos") - } - - func test_platformfilters_xcodeprojValue() { - func xcodeProjValueFor(_ filters: PlatformFilters) -> [String] { - filters.xcodeprojValue - } - - XCTAssertEqual(xcodeProjValueFor([.ios, .macos]), ["ios", "macos"]) - XCTAssertEqual(xcodeProjValueFor([.macos, .ios]), ["ios", "macos"]) - XCTAssertEqual(xcodeProjValueFor([.tvos, .macos, .ios]), ["ios", "macos", "tvos"]) - } -} diff --git a/Tests/TuistGraphTests/Models/PlatformTests.swift b/Tests/TuistGraphTests/Models/PlatformTests.swift deleted file mode 100644 index bd65afa6766..00000000000 --- a/Tests/TuistGraphTests/Models/PlatformTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Foundation -import XCTest -@testable import TuistGraph - -final class PlatformTests: XCTestCase { - func test_codable_iOS() { - // Given - let subject = Platform.iOS - - // Then - XCTAssertCodable(subject) - } - - func test_codable_tvOS() { - // Given - let subject = Platform.tvOS - - // Then - XCTAssertCodable(subject) - } - - func test_xcodeSdkRoot_returns_the_right_value() { - XCTAssertEqual(Platform.macOS.xcodeSdkRoot, "macosx") - XCTAssertEqual(Platform.iOS.xcodeSdkRoot, "iphoneos") - XCTAssertEqual(Platform.tvOS.xcodeSdkRoot, "appletvos") - XCTAssertEqual(Platform.watchOS.xcodeSdkRoot, "watchos") - XCTAssertEqual(Platform.visionOS.xcodeSdkRoot, "xros") - } - - func test_xcodeSimulatorSDK() { - XCTAssertEqual(Platform.tvOS.xcodeSimulatorSDK, "appletvsimulator") - XCTAssertEqual(Platform.iOS.xcodeSimulatorSDK, "iphonesimulator") - XCTAssertEqual(Platform.watchOS.xcodeSimulatorSDK, "watchsimulator") - XCTAssertEqual(Platform.visionOS.xcodeSimulatorSDK, "xrsimulator") - XCTAssertNil(Platform.macOS.xcodeSimulatorSDK) - } - - func test_xcodeDeviceSDK() { - XCTAssertEqual(Platform.tvOS.xcodeDeviceSDK, "appletvos") - XCTAssertEqual(Platform.iOS.xcodeDeviceSDK, "iphoneos") - XCTAssertEqual(Platform.watchOS.xcodeDeviceSDK, "watchos") - XCTAssertEqual(Platform.macOS.xcodeDeviceSDK, "macosx") - XCTAssertEqual(Platform.visionOS.xcodeDeviceSDK, "xros") - } - - func test_hasSimulators() { - XCTAssertFalse(Platform.macOS.hasSimulators) - XCTAssertTrue(Platform.tvOS.hasSimulators) - XCTAssertTrue(Platform.watchOS.hasSimulators) - XCTAssertTrue(Platform.tvOS.hasSimulators) - XCTAssertTrue(Platform.visionOS.hasSimulators) - } - - func test_carthageDirectory() { - XCTAssertEqual(Platform.tvOS.carthageDirectory, "tvOS") - XCTAssertEqual(Platform.iOS.carthageDirectory, "iOS") - XCTAssertEqual(Platform.watchOS.carthageDirectory, "watchOS") - XCTAssertEqual(Platform.macOS.carthageDirectory, "Mac") - } - - func test_xcodeSdkRootPath() { - // Given - let platforms: [Platform] = [ - .iOS, - .macOS, - .tvOS, - .watchOS, - .visionOS, - ] - - // When - let paths = platforms.map(\.xcodeSdkRootPath) - - // Then - XCTAssertEqual(paths, [ - "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk", - "Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", - "Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk", - "Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk", - "Platforms/XROS.platform/Developer/SDKs/XROS.sdk", - ]) - } -} diff --git a/Tests/TuistGraphTests/Models/ProfileActionTests.swift b/Tests/TuistGraphTests/Models/ProfileActionTests.swift deleted file mode 100644 index 01518474041..00000000000 --- a/Tests/TuistGraphTests/Models/ProfileActionTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ProfileActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = ProfileAction( - configurationName: "name", - executable: .init( - projectPath: "/path/to/project", - name: "name" - ), - arguments: .init( - environmentVariables: [ - "key": EnvironmentVariable(value: "value", isEnabled: true), - ], - launchArguments: [ - .init( - name: "name", - isEnabled: false - ), - ] - ) - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/ProjectGroupTests.swift b/Tests/TuistGraphTests/Models/ProjectGroupTests.swift deleted file mode 100644 index c4cdf404b53..00000000000 --- a/Tests/TuistGraphTests/Models/ProjectGroupTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ProjectGroupTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = ProjectGroup.group(name: "name") - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/RawScriptBuildPhaseTests.swift b/Tests/TuistGraphTests/Models/RawScriptBuildPhaseTests.swift deleted file mode 100644 index 9afefd47801..00000000000 --- a/Tests/TuistGraphTests/Models/RawScriptBuildPhaseTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class RawScriptBuildPhaseTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = RawScriptBuildPhase( - name: "name", - script: "script", - showEnvVarsInLog: true, - hashable: true - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/RequirementTests.swift b/Tests/TuistGraphTests/Models/RequirementTests.swift deleted file mode 100644 index 9f983d7177f..00000000000 --- a/Tests/TuistGraphTests/Models/RequirementTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class RequirementTests: TuistUnitTestCase { - func test_codable_range() { - // Given - let subject = Requirement.range(from: "1.0.0", to: "2.0.0") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_upToNextMajor() { - // Given - let subject = Requirement.upToNextMajor("1.2.3") - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/ResourceFileElementTests.swift b/Tests/TuistGraphTests/Models/ResourceFileElementTests.swift deleted file mode 100644 index da65b35cc94..00000000000 --- a/Tests/TuistGraphTests/Models/ResourceFileElementTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ResourceFileElementTests: TuistUnitTestCase { - func test_codable_file() { - // Given - let subject = ResourceFileElement.file( - path: "/path/to/element", - tags: [ - "tag", - ] - ) - - // Then - XCTAssertCodable(subject) - } - - func test_codable_folderReference() { - // Given - let subject = ResourceFileElement.folderReference( - path: "/path/to/folder", - tags: [ - "tag", - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/ResourceSynthesizerTests.swift b/Tests/TuistGraphTests/Models/ResourceSynthesizerTests.swift deleted file mode 100644 index 363b00ce4d3..00000000000 --- a/Tests/TuistGraphTests/Models/ResourceSynthesizerTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class ResourceSynthesizerTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = ResourceSynthesizer( - parser: .coreData, - parserOptions: ["key": "value"], - extensions: [ - "extension1", - "extension2", - ], - template: .defaultTemplate("template") - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/RunActionTests.swift b/Tests/TuistGraphTests/Models/RunActionTests.swift deleted file mode 100644 index 87ef811d767..00000000000 --- a/Tests/TuistGraphTests/Models/RunActionTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class RunActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = RunAction( - configurationName: "name", - attachDebugger: true, - customLLDBInitFile: "/path/to/project", - executable: .init( - projectPath: "/path/to/project", - name: "name" - ), - filePath: "/path/to/file", - arguments: .init( - environmentVariables: [ - "key": EnvironmentVariable(value: "value", isEnabled: true), - ], - launchArguments: [ - .init( - name: "name", - isEnabled: true - ), - ] - ), - options: .init(), - diagnosticsOptions: [ - .mainThreadChecker, - .performanceAntipatternChecker, - ] - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/SDKSourceTests.swift b/Tests/TuistGraphTests/Models/SDKSourceTests.swift deleted file mode 100644 index e2d7729a7b9..00000000000 --- a/Tests/TuistGraphTests/Models/SDKSourceTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class SDKSourceTests: TuistUnitTestCase { - func test_codable_developer() { - // Given - let subject = SDKSource.developer - - // Then - XCTAssertCodable(subject) - } - - func test_codable_system() { - // Given - let subject = SDKSource.system - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/SchemeDiagnosticsOptionTests.swift b/Tests/TuistGraphTests/Models/SchemeDiagnosticsOptionTests.swift deleted file mode 100644 index b4b1939293c..00000000000 --- a/Tests/TuistGraphTests/Models/SchemeDiagnosticsOptionTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class SchemeDiagnosticsOptionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = SchemeDiagnosticsOption.mainThreadChecker - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/SchemeTests.swift b/Tests/TuistGraphTests/Models/SchemeTests.swift deleted file mode 100644 index bd006d50925..00000000000 --- a/Tests/TuistGraphTests/Models/SchemeTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class SchemeTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = Scheme.test(name: "name", shared: true) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/SettingsTests.swift b/Tests/TuistGraphTests/Models/SettingsTests.swift deleted file mode 100644 index fece94c1849..00000000000 --- a/Tests/TuistGraphTests/Models/SettingsTests.swift +++ /dev/null @@ -1,178 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupportTesting -import XCTest -@testable import TuistGraph - -final class SettingsTests: XCTestCase { - func test_codable() { - // Given - let subject = Settings.default - - // Then - XCTAssertCodable(subject) - } - - func testXcconfigs() { - // Given - let configurations: [BuildConfiguration: Configuration?] = [ - BuildConfiguration(name: "D", variant: .debug): Configuration( - settings: [:], - xcconfig: try! AbsolutePath(validating: "/D") - ), - .release("C"): nil, - .debug("A"): Configuration(settings: [:], xcconfig: try! AbsolutePath(validating: "/A")), - .release("B"): Configuration(settings: [:], xcconfig: try! AbsolutePath(validating: "/B")), - ] - - // When - let got = configurations.xcconfigs() - - // Then - XCTAssertEqual(got.map(\.pathString), ["/A", "/B", "/D"]) - } - - func testSortedByBuildConfigurationName() { - // Given - let configurations: [BuildConfiguration: Configuration?] = [ - BuildConfiguration(name: "D", variant: .debug): emptyConfiguration(), - .release("C"): nil, - .debug("A"): nil, - .release("B"): emptyConfiguration(), - ] - - // When - let got = configurations.sortedByBuildConfigurationName() - - // Then - XCTAssertEqual(got.map(\.0.name), ["A", "B", "C", "D"]) - } - - func testDefaultDebugConfigurationWhenDefaultExists() { - // Given - // .debug (i.e. name: "Debug", variant: .debug) is the default debug - let configurations: [BuildConfiguration: Configuration?] = [ - .release("C"): nil, - .debug("A"): nil, - .release("B"): nil, - .debug: nil, - ] - let settings = Settings(configurations: configurations) - - // When - let got = settings.defaultDebugBuildConfiguration() - - // Then - XCTAssertEqual(got, .debug) - } - - func testDefaultDebugConfigurationWhenDefaultDoesNotExist() { - // Given - // .debug (i.e. name: "Debug", variant: .debug) is the default debug - let configurations: [BuildConfiguration: Configuration?] = [ - .release("C"): nil, - .debug("A"): nil, - .release("B"): nil, - ] - let settings = Settings(configurations: configurations) - - // When - let got = settings.defaultDebugBuildConfiguration() - - // Then - XCTAssertEqual(got, .debug("A")) - } - - func testDefaultDebugConfigurationWhenNoDebugConfigurationsExist() { - // Given - let configurations: [BuildConfiguration: Configuration?] = [ - .release("C"): nil, - .release("B"): nil, - ] - let settings = Settings(configurations: configurations) - - // When - let got = settings.defaultDebugBuildConfiguration() - - // Then - XCTAssertNil(got) - } - - func testDefaultReleaseConfigurationWhenDefaultExist() { - // Given - // .release (i.e. name: "Release", variant: .release) is the default release - let configurations: [BuildConfiguration: Configuration?] = [ - .release("C"): nil, - .debug("A"): nil, - .release("B"): nil, - .release: nil, - ] - let settings = Settings(configurations: configurations) - - // When - let got = settings.defaultReleaseBuildConfiguration() - - // Then - XCTAssertEqual(got, .release) - } - - func testDefaultReleaseConfigurationWhenDefaultDoesNotExist() { - // Given - // .release (i.e. name: "Release", variant: .release) is the default release - let configurations: [BuildConfiguration: Configuration?] = [ - .release("C"): nil, - .debug("A"): nil, - .release("B"): nil, - ] - let settings = Settings(configurations: configurations) - - // When - let got = settings.defaultReleaseBuildConfiguration() - - // Then - XCTAssertEqual(got, .release("B")) - } - - func testDefaultReleaseConfigurationWhenNoReleaseConfigurationsExist() { - // Given - let configurations: [BuildConfiguration: Configuration?] = [ - .debug("A"): nil, - .debug("B"): nil, - ] - let settings = Settings(configurations: configurations) - - // When - let got = settings.defaultReleaseBuildConfiguration() - - // Then - XCTAssertNil(got) - } - - // MARK: - Helpers - - private func emptyConfiguration() -> Configuration { - Configuration(settings: [:], xcconfig: nil) - } -} - -final class DictionaryStringSettingValueExtensionTests: XCTestCase { - func testToAny() { - // Given - let buildConfig: [String: SettingValue] = [ - "A": ["A_VALUE_1", "A_VALUE_2"], - "B": "B_VALUE", - "C": ["C_VALUE"], - ] - let expected: [String: Any] = [ - "A": ["A_VALUE_1", "A_VALUE_2"], - "B": "B_VALUE", - "C": ["C_VALUE"], - ] - - // When - let got = buildConfig.toAny() - - // Then - XCTAssertEqualDictionaries(got, expected) - } -} diff --git a/Tests/TuistGraphTests/Models/SourceFileTests.swift b/Tests/TuistGraphTests/Models/SourceFileTests.swift deleted file mode 100644 index f9ee8706669..00000000000 --- a/Tests/TuistGraphTests/Models/SourceFileTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class SourceFileTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = SourceFile( - path: "/path/to/file", - compilerFlags: "flag", - contentHash: "hash" - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/TargetDependencyTests.swift b/Tests/TuistGraphTests/Models/TargetDependencyTests.swift deleted file mode 100644 index 9d1661e8aed..00000000000 --- a/Tests/TuistGraphTests/Models/TargetDependencyTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class TargetDependencyTests: TuistUnitTestCase { - func test_codable_framework() { - // Given - let subject = TargetDependency.framework( - path: "/path/to/framework", - status: .required - ) - - // Then - XCTAssertCodable(subject) - } - - func test_codable_project() { - // Given - let subject = TargetDependency.project(target: "target", path: "/path/to/target") - - // Then - XCTAssertCodable(subject) - } - - func test_codable_library() { - // Given - let subject = TargetDependency.library( - path: "/path/to/library", - publicHeaders: "/path/to/publicheaders", - swiftModuleMap: "/path/to/swiftModuleMap" - ) - - // Then - XCTAssertCodable(subject) - } - - func test_filtering() { - let expected: PlatformCondition? = .when([.macos]) - - let subjects: [TargetDependency] = [ - .framework(path: "/", status: .required, condition: expected), - .library(path: "/", publicHeaders: "/", swiftModuleMap: "/", condition: expected), - .sdk(name: "", status: .required, condition: expected), - .package(product: "", type: .plugin, condition: expected), - .target(name: "", condition: expected), - .xcframework(path: "/", status: .required, condition: expected), - .project(target: "", path: "/", condition: expected), - ] - - for subject in subjects { - XCTAssertEqual(subject.condition, expected) - XCTAssertEqual(subject.withCondition(.when([.catalyst])).condition, .when([.catalyst])) - } - } - - func test_xctest_platformFilters_alwaysReturnAll() { - let subject = TargetDependency.xctest - - XCTAssertNil(subject.condition) - XCTAssertNil(subject.withCondition(.when([.catalyst])).condition) - } -} diff --git a/Tests/TuistGraphTests/Models/TargetReferenceTests.swift b/Tests/TuistGraphTests/Models/TargetReferenceTests.swift deleted file mode 100644 index d08d2a0fbb1..00000000000 --- a/Tests/TuistGraphTests/Models/TargetReferenceTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class TargetReferenceTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = TargetReference( - projectPath: "/path/to/project", - name: "name" - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/TestActionTests.swift b/Tests/TuistGraphTests/Models/TestActionTests.swift deleted file mode 100644 index 68cb0603764..00000000000 --- a/Tests/TuistGraphTests/Models/TestActionTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class TestActionTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = TestAction.test() - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/TestPlanTests.swift b/Tests/TuistGraphTests/Models/TestPlanTests.swift deleted file mode 100644 index 3df7672e657..00000000000 --- a/Tests/TuistGraphTests/Models/TestPlanTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class TestPlanTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = TestPlan( - path: "/path/to", - testTargets: [], - isDefault: true - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/TestableTargetTests.swift b/Tests/TuistGraphTests/Models/TestableTargetTests.swift deleted file mode 100644 index bc773849640..00000000000 --- a/Tests/TuistGraphTests/Models/TestableTargetTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class TestableTargetTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = TestableTarget( - target: .init( - projectPath: "/path/to/project", - name: "name" - ), - skipped: true, - parallelizable: true, - randomExecutionOrdering: true - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/WorkspaceGenerationOptionsTests.swift b/Tests/TuistGraphTests/Models/WorkspaceGenerationOptionsTests.swift deleted file mode 100644 index d9a6d75c9ba..00000000000 --- a/Tests/TuistGraphTests/Models/WorkspaceGenerationOptionsTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class WorkspaceGenerationOptionsTests: TuistUnitTestCase { - func test_codable_whenDefault() { - // Given - let subject = Workspace.GenerationOptions.test() - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistGraphTests/Models/WorkspaceTests.swift b/Tests/TuistGraphTests/Models/WorkspaceTests.swift deleted file mode 100644 index 2f4cb55f296..00000000000 --- a/Tests/TuistGraphTests/Models/WorkspaceTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistGraph -@testable import TuistSupportTesting - -final class WorkspaceTests: TuistUnitTestCase { - func test_codable() { - // Given - let subject = Workspace.test( - path: "/path/to/workspace", - name: "name" - ) - - // Then - XCTAssertCodable(subject) - } -} diff --git a/Tests/TuistHasherTests/CachedContentHasherTests.swift b/Tests/TuistHasherTests/CachedContentHasherTests.swift new file mode 100644 index 00000000000..99ecf438cd3 --- /dev/null +++ b/Tests/TuistHasherTests/CachedContentHasherTests.swift @@ -0,0 +1,97 @@ +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistHasher + +final class CachedContentHasherTests: TuistUnitTestCase { + private var subject: CachedContentHasher! + private var contentHasher: MockContentHashing! + + override func setUp() { + super.setUp() + contentHasher = MockContentHashing() + subject = CachedContentHasher(contentHasher: contentHasher) + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + } + + override func tearDown() { + subject = nil + contentHasher = nil + super.tearDown() + } + + // MARK: - Tests + + func test_hashString_callsContentHasherWithExpectedString() throws { + // Given + given(contentHasher) + .hash(.value("foo")) + .willReturn("foo") + + // When + _ = try subject.hash("foo") + + // Then + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hashStrings_callsContentHasherWithExpectedStrings() throws { + // Given + given(contentHasher) + .hash(.value("foo")) + .willReturn("foo") + given(contentHasher) + .hash(.value("bar")) + .willReturn("bar") + + // When + _ = try subject.hash(["foo", "bar"]) + + // Then + verify(contentHasher) + .hash(Parameter<[String]>.any) + .called(1) + } + + func test_hashpath_callsContentHasherWithExpectedPath() throws { + // Given + let path = try AbsolutePath(validating: "/foo") + given(contentHasher) + .hash(path: .value(path)) + .willReturn("foo-hash") + + // When + _ = try subject.hash(path: path) + + // Then + verify(contentHasher) + .hash(path: .any) + .called(1) + } + + func test_hashpath_secondTime_doesntCallContentHasher() throws { + // Given + let path = try AbsolutePath(validating: "/foo") + given(contentHasher) + .hash(path: .value(path)) + .willReturn("foo-hash") + + // When + let hash = try subject.hash(path: path) + let cachedHash = try subject.hash(path: path) + + // Then + verify(contentHasher) + .hash(path: .any) + .called(1) + XCTAssertEqual(hash, cachedHash) + } +} diff --git a/Tests/TuistHasherTests/CopyFilesContentHasherTests.swift b/Tests/TuistHasherTests/CopyFilesContentHasherTests.swift new file mode 100644 index 00000000000..a8b1eeb7ec9 --- /dev/null +++ b/Tests/TuistHasherTests/CopyFilesContentHasherTests.swift @@ -0,0 +1,112 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class CopyFilesContentHasherTests: TuistUnitTestCase { + private var subject: CopyFilesContentHasher! + private var contentHasher: MockContentHashing! + private var temporaryDirectory: TemporaryDirectory! + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = CopyFilesContentHasher(contentHasher: contentHasher) + + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + + do { + temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) + } catch { + XCTFail("Error while creating temporary directory") + } + } + + override func tearDown() { + subject = nil + temporaryDirectory = nil + contentHasher = nil + + super.tearDown() + } + + private func makeCopyFilesAction( + name: String = "Copy Fonts", + destination: CopyFilesAction.Destination = .resources, + subpath: String? = "Fonts", + files: [CopyFileElement] = [.file(path: "/file1.ttf"), .file(path: "/file2.ttf")] + ) -> CopyFilesAction { + CopyFilesAction( + name: name, + destination: destination, + subpath: subpath, + files: files + ) + } + + // MARK: - Tests + + func test_hash_copyFilesAction_callsMockHasherWithExpectedStrings() throws { + // Given + let file1Hash = "file1-content-hash" + let file2Hash = "file2-content-hash" + let copyFilesAction = makeCopyFilesAction() + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/file1.ttf"))) + .willReturn(file1Hash) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/file2.ttf"))) + .willReturn(file2Hash) + + // When + _ = try subject.hash(copyFiles: [copyFilesAction]) + + // Then + verify(contentHasher) + .hash(.value(["file1-content-hash", "file2-content-hash", "Copy Fonts", "resources", "Fonts"])) + .called(1) + verify(contentHasher) + .hash(Parameter<[String]>.any) + .called(1) + verify(contentHasher) + .hash(path: .any) + .called(2) + } + + func test_hash__copyFilesAction_valuesAreNotHarcoded() throws { + // Given + let file1Hash = "file1-content-hash" + let copyFilesAction = makeCopyFilesAction( + name: "Copy Templates", + destination: .sharedSupport, + subpath: "Templates", + files: [.file(path: "/file1.template")] + ) + + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/file1.template"))) + .willReturn(file1Hash) + + // When + _ = try subject.hash(copyFiles: [copyFilesAction]) + + // Then + verify(contentHasher) + .hash(.value(["file1-content-hash", "Copy Templates", "sharedSupport", "Templates"])) + .called(1) + verify(contentHasher) + .hash(Parameter<[String]>.any) + .called(1) + verify(contentHasher) + .hash(path: .any) + .called(1) + } +} diff --git a/Tests/TuistHasherTests/CoreDataModelsContentHasherTests.swift b/Tests/TuistHasherTests/CoreDataModelsContentHasherTests.swift new file mode 100644 index 00000000000..6f95032743a --- /dev/null +++ b/Tests/TuistHasherTests/CoreDataModelsContentHasherTests.swift @@ -0,0 +1,126 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class CoreDataModelsContentHasherTests: TuistUnitTestCase { + private var subject: CoreDataModelsContentHasher! + private var coreDataModel: CoreDataModel! + private var contentHasher: MockContentHashing! + private let defaultValuesHash = + "05c9d517e2cf12b45786787dae929a23" // Expected hash for the CoreDataModel created with the buildCoreDataModel function + // using default values + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = CoreDataModelsContentHasher(contentHasher: contentHasher) + do { + _ = try TemporaryDirectory(removeTreeOnDeinit: true) + } catch { + XCTFail("Error while creating temporary directory") + } + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + } + + override func tearDown() { + subject = nil + coreDataModel = nil + contentHasher = nil + super.tearDown() + } + + // MARK: - Tests + + func test_hash_returnsSameValue() throws { + // Given + coreDataModel = try buildCoreDataModel(versions: ["v1", "v2"], currentVersion: "currentV1") + given(contentHasher) + .hash(path: .any) + .willProduce { $0.basename } + + // When + let hash = try subject.hash(coreDataModels: [coreDataModel]) + + // Then + XCTAssertEqual(hash, "fixed-hash;currentV1;v1;v2") + } + + func test_hash_fileContentChangesHash() throws { + // Given + let name = "CoreDataModel" + coreDataModel = try buildCoreDataModel() + let fakePath = buildFakePath(from: name) + given(contentHasher) + .hash(path: .any) + .willProduce { $0.basename } + given(contentHasher) + .hash(path: .value(fakePath)) + .willReturn("different-hash") + + // When + let hash = try subject.hash(coreDataModels: [coreDataModel]) + + // Then + XCTAssertNotEqual(hash, defaultValuesHash) + } + + func test_hash_currentVersionChangesHash() throws { + // Given + coreDataModel = try buildCoreDataModel(currentVersion: "2") + given(contentHasher) + .hash(path: .any) + .willProduce { $0.basename } + + // When + let hash = try subject.hash(coreDataModels: [coreDataModel]) + + XCTAssertNotEqual(hash, defaultValuesHash) + } + + func test_hash_versionsChangeHash() throws { + // Given + coreDataModel = try buildCoreDataModel(versions: ["1", "2", "3"]) + given(contentHasher) + .hash(path: .any) + .willProduce { $0.basename } + + // When + let hash = try subject.hash(coreDataModels: [coreDataModel]) + + // Then + XCTAssertNotEqual(hash, defaultValuesHash) + } + + // MARK: - Private + + private func buildFakePath(from name: String) -> AbsolutePath { + try! AbsolutePath(validating: "/\(name)+path") + } + + private func buildCoreDataModel( + name: String = "CoreDataModel", + versions: [String] = ["1", "2"], + currentVersion: String = "1" + ) throws -> CoreDataModel { + let fakePath = buildFakePath(from: name) + + given(contentHasher) + .hash(path: .value(fakePath)) + .willReturn("fixed-hash") + let versionsAbsolutePaths = try versions.map { try AbsolutePath(validating: "/\($0)") } + return CoreDataModel( + path: fakePath, + versions: versionsAbsolutePaths, + currentVersion: currentVersion + ) + } +} diff --git a/Tests/TuistHasherTests/DependenciesContentHasherTests.swift b/Tests/TuistHasherTests/DependenciesContentHasherTests.swift new file mode 100644 index 00000000000..33a42e15bcc --- /dev/null +++ b/Tests/TuistHasherTests/DependenciesContentHasherTests.swift @@ -0,0 +1,314 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class DependenciesContentHasherTests: TuistUnitTestCase { + private var subject: DependenciesContentHasher! + private var contentHasher: MockContentHashing! + private var filePath1: AbsolutePath! = try! AbsolutePath(validating: "/file1") + private var filePath2: AbsolutePath! = try! AbsolutePath(validating: "/file2") + private var filePath3: AbsolutePath! = try! AbsolutePath(validating: "/file3") + private var graphTarget: GraphTarget! + private var hashedTargets: [GraphHashedTarget: String]! + private var hashedPaths: [AbsolutePath: String]! + + override func setUp() { + super.setUp() + contentHasher = .init() + hashedTargets = [:] + hashedPaths = [:] + subject = DependenciesContentHasher(contentHasher: contentHasher) + + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 + "-hash" } + } + + override func tearDown() { + subject = nil + contentHasher = nil + hashedTargets = nil + hashedPaths = nil + graphTarget = nil + filePath1 = nil + filePath2 = nil + filePath3 = nil + super.tearDown() + } + + func test_hash_whenDependencyIsTarget_returnsTheRightHash() throws { + // Given + let dependency = TargetDependency.target(name: "foo") + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + hashedTargets[ + GraphHashedTarget( + projectPath: graphTarget.path, + targetName: "foo" + ) + ] = "target-foo-hash" + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "target-foo-hash") + } + + func test_hash_whenDependencyIsTarget_throwsWhenTheDependencyHasntBeenHashed() throws { + // Given + let dependency = TargetDependency.target(name: "foo") + + // When/Then + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let expectedError = DependenciesContentHasherError.missingTargetHash( + sourceTargetName: graphTarget.target.name, + dependencyProjectPath: graphTarget.path, + dependencyTargetName: "foo" + ) + XCTAssertThrowsSpecific( + try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths), + expectedError + ) + } + + func test_hash_whenDependencyIsProject_returnsTheRightHash() throws { + // Given + let dependency = TargetDependency.project(target: "foo", path: filePath1) + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + hashedTargets[ + GraphHashedTarget( + projectPath: filePath1, + targetName: "foo" + ) + ] = "project-file-hashed-foo-hash" + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "project-file-hashed-foo-hash") + } + + func test_hash_whenDependencyIsProjectWithACondition_returnsTheRightHash() throws { + // Given + let dependency = TargetDependency.project(target: "foo", path: filePath1, condition: .when([.ios])) + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + hashedTargets[ + GraphHashedTarget( + projectPath: filePath1, + targetName: "foo" + ) + ] = "project-file-hashed-foo-hash" + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "project-file-hashed-foo-hash") + } + + func test_hash_whenDependencyIsProject_throwsAnErrorIfTheDependencyHashDoesntExist() throws { + // Given + let dependency = TargetDependency.project(target: "foo", path: filePath1) + + // When/Then + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let expectedError = DependenciesContentHasherError.missingProjectTargetHash( + sourceProjectPath: graphTarget.path, + sourceTargetName: graphTarget.target.name, + dependencyProjectPath: filePath1, + dependencyTargetName: "foo" + ) + XCTAssertThrowsSpecific( + try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths), + expectedError + ) + } + + func test_hash_whenDependencyIsFramework_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.framework(path: filePath1, status: .required) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("file-hashed") + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "file-hashed") + verify(contentHasher) + .hash(path: .any) + .called(1) + } + + func test_hash_whenDependencyIsXCFramework_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.xcframework(path: filePath1, status: .required) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("file-hashed") + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "file-hashed") + verify(contentHasher) + .hash(path: .any) + .called(1) + } + + func test_hash_whenDependencyIsLibrary_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.library( + path: filePath1, + publicHeaders: filePath2, + swiftModuleMap: filePath3 + ) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("file1-hashed") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("file2-hashed") + given(contentHasher) + .hash(path: .value(filePath3)) + .willReturn("file3-hashed") + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "library-file1-hashed-file2-hashed-file3-hashed-hash") + verify(contentHasher) + .hash(path: .any) + .called(3) + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenDependencyIsLibrary_swiftModuleMapIsNil_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.library( + path: filePath1, + publicHeaders: filePath2, + swiftModuleMap: nil + ) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("file1-hashed") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("file2-hashed") + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "library-file1-hashed-file2-hashed-hash") + verify(contentHasher) + .hash(path: .any) + .called(2) + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenDependencyIsPackage_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.package(product: "foo", type: .runtime) + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "package-foo-runtime-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenDependencyIsOptionalSDK_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.sdk(name: "foo", status: .optional) + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "sdk-foo-optional-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenDependencyIsRequiredSDK_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.sdk(name: "foo", status: .required) + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "sdk-foo-required-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenDependencyIsXCTest_callsContentHasherAsExpected() throws { + // Given + let dependency = TargetDependency.xctest + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependency])) + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "xctest-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_sorts_dependency_hashes() throws { + // Given + let dependencyFoo = TargetDependency.target(name: "foo") + let dependencyBar = TargetDependency.target(name: "bar") + + // When + let graphTarget = GraphTarget.test(target: Target.test(dependencies: [dependencyFoo, dependencyBar])) + hashedTargets[ + GraphHashedTarget( + projectPath: graphTarget.path, + targetName: "foo" + ) + ] = "target-foo-hash" + hashedTargets[ + GraphHashedTarget( + projectPath: graphTarget.path, + targetName: "bar" + ) + ] = "target-bar-hash" + let hash = try subject.hash(graphTarget: graphTarget, hashedTargets: &hashedTargets, hashedPaths: &hashedPaths) + + // Then + XCTAssertEqual(hash, "target-bar-hashtarget-foo-hash") + } +} diff --git a/Tests/TuistHasherTests/DeploymentTargetContentHasherTests.swift b/Tests/TuistHasherTests/DeploymentTargetContentHasherTests.swift new file mode 100644 index 00000000000..a64c5f7df4a --- /dev/null +++ b/Tests/TuistHasherTests/DeploymentTargetContentHasherTests.swift @@ -0,0 +1,90 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class DeploymentTargetContentHasherTests: TuistUnitTestCase { + private var subject: DeploymentTargetsContentHasher! + private var contentHasher: MockContentHashing! + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = DeploymentTargetsContentHasher(contentHasher: contentHasher) + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 + "-hash" } + } + + override func tearDown() { + subject = nil + contentHasher = nil + super.tearDown() + } + + func test_hash_whenIosIphoneV1_callsContentHasherWithExpectedStrings() throws { + // When + let deploymentTargets = DeploymentTargets.iOS("v1") + + // Then + let hash = try subject.hash(deploymentTargets: deploymentTargets) + XCTAssertEqual(hash, "iOS-v1-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenIosIpadV2_callsContentHasherWithExpectedStrings() throws { + // When + let deploymentTargets = DeploymentTargets.iOS("v2") + + // Then + let hash = try subject.hash(deploymentTargets: deploymentTargets) + XCTAssertEqual(hash, "iOS-v2-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenMacOSV2_callsContentHasherWithExpectedStrings() throws { + // When + let deploymentTargets = DeploymentTargets.macOS("v2") + + // Then + let hash = try subject.hash(deploymentTargets: deploymentTargets) + XCTAssertEqual(hash, "macOS-v2-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whenWatchOSV2_callsContentHasherWithExpectedStrings() throws { + // When + let deploymentTargets = DeploymentTargets.watchOS("v2") + + // Then + let hash = try subject.hash(deploymentTargets: deploymentTargets) + XCTAssertEqual(hash, "watchOS-v2-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } + + func test_hash_whentvOSV2_callsContentHasherWithExpectedStrings() throws { + // When + let deploymentTargets = DeploymentTargets.tvOS("v2") + + // Then + let hash = try subject.hash(deploymentTargets: deploymentTargets) + XCTAssertEqual(hash, "tvOS-v2-hash") + verify(contentHasher) + .hash(Parameter.any) + .called(1) + } +} diff --git a/Tests/TuistHasherTests/Extensions/Plist+ExtrasTests.swift b/Tests/TuistHasherTests/Extensions/Plist+ExtrasTests.swift new file mode 100644 index 00000000000..8d79e8a2653 --- /dev/null +++ b/Tests/TuistHasherTests/Extensions/Plist+ExtrasTests.swift @@ -0,0 +1,16 @@ +import Path +import TuistCore +import XcodeGraph +import XCTest +@testable import TuistHasher + +class PlistrExtrasTests: XCTestCase { + func test_normalize() throws { + XCTAssertEqual(Plist.Value.string("test").normalize() as? String, "test") + XCTAssertEqual(Plist.Value.integer(1).normalize() as? Int, 1) + XCTAssertEqual(Plist.Value.real(1).normalize() as? Double, 1) + XCTAssertEqual(Plist.Value.boolean(true).normalize() as? Bool, true) + XCTAssertEqual(Plist.Value.array([.string("test")]).normalize() as? [String], ["test"]) + XCTAssertEqual(Plist.Value.dictionary(["test": .string("tuist")]).normalize() as? [String: String], ["test": "tuist"]) + } +} diff --git a/Tests/TuistHasherTests/GraphContentHasherTests.swift b/Tests/TuistHasherTests/GraphContentHasherTests.swift new file mode 100644 index 00000000000..8390380be3b --- /dev/null +++ b/Tests/TuistHasherTests/GraphContentHasherTests.swift @@ -0,0 +1,105 @@ +import Foundation +import Path +import TuistCore +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class GraphContentHasherTests: TuistUnitTestCase { + private var subject: GraphContentHasher! + + override func setUp() { + super.setUp() + subject = GraphContentHasher(contentHasher: ContentHasher()) + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_contentHashes_emptyGraph() throws { + // Given + let graph = Graph.test() + + // When + let hashes = try subject.contentHashes(for: graph, include: { _ in true }, additionalStrings: []) + + // Then + XCTAssertEqual(hashes, Dictionary()) + } + + func test_contentHashes_returnsOnlyFrameworks() throws { + // Given + let path: AbsolutePath = "/project" + let frameworkATarget: Target = .test( + name: "FrameworkA", + product: .framework, + infoPlist: nil, + entitlements: nil + ) + let frameworkBTarget: Target = .test( + name: "FrameworkB", + product: .framework, + infoPlist: nil, + entitlements: nil + ) + let appTarget: Target = .test( + name: "App", + product: .app, + infoPlist: nil, + entitlements: nil + ) + let dynamicLibraryTarget: Target = .test( + name: "DynamicLibrary", + product: .dynamicLibrary, + infoPlist: nil, + entitlements: nil + ) + let staticFrameworkTarget: Target = .test( + name: "StaticFramework", + product: .staticFramework, + infoPlist: nil, + entitlements: nil + ) + + let project: Project = .test( + path: path, + targets: [frameworkATarget, frameworkBTarget, appTarget, dynamicLibraryTarget, staticFrameworkTarget] + ) + let frameworkTarget = GraphTarget.test( + path: path, + target: frameworkATarget, + project: project + ) + let secondFrameworkTarget = GraphTarget.test( + path: path, + target: frameworkBTarget, + project: project + ) + let graph = Graph.test( + path: path, + projects: [project.path: project] + ) + + let expectedCachableTargets = [frameworkTarget, secondFrameworkTarget].sorted(by: { $0.target.name < $1.target.name }) + + // When + let hashes = try subject.contentHashes( + for: graph, + include: { + $0.target.product == .framework + }, + additionalStrings: [] + ) + let hashedTargets: [GraphTarget] = hashes.keys.sorted { left, right -> Bool in + left.path.pathString < right.path.pathString + } + .sorted(by: { $0.target.name < $1.target.name }) + + // Then + XCTAssertEqual(hashedTargets, expectedCachableTargets) + } +} diff --git a/Tests/TuistHasherTests/HeadersContentHasherTests.swift b/Tests/TuistHasherTests/HeadersContentHasherTests.swift new file mode 100644 index 00000000000..609456654e5 --- /dev/null +++ b/Tests/TuistHasherTests/HeadersContentHasherTests.swift @@ -0,0 +1,72 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class HeadersContentHasherTests: TuistUnitTestCase { + private var subject: HeadersContentHasher! + private var contentHasher: MockContentHashing! + private let filePath1 = try! AbsolutePath(validating: "/file1") + private let filePath2 = try! AbsolutePath(validating: "/file2") + private let filePath3 = try! AbsolutePath(validating: "/file3") + private let filePath4 = try! AbsolutePath(validating: "/file4") + private let filePath5 = try! AbsolutePath(validating: "/file5") + private let filePath6 = try! AbsolutePath(validating: "/file6") + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = HeadersContentHasher(contentHasher: contentHasher) + } + + override func tearDown() { + subject = nil + contentHasher = nil + super.tearDown() + } + + func test_hash_callsContentHasherWithTheExpectedParameters() throws { + // Given + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("1") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("2") + given(contentHasher) + .hash(path: .value(filePath3)) + .willReturn("3") + given(contentHasher) + .hash(path: .value(filePath4)) + .willReturn("4") + given(contentHasher) + .hash(path: .value(filePath5)) + .willReturn("5") + given(contentHasher) + .hash(path: .value(filePath6)) + .willReturn("6") + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + + // When + let headers = Headers( + public: [filePath1, filePath2], + private: [filePath3, filePath4], + project: [filePath5, filePath6] + ) + + // Then + let hash = try subject.hash(headers: headers) + XCTAssertEqual(hash, "1;2;3;4;5;6") + verify(contentHasher) + .hash(path: .any) + .called(6) + } +} diff --git a/Tests/TuistHasherTests/PlistContentHasherTests.swift b/Tests/TuistHasherTests/PlistContentHasherTests.swift new file mode 100644 index 00000000000..5a3ff202561 --- /dev/null +++ b/Tests/TuistHasherTests/PlistContentHasherTests.swift @@ -0,0 +1,115 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class InfoPlistContentHasherTests: TuistUnitTestCase { + private var subject: PlistContentHasher! + private var contentHasher: MockContentHashing! + private let filePath1 = try! AbsolutePath(validating: "/file1") + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = PlistContentHasher(contentHasher: contentHasher) + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 + "-hash" } + } + + override func tearDown() { + subject = nil + contentHasher = nil + super.tearDown() + } + + func test_hash_whenPlistIsFile_tellsContentHasherToHashFileContent() throws { + // Given + let infoPlist = InfoPlist.file(path: filePath1) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("stubHash") + + // When + let hash = try subject.hash(plist: .infoPlist(infoPlist)) + + // Then + verify(contentHasher) + .hash(path: .any) + .called(1) + XCTAssertEqual(hash, "stubHash") + } + + func test_hash_whenPlistIsGeneratedFile_tellsContentHasherToHashFileContent() throws { + // Given + let infoPlist = InfoPlist.generatedFile( + path: filePath1, + data: try XCTUnwrap(Data(base64Encoded: "stubHash")) + ) + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0.base64EncodedString() + "-hash" } + + // When + let hash = try subject.hash(plist: .infoPlist(infoPlist)) + + // Then + verify(contentHasher) + .hash(Parameter.any) + .called(1) + XCTAssertEqual(hash, "stubHash-hash") + } + + func test_hash_whenPlistIsDictionary_allDictionaryValuesAreConsideredForHash() throws { + // Given + let infoPlist = InfoPlist.dictionary([ + "1": 23, + "2": "foo", + "3": true, + "4": false, + "5": ["5a", "5b"], + "6": ["6a": "6value"], + ]) + // When + let hash = try subject.hash(plist: .infoPlist(infoPlist)) + + // Then + verify(contentHasher) + .hash(Parameter.any) + .called(1) + XCTAssertEqual( + hash, + "1=integer(23);2=string(\"foo\");3=boolean(true);4=boolean(false);5=array([XcodeGraph.Plist.Value.string(\"5a\"), XcodeGraph.Plist.Value.string(\"5b\")]);6=dictionary([\"6a\": XcodeGraph.Plist.Value.string(\"6value\")]);-hash" + ) + } + + func test_hash_whenPlistIsExtendingDefault_allDictionaryValuesAreConsideredForHash() throws { + // Given + let infoPlist = InfoPlist.extendingDefault(with: [ + "1": 23, + "2": "foo", + "3": true, + "4": false, + "5": ["5a", "5b"], + "6": ["6a": "6value"], + ]) + + // When + let hash = try subject.hash(plist: .infoPlist(infoPlist)) + + // Then + verify(contentHasher) + .hash(Parameter.any) + .called(1) + XCTAssertEqual( + hash, + "1=integer(23);2=string(\"foo\");3=boolean(true);4=boolean(false);5=array([XcodeGraph.Plist.Value.string(\"5a\"), XcodeGraph.Plist.Value.string(\"5b\")]);6=dictionary([\"6a\": XcodeGraph.Plist.Value.string(\"6value\")]);-hash" + ) + } +} diff --git a/Tests/TuistHasherTests/PrivacyManifestContentHasherTests.swift b/Tests/TuistHasherTests/PrivacyManifestContentHasherTests.swift new file mode 100644 index 00000000000..71eb732a4a5 --- /dev/null +++ b/Tests/TuistHasherTests/PrivacyManifestContentHasherTests.swift @@ -0,0 +1,41 @@ +import Foundation +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistHasher + +final class PrivacyManifestContentHasherTests: TuistUnitTestCase { + private var subject: PrivacyManifestContentHasher! + + override func setUp() { + super.setUp() + subject = PrivacyManifestContentHasher(contentHasher: ContentHasher()) + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_hash_isDeterministic() throws { + // Given + let privacyManifest = PrivacyManifest( + tracking: true, + trackingDomains: ["io.tuist"], + collectedDataTypes: [["test": .string("tuist")]], + accessedApiTypes: [["test": .string("tuist")]] + ) + var results: Set = Set() + + // When + for _ in 0 ... 100 { + results.insert(try subject.hash(privacyManifest)) + } + + // Then + XCTAssertEqual(results.count, 1) + } +} diff --git a/Tests/TuistHasherTests/ResourcesContentHasherTests.swift b/Tests/TuistHasherTests/ResourcesContentHasherTests.swift new file mode 100644 index 00000000000..6437f499d2e --- /dev/null +++ b/Tests/TuistHasherTests/ResourcesContentHasherTests.swift @@ -0,0 +1,116 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class ResourcesContentHasherTests: TuistUnitTestCase { + private var subject: ResourcesContentHasher! + private var contentHasher: MockContentHashing! + private let filePath1 = try! AbsolutePath(validating: "/file1") + private let filePath2 = try! AbsolutePath(validating: "/file2") + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = ResourcesContentHasher(contentHasher: contentHasher) + + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + } + + override func tearDown() { + subject = nil + contentHasher = nil + super.tearDown() + } + + // MARK: - Tests + + func test_hash_callsContentHasherWithTheExpectedParameter() throws { + // Given + let file1 = ResourceFileElement.file(path: filePath1) + let file2 = ResourceFileElement.file(path: filePath2) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("1") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("2") + + // When + let hash = try subject.hash(resources: .init([file1, file2])) + + // Then + verify(contentHasher) + .hash(path: .any) + .called(2) + XCTAssertEqual(hash, "1;2") + } + + func test_hash_includesFolderReference() throws { + // Given + let file1 = ResourceFileElement.file(path: filePath1) + let file2 = ResourceFileElement.folderReference(path: filePath2) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("1") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("2") + + // When + let hash = try subject.hash(resources: .init([file1, file2])) + + // Then + verify(contentHasher) + .hash(path: .any) + .called(2) + XCTAssertEqual(hash, "1;2") + } + + func test_hash_sortsTheResourcesBeforeCalculatingTheHash() throws { + // Given + let file1 = ResourceFileElement.file(path: filePath1) + let file2 = ResourceFileElement.folderReference(path: filePath2) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("1") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("2") + + // When/Then + XCTAssertEqual(try subject.hash(resources: .init([file1, file2])), try subject.hash(resources: .init([file2, file1]))) + } + + func test_hash_hashesThePrivacyManifestToo() throws { + // Given + let file1 = ResourceFileElement.file(path: filePath1) + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("1") + given(contentHasher) + .hash(path: .value(filePath2)) + .willReturn("2") + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 } + + let resources = ResourceFileElements([file1, file1], privacyManifest: PrivacyManifest( + tracking: true, + trackingDomains: ["io.tuist"], + collectedDataTypes: [["test": .string("tuist")]], + accessedApiTypes: [["test": .string("tuist")]] + )) + + // When/Then + XCTAssertEqual(try subject.hash(resources: resources), try subject.hash(resources: resources)) + } +} diff --git a/Tests/TuistHasherTests/SettingsContentHasherTests.swift b/Tests/TuistHasherTests/SettingsContentHasherTests.swift new file mode 100644 index 00000000000..74969eb1163 --- /dev/null +++ b/Tests/TuistHasherTests/SettingsContentHasherTests.swift @@ -0,0 +1,84 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class SettingsContentHasherTests: TuistUnitTestCase { + private var subject: SettingsContentHasher! + private var contentHasher: MockContentHashing! + private let filePath1 = try! AbsolutePath(validating: "/file1") + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = SettingsContentHasher(contentHasher: contentHasher) + + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 + "-hash" } + } + + override func tearDown() { + subject = nil + contentHasher = nil + super.tearDown() + } + + // MARK: - Tests + + func test_hash_whenRecommended_withXCConfig_callsContentHasherWithExpectedStrings() throws { + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("xconfigHash") + + // Given + let settings = Settings( + base: ["CURRENT_PROJECT_VERSION": SettingValue.string("1")], + configurations: [ + BuildConfiguration + .debug("dev"): Configuration(settings: ["SWIFT_VERSION": SettingValue.string("5")], xcconfig: filePath1), + ], + defaultSettings: .recommended + ) + + // When + let hash = try subject.hash(settings: settings) + + // Then + XCTAssertEqual( + hash, + "CURRENT_PROJECT_VERSION:string(\"1\")-hash;devdebugSWIFT_VERSION:string(\"5\")-hashxconfigHash;recommended" + ) + } + + func test_hash_whenEssential_withoutXCConfig_callsContentHasherWithExpectedStrings() throws { + given(contentHasher) + .hash(path: .value(filePath1)) + .willReturn("xconfigHash") + + // Given + let settings = Settings( + base: ["CURRENT_PROJECT_VERSION": SettingValue.string("2")], + configurations: [ + BuildConfiguration + .release("prod"): Configuration(settings: ["SWIFT_VERSION": SettingValue.string("5")], xcconfig: nil), + ], + defaultSettings: .essential + ) + + // When + let hash = try subject.hash(settings: settings) + + // Then + XCTAssertEqual(hash, "CURRENT_PROJECT_VERSION:string(\"2\")-hash;prodreleaseSWIFT_VERSION:string(\"5\")-hash;essential") + } +} diff --git a/Tests/TuistHasherTests/SourceFilesContentHasherTests.swift b/Tests/TuistHasherTests/SourceFilesContentHasherTests.swift new file mode 100644 index 00000000000..8808b3943ec --- /dev/null +++ b/Tests/TuistHasherTests/SourceFilesContentHasherTests.swift @@ -0,0 +1,84 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class SourceFilesContentHasherTests: TuistUnitTestCase { + private var subject: SourceFilesContentHasher! + private var contentHasher: MockContentHashing! + private let sourceFile1Path = try! AbsolutePath(validating: "/file1") + private let sourceFile2Path = try! AbsolutePath(validating: "/file2") + private var sourceFile1: SourceFile! + private var sourceFile2: SourceFile! + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = SourceFilesContentHasher(contentHasher: contentHasher) + sourceFile1 = SourceFile(path: sourceFile1Path, compilerFlags: "-fno-objc-arc") + sourceFile2 = SourceFile(path: sourceFile2Path, compilerFlags: "-print-objc-runtime-info") + + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 + "-hash" } + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + } + + override func tearDown() { + subject = nil + contentHasher = nil + sourceFile1 = nil + sourceFile2 = nil + super.tearDown() + } + + // MARK: - Tests + + func test_hash_when_the_files_have_a_hash() throws { + // When + sourceFile1 = SourceFile(path: sourceFile1Path, contentHash: "first") + sourceFile2 = SourceFile(path: sourceFile2Path, contentHash: "second") + let hash = try subject.hash(sources: [sourceFile1, sourceFile2]) + + // Then + XCTAssertEqual(hash, "first;second") + } + + func test_hash_returnsSameValue() throws { + // When + given(contentHasher) + .hash(path: .value(sourceFile1Path)) + .willReturn("") + given(contentHasher) + .hash(path: .value(sourceFile2Path)) + .willReturn("") + let hash = try subject.hash(sources: [sourceFile1, sourceFile2]) + + // Then + XCTAssertEqual(hash, "-fno-objc-arc-hash;-print-objc-runtime-info-hash") + } + + func test_hash_includesFileContentHashAndCompilerFlags() throws { + // Given + given(contentHasher) + .hash(path: .value(sourceFile1Path)) + .willReturn("file1-content-hash") + given(contentHasher) + .hash(path: .value(sourceFile2Path)) + .willReturn("file2-content-hash") + + // When + let hash = try subject.hash(sources: [sourceFile1, sourceFile2]) + + // Then + XCTAssertEqual(hash, "file1-content-hash-fno-objc-arc-hash;file2-content-hash-print-objc-runtime-info-hash") + } +} diff --git a/Tests/TuistHasherTests/TargetScriptsContentHasherTests.swift b/Tests/TuistHasherTests/TargetScriptsContentHasherTests.swift new file mode 100644 index 00000000000..b1761c661e0 --- /dev/null +++ b/Tests/TuistHasherTests/TargetScriptsContentHasherTests.swift @@ -0,0 +1,224 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistHasher + +final class TargetScriptsContentHasherTests: TuistUnitTestCase { + private var subject: TargetScriptsContentHasher! + private var contentHasher: MockContentHashing! + private var temporaryDirectory: TemporaryDirectory! + + override func setUp() { + super.setUp() + contentHasher = .init() + subject = TargetScriptsContentHasher(contentHasher: contentHasher) + do { + temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) + } catch { + XCTFail("Error while creating temporary directory") + } + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 + "-hash" } + given(contentHasher) + .hash(Parameter<[String]>.any) + .willProduce { $0.joined(separator: ";") } + } + + override func tearDown() { + subject = nil + temporaryDirectory = nil + contentHasher = nil + + super.tearDown() + } + + private func makeTargetScript( + name: String = "1", + order: TargetScript.Order = .pre, + tool: String = "tool1", + arguments: [String] = ["arg1", "arg2"], + inputPaths: [AbsolutePath] = [try! AbsolutePath(validating: "/inputPaths1")], + inputFileListPaths: [AbsolutePath] = [try! AbsolutePath(validating: "/inputFileListPaths1")], + outputPaths: [AbsolutePath] = [try! AbsolutePath(validating: "/outputPaths1")], + outputFileListPaths: [AbsolutePath] = [try! AbsolutePath(validating: "/outputFileListPaths1")], + dependencyFile: AbsolutePath = try! AbsolutePath(validating: "/dependencyFile1") + ) -> TargetScript { + TargetScript( + name: name, + order: order, + script: .tool(path: tool, args: arguments), + inputPaths: inputPaths.map(\.pathString), + inputFileListPaths: inputFileListPaths, + outputPaths: outputPaths.map(\.pathString), + outputFileListPaths: outputFileListPaths, + dependencyFile: dependencyFile + ) + } + + // MARK: - Tests + + func test_hash_targetAction_withBuildVariables_callsMockHasherWithOnlyPathWithoutBuildVariable() throws { + // Given + let inputFileListPaths1 = "inputFileListPaths1-hash" + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputFileListPaths1"))) + .willReturn(inputFileListPaths1) + let targetScript = makeTargetScript( + inputPaths: [try AbsolutePath(validating: "/$(SRCROOT)/inputPaths1")], + inputFileListPaths: [try AbsolutePath(validating: "/inputFileListPaths1")], + outputPaths: [try AbsolutePath(validating: "/$(DERIVED_FILE_DIR)/outputPaths1")], + outputFileListPaths: [try AbsolutePath(validating: "/outputFileListPaths1")], + dependencyFile: try AbsolutePath(validating: "/$(DERIVED_FILE_DIR)/file.d") + ) + + // When + _ = try subject.hash(targetScripts: [targetScript], sourceRootPath: "/") + + // Then + let expected = [ + "$(SRCROOT)/inputPaths1", + "$(DERIVED_FILE_DIR)/file.d", + inputFileListPaths1, + "$(DERIVED_FILE_DIR)/outputPaths1", + "outputFileListPaths1", + "1", + "tool1", + "pre", + "arg1", + "arg2", + ] + + verify(contentHasher) + .hash(.value(expected)) + .called(1) + } + + func test_hash_targetAction_callsMockHasherWithExpectedStrings() throws { + // Given + let inputPaths1Hash = "inputPaths1-hash" + let inputFileListPaths1 = "inputFileListPaths1-hash" + let dependencyFileHash = "dependencyFile1-hash" + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputPaths1"))) + .willReturn(inputPaths1Hash) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputFileListPaths1"))) + .willReturn(inputFileListPaths1) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/dependencyFile1"))) + .willReturn(dependencyFileHash) + let targetScript = makeTargetScript() + + // When + _ = try subject.hash(targetScripts: [targetScript], sourceRootPath: "/") + + // Then + let expected = [ + inputPaths1Hash, + inputFileListPaths1, + dependencyFileHash, + "outputPaths1", + "outputFileListPaths1", + "1", + "tool1", + "pre", + "arg1", + "arg2", + ] + verify(contentHasher) + .hash(.value(expected)) + .called(1) + } + + func test_hash_targetAction_when_path_nil_callsMockHasherWithExpectedStrings() throws { + // Given + let inputPaths1Hash = "inputPaths1-hash" + let inputFileListPaths1 = "inputFileListPaths1-hash" + let dependencyFileHash = "dependencyFile1-hash" + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputPaths1"))) + .willReturn(inputPaths1Hash) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputFileListPaths1"))) + .willReturn(inputFileListPaths1) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/dependencyFile1"))) + .willReturn(dependencyFileHash) + + let targetScript = makeTargetScript() + + // When + _ = try subject.hash(targetScripts: [targetScript], sourceRootPath: "/") + + // Then + let expected = [ + inputPaths1Hash, + inputFileListPaths1, + dependencyFileHash, + "outputPaths1", + "outputFileListPaths1", + "1", + "tool1", + "pre", + "arg1", + "arg2", + ] + verify(contentHasher) + .hash(.value(expected)) + .called(1) + } + + func test_hash_targetAction_valuesAreNotHarcoded() throws { + // Given + let inputPaths2Hash = "inputPaths2-hash" + let inputFileListPaths2 = "inputFileListPaths2-hash" + let dependencyFileHash = "/dependencyFilePath4-hash" + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputPaths2"))) + .willReturn(inputPaths2Hash) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/inputFileListPaths2"))) + .willReturn(inputFileListPaths2) + given(contentHasher) + .hash(path: .value(try AbsolutePath(validating: "/dependencyFilePath4"))) + .willReturn(dependencyFileHash) + + let targetScript = makeTargetScript( + name: "2", + order: .post, + tool: "tool2", + inputPaths: [try AbsolutePath(validating: "/inputPaths2")], + inputFileListPaths: [try AbsolutePath(validating: "/inputFileListPaths2")], + outputPaths: [try AbsolutePath(validating: "/outputPaths2")], + outputFileListPaths: [try AbsolutePath(validating: "/outputFileListPaths2")], + dependencyFile: try AbsolutePath(validating: "/dependencyFilePath4") + ) + + // When + _ = try subject.hash(targetScripts: [targetScript], sourceRootPath: "/") + + // Then + let expected = [ + inputPaths2Hash, + inputFileListPaths2, + dependencyFileHash, + "outputPaths2", + "outputFileListPaths2", + "2", + "tool2", + "post", + "arg1", + "arg2", + ] + verify(contentHasher) + .hash(.value(expected)) + .called(1) + } +} diff --git a/Tests/TuistAcceptanceTests/BuildRulesAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/BuildRulesAcceptanceTests.swift similarity index 91% rename from Tests/TuistAcceptanceTests/BuildRulesAcceptanceTests.swift rename to Tests/TuistKitAcceptanceTests/BuildRulesAcceptanceTests.swift index 64b09951dd8..777aca1562b 100644 --- a/Tests/TuistAcceptanceTests/BuildRulesAcceptanceTests.swift +++ b/Tests/TuistKitAcceptanceTests/BuildRulesAcceptanceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistAcceptanceTesting import TuistSupport import TuistSupportTesting @@ -7,7 +7,7 @@ import XCTest final class BuildRulesAcceptanceTestAppWithBuildRules: TuistAcceptanceTestCase { func test_app_with_build_rules() async throws { - try setUpFixture(.appWithBuildRules) + try await setUpFixture(.appWithBuildRules) try await run(GenerateCommand.self) try await run(BuildCommand.self) let xcodeproj = try XcodeProj(pathString: xcodeprojPath.pathString) diff --git a/Tests/TuistKitAcceptanceTests/CacheAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/CacheAcceptanceTests.swift new file mode 100644 index 00000000000..b529c8ed8ba --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/CacheAcceptanceTests.swift @@ -0,0 +1,17 @@ +import Foundation +import TuistAcceptanceTesting + +final class CacheAcceptanceTestiOSAppWithFrameworks: TuistAcceptanceTestCase { + func test_ios_app_with_frameworks() async throws { + try await setUpFixture(.iosAppWithFrameworks) + try await run(CacheCommand.self, "--print-hashes") + XCTAssertStandardOutput(pattern: """ + Framework1 - 31b5dd46503cc78a7d84514b3a59e462 + Framework2-iOS - 75676dc68658f4996630a6eb070f3072 + Framework2-macOS - 965232ae7d0ef41aa39a5556dae4dc50 + Framework3 - 8e0d6b1a1fc70c0b9f27c41e1b0669d5 + Framework4 - 3657f4ec388cc692d6f18ed906838d71 + Framework5 - daf3deb97fcd2aef89d7f417bdccac0b + """) + } +} diff --git a/Tests/TuistKitAcceptanceTests/EditAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/EditAcceptanceTests.swift new file mode 100644 index 00000000000..53f359acc22 --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/EditAcceptanceTests.swift @@ -0,0 +1,66 @@ +import Path +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class EditAcceptanceTestiOSAppWithHelpers: TuistAcceptanceTestCase { + func test_ios_app_with_helpers() async throws { + try await setUpFixture(.iosAppWithHelpers) + try await run(EditCommand.self) + try build(scheme: "Manifests") + } +} + +final class EditAcceptanceTestPlugin: TuistAcceptanceTestCase { + func test_plugin() async throws { + try await setUpFixture(.plugin) + try await run(EditCommand.self) + try build(scheme: "Plugins") + } +} + +final class EditAcceptanceTestAppWithPlugins: TuistAcceptanceTestCase { + func test_app_with_plugins() async throws { + try await setUpFixture(.appWithPlugins) + try await run(InstallCommand.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 await setUpFixture(.appWithSpmDependencies) + try await run(EditCommand.self) + try build(scheme: "Manifests") + } +} + +final class EditAcceptanceTestSPMPackage: TuistAcceptanceTestCase { + func test_spm_package() async throws { + try await setUpFixture(.spmPackage) + 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/TuistKitAcceptanceTests/GraphAcceptanceTests.swift similarity index 91% rename from Tests/TuistAcceptanceTests/GraphAcceptanceTests.swift rename to Tests/TuistKitAcceptanceTests/GraphAcceptanceTests.swift index 1c35777faf2..9584dc396ec 100644 --- a/Tests/TuistAcceptanceTests/GraphAcceptanceTests.swift +++ b/Tests/TuistKitAcceptanceTests/GraphAcceptanceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistAcceptanceTesting import TuistSupport import TuistSupportTesting @@ -8,7 +8,7 @@ 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 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( diff --git a/Tests/TuistKitAcceptanceTests/InitAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/InitAcceptanceTests.swift new file mode 100644 index 00000000000..e4166e507c0 --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/InitAcceptanceTests.swift @@ -0,0 +1,46 @@ +import Path +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class InitAcceptanceTestmacOSApp: TuistAcceptanceTestCase { + func test_init_macos_app() async throws { + try await run(InitCommand.self, "--platform", "macos", "--name", "Test") + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class InitAcceptanceTestiOSApp: TuistAcceptanceTestCase { + func test_init_ios_app() async throws { + try await run(InitCommand.self, "--platform", "ios", "--name", "My-App") + try await run(InstallCommand.self) + try await run(GenerateCommand.self) + 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 InitAcceptanceTestCLIProjectWithTemplateInADifferentRepository: TuistAcceptanceTestCase { + func test_cli_project_with_template_in_a_different_repository() async throws { + try await run( + InitCommand.self, + "--template", + "https://github.com/tuist/ExampleTuistTemplate-Tuist4.git", + "--name", + "MyApp" + ) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} diff --git a/Tests/TuistAcceptanceTests/ListTargetsAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/ListTargetsAcceptanceTests.swift similarity index 92% rename from Tests/TuistAcceptanceTests/ListTargetsAcceptanceTests.swift rename to Tests/TuistKitAcceptanceTests/ListTargetsAcceptanceTests.swift index 370d3d426ac..ddc5546dc4f 100644 --- a/Tests/TuistAcceptanceTests/ListTargetsAcceptanceTests.swift +++ b/Tests/TuistKitAcceptanceTests/ListTargetsAcceptanceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistAcceptanceTesting import TuistSupport import TuistSupportTesting @@ -7,7 +7,7 @@ import XCTest final class ListTargetsAcceptanceTestiOSWorkspaceWithMicrofeatureArchitecture: TuistAcceptanceTestCase { func test_ios_workspace_with_microfeature_architecture() async throws { - try setUpFixture(.iosWorkspaceWithMicrofeatureArchitecture) + try await setUpFixture(.iosWorkspaceWithMicrofeatureArchitecture) try await run(GenerateCommand.self) try listTargets(for: "UIComponents") try listTargets(for: "Core") diff --git a/Tests/TuistAcceptanceTests/PluginAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/PluginAcceptanceTests.swift similarity index 78% rename from Tests/TuistAcceptanceTests/PluginAcceptanceTests.swift rename to Tests/TuistKitAcceptanceTests/PluginAcceptanceTests.swift index bf712ce142c..c92ba36ef6c 100644 --- a/Tests/TuistAcceptanceTests/PluginAcceptanceTests.swift +++ b/Tests/TuistKitAcceptanceTests/PluginAcceptanceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistAcceptanceTesting import TuistSupport import TuistSupportTesting @@ -7,7 +7,7 @@ import XCTest final class PluginAcceptanceTestTuistPlugin: TuistAcceptanceTestCase { func test_tuist_plugin() async throws { - try setUpFixture(.tuistPlugin) + try await setUpFixture(.tuistPlugin) try run(PluginBuildCommand.self) try run(PluginRunCommand.self, "tuist-create-file") } @@ -15,8 +15,8 @@ final class PluginAcceptanceTestTuistPlugin: TuistAcceptanceTestCase { final class PluginAcceptanceTestAppWithPlugins: TuistAcceptanceTestCase { func test_app_with_plugins() async throws { - try setUpFixture(.appWithPlugins) - try await run(FetchCommand.self) + try await setUpFixture(.appWithPlugins) + try await run(InstallCommand.self) try await run(GenerateCommand.self) try await run(BuildCommand.self) } diff --git a/Tests/TuistAcceptanceTests/PrecompiledAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/PrecompiledAcceptanceTests.swift similarity index 91% rename from Tests/TuistAcceptanceTests/PrecompiledAcceptanceTests.swift rename to Tests/TuistKitAcceptanceTests/PrecompiledAcceptanceTests.swift index d205ec5e50a..780193e09af 100644 --- a/Tests/TuistAcceptanceTests/PrecompiledAcceptanceTests.swift +++ b/Tests/TuistKitAcceptanceTests/PrecompiledAcceptanceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistAcceptanceTesting import TuistSupport import TuistSupportTesting @@ -7,7 +7,7 @@ import XCTest final class PrecomiledAcceptanceTestiOSAppWithStaticFrameworks: TuistAcceptanceTestCase { func test_ios_app_with_static_frameworks() async throws { - try setUpFixture(.iosAppWithStaticFrameworks) + try await setUpFixture(.iosAppWithStaticFrameworks) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -15,7 +15,7 @@ final class PrecomiledAcceptanceTestiOSAppWithStaticFrameworks: TuistAcceptanceT final class PrecomiledAcceptanceTestiOSAppWithStaticLibraries: TuistAcceptanceTestCase { func test_ios_app_with_static_libraries() async throws { - try setUpFixture(.iosAppWithStaticLibraries) + try await setUpFixture(.iosAppWithStaticLibraries) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -23,12 +23,12 @@ final class PrecomiledAcceptanceTestiOSAppWithStaticLibraries: TuistAcceptanceTe final class PrecomiledAcceptanceTestiOSAppWithTransitiveFramework: TuistAcceptanceTestCase { func test_ios_app_with_transitive_framework() async throws { - try setUpFixture(.iosAppWithTransitiveFramework) + try await setUpFixture(.iosAppWithTransitiveFramework) try await run(GenerateCommand.self) try await run(BuildCommand.self, "App", "--platform", "iOS") try await XCTAssertProductWithDestinationContainsFrameworkWithArchitecture( framework: "Framework1", - architecture: "x86_64" + architecture: "arm64" ) try XCTAssertProductWithDestinationDoesNotContainHeaders( "App.app", @@ -44,7 +44,7 @@ final class PrecomiledAcceptanceTestiOSAppWithTransitiveFramework: TuistAcceptan final class PrecompiledAcceptanceTestiOSAppWithStaticLibraryAndPackage: TuistAcceptanceTestCase { func test_ios_app_with_static_library_and_package() async throws { - try setUpFixture(.iosAppWithStaticLibraryAndPackage) + try await setUpFixture(.iosAppWithStaticLibraryAndPackage) try await run(GenerateCommand.self) try await run(BuildCommand.self) } @@ -52,7 +52,7 @@ final class PrecompiledAcceptanceTestiOSAppWithStaticLibraryAndPackage: TuistAcc final class PrecompiledAcceptanceTestiOSAppWithXCFrameworks: TuistAcceptanceTestCase { func test_ios_app_with_xcframeworks() async throws { - try setUpFixture(.iosAppWithXcframeworks) + try await setUpFixture(.iosAppWithXcframeworks) try await run(GenerateCommand.self) try await run(BuildCommand.self) try await XCTAssertProductWithDestinationContainsFrameworkWithArchitecture( diff --git a/Tests/TuistKitAcceptanceTests/ProjectAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/ProjectAcceptanceTests.swift new file mode 100644 index 00000000000..77b3f9db73c --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/ProjectAcceptanceTests.swift @@ -0,0 +1,61 @@ +import Foundation +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistKit +@testable import TuistServer + +final class ProjectAcceptanceTestProjects: ServerAcceptanceTestCase { + func test_list_project() async throws { + try await setUpFixture(.iosAppWithFrameworks) + try await run(ProjectListCommand.self) + XCTAssertStandardOutput(pattern: "Listing all your projects:") + XCTAssertStandardOutput(pattern: "• \(fullHandle)") + } +} + +final class ProjectAcceptanceTestProjectTokens: ServerAcceptanceTestCase { + func test_create_list_and_revoke_project_token() async throws { + try await setUpFixture(.iosAppWithFrameworks) + try await run(ProjectTokensCreateCommand.self, fullHandle) + TestingLogHandler.reset() + try await run(ProjectTokensListCommand.self, fullHandle) + let id = try XCTUnwrap( + TestingLogHandler.collected[.info, <=] + .components(separatedBy: .newlines) + .dropLast().last? + .components(separatedBy: .whitespaces) + .first + ) + try await run(ProjectTokensRevokeCommand.self, id, fullHandle) + TestingLogHandler.reset() + try await run(ProjectTokensListCommand.self, fullHandle) + XCTAssertStandardOutput( + pattern: "No project tokens found. Create one by running `tuist project tokens create \(fullHandle)." + ) + } +} + +final class ProjectAcceptanceTestProjectDefaultBranch: ServerAcceptanceTestCase { + func test_update_default_branch() async throws { + try await setUpFixture(.iosAppWithFrameworks) + try await run(ProjectShowCommand.self, fullHandle) + XCTAssertStandardOutput( + pattern: """ + Full handle: \(fullHandle) + Default branch: main + """ + ) + try await run(ProjectUpdateCommand.self, fullHandle, "--default-branch", "new-default-branch") + TestingLogHandler.reset() + try await run(ProjectShowCommand.self, fullHandle) + XCTAssertStandardOutput( + pattern: """ + Full handle: \(fullHandle) + Default branch: new-default-branch + """ + ) + } +} diff --git a/Tests/TuistKitAcceptanceTests/RunAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/RunAcceptanceTests.swift new file mode 100644 index 00000000000..8088a107e8b --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/RunAcceptanceTests.swift @@ -0,0 +1,14 @@ +import Path +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class RunAcceptanceTestCommandLineToolBasic: TuistAcceptanceTestCase { + func test_command_line_tool_basic() async throws { + try await setUpFixture(.commandLineToolBasic) + try await run(InstallCommand.self) + try await run(RunCommand.self, "CommandLineTool") + } +} diff --git a/Tests/TuistKitAcceptanceTests/ScaffoldAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/ScaffoldAcceptanceTests.swift new file mode 100644 index 00000000000..225ba069dea --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/ScaffoldAcceptanceTests.swift @@ -0,0 +1,210 @@ +import Path +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 await setUpFixture(.iosAppWithTemplates) + try await run(InstallCommand.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 await setUpFixture(.iosAppWithTemplates) + try await run(InstallCommand.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 await setUpFixture(.iosAppWithTemplates) + try await run(InstallCommand.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 await setUpFixture(.appWithPlugins) + try await run(InstallCommand.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 await setUpFixture(.appWithPlugins) + try await run(InstallCommand.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 + + """ + ) + } + + func test_ios_app_with_templates_custom_using_attribute() async throws { + try await setUpFixture(.iosAppWithTemplates) + try await run(InstallCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "custom_using_attribute", + "--name", + "TemplateProject", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "custom_using_attribute", "--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 name: TemplateProject + // Generated file with supporting platforms + // iOS + + """ + ) + } + + func test_ios_app_with_local_template_and_project_description_helpers_plugin() async throws { + try await setUpFixture(.iosAppWithPluginsAndTemplates) + try await run(InstallCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "example", + "--name", + "PluginAndTemplate", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "example") + let pluginTemplateDirectory = fixturePath.appending(component: "Sources") + XCTAssertEqual( + try FileHandler.shared.readTextFile(pluginTemplateDirectory.appending(component: "LocalTemplateTest.swift")), + "// Generated file named LocalPlugin from local template" + ) + } + + func test_ios_app_with_plugin_template_and_project_description_helpers_plugin() async throws { + try await setUpFixture(.iosAppWithPluginsAndTemplates) + try await run(InstallCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "plugin", + "--name", + "PluginAndTemplate", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "plugin") + let pluginTemplateDirectory = fixturePath.appending(component: "Sources") + XCTAssertEqual( + try FileHandler.shared.readTextFile(pluginTemplateDirectory.appending(component: "PluginTemplateTest.swift")), + "// Generated file named LocalPlugin from plugin" + ) + } +} diff --git a/Tests/TuistKitAcceptanceTests/ShareAcceptanceTests.swift b/Tests/TuistKitAcceptanceTests/ShareAcceptanceTests.swift new file mode 100644 index 00000000000..b41a0fade5c --- /dev/null +++ b/Tests/TuistKitAcceptanceTests/ShareAcceptanceTests.swift @@ -0,0 +1,89 @@ +import Foundation +import TuistAcceptanceTesting +import TuistCore +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistKit +@testable import TuistServer + +final class ShareAcceptanceTests: ServerAcceptanceTestCase { + func test_share_ios_app_with_frameworks() async throws { + try await setUpFixture(.iosAppWithFrameworks) + try await run(BuildCommand.self, "App") + try await run(ShareCommand.self) + let shareLink = try previewLink() + try await run(RunCommand.self, shareLink, "-destination", "iPhone 15 Pro") + XCTAssertStandardOutput(pattern: "Installing and launching App on iPhone 15 Pro") + XCTAssertStandardOutput(pattern: "App was successfully launched 📲") + } + + func test_share_xcode_app() async throws { + try await setUpFixture(.xcodeApp) + try System.shared.runAndPrint( + [ + "/usr/bin/xcrun", + "xcodebuild", + "clean", + "build", + "-project", + fixturePath.appending(component: "App.xcodeproj").pathString, + "-scheme", + "App", + "-sdk", + "iphonesimulator", + "-derivedDataPath", + derivedDataPath.pathString, + ] + ) + try await run(ShareCommand.self, "App", "--platforms", "ios") + try await run(RunCommand.self, try previewLink(), "-destination", "iPhone 15 Plus") + XCTAssertStandardOutput(pattern: "Installing and launching App on iPhone 15 Plus") + XCTAssertStandardOutput(pattern: "App was successfully launched 📲") + } + + func test_share_xcode_app_files() async throws { + try await setUpFixture(.xcodeApp) + let buildDirectory = fixturePath.appending(component: "Build") + try System.shared.runAndPrint( + [ + "/usr/bin/xcrun", + "xcodebuild", + "clean", + "build", + "-project", + fixturePath.appending(component: "App.xcodeproj").pathString, + "-scheme", + "App", + "-sdk", + "iphonesimulator", + "-derivedDataPath", + derivedDataPath.pathString, + "CONFIGURATION_BUILD_DIR=\(buildDirectory)", + ] + ) + + // Testing sharing `.app` file directly + try await run( + ShareCommand.self, + buildDirectory.appending(component: "App.app").pathString, + "--platforms", "ios" + ) + try await run(RunCommand.self, try previewLink(), "-destination", "iPhone 15 Pro Max") + XCTAssertStandardOutput(pattern: "Installing and launching App on iPhone 15 Pro Max") + XCTAssertStandardOutput(pattern: "App was successfully launched 📲") + } +} + +extension ServerAcceptanceTestCase { + fileprivate func previewLink() throws -> String { + try XCTUnwrap( + TestingLogHandler.collected[.notice, >=] + .components(separatedBy: .newlines) + .first(where: { $0.contains("App uploaded – share") })? + .components(separatedBy: .whitespaces) + .last + ) + } +} diff --git a/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift b/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift index f40ea0fccfc..1f8071d2e6d 100644 --- a/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift +++ b/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest @@ -148,8 +148,7 @@ final class DumpServiceTests: TuistTestCase { let config = Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, + fullHandle: "tuist/tuist", swiftVersion: nil, plugins: [], generationOptions: .options() @@ -169,9 +168,11 @@ final class DumpServiceTests: TuistTestCase { } }, + "fullHandle": "tuist/tuist", "generationOptions": { "disablePackageVersionLocking": false, "enforceExplicitDependencies": false, + "optionalAuthentication": false, "resolveDependenciesWithSystemScm": false, "staticSideEffectsWarningTargets": { "all": { @@ -181,7 +182,8 @@ final class DumpServiceTests: TuistTestCase { }, "plugins": [ - ] + ], + "url": "https://cloud.tuist.io" } """ @@ -247,31 +249,130 @@ final class DumpServiceTests: TuistTestCase { XCTAssertPrinterOutputContains(expected) } - func test_prints_the_manifest_when_dependencies_manifest() async throws { + func test_prints_the_manifest_when_package_manifest() async throws { let tmpDir = try temporaryPath() let config = """ + // swift-tools-version: 5.9 + import PackageDescription + + #if TUIST import ProjectDescription - let dependencies = Dependencies( - carthage: nil, - swiftPackageManager: nil, - platforms: [] + let packageSettings = PackageSettings( + targetSettings: ["TargetA": ["OTHER_LDFLAGS": "-ObjC"]] ) + + #endif + + let package = Package( + name: "PackageName", + dependencies: [] + ) + """ - try fileHandler.createFolder(tmpDir.appending(component: "Tuist")) + try fileHandler.createFolder(tmpDir.appending(component: Constants.tuistDirectoryName)) try config.write( - toFile: tmpDir.appending(components: "Tuist", "Dependencies.swift").pathString, + toFile: tmpDir.appending( + component: Constants.SwiftPackageManager.packageSwiftName + ).pathString, atomically: true, encoding: .utf8 ) - try await subject.run(path: tmpDir.pathString, manifest: .dependencies) + try await subject.run(path: tmpDir.pathString, manifest: .package) let expected = """ { - "platforms": [ + "baseSettings": { + "base": { - ] - } + }, + "configurations": [ + { + "name": { + "rawValue": "Debug" + }, + "settings": { + }, + "variant": "debug" + }, + { + "name": { + "rawValue": "Release" + }, + "settings": { + + }, + "variant": "release" + } + ], + "defaultSettings": { + "recommended": { + "excluding": [ + + ] + } + } + }, + """ + + XCTAssertPrinterOutputContains(expected) + } + + func test_prints_the_manifest_when_package_manifest_without_package_settings() async throws { + let tmpDir = try temporaryPath() + let config = """ + // swift-tools-version: 5.9 + import PackageDescription + + let package = Package( + name: "PackageName", + dependencies: [] + ) + + """ + try fileHandler.createFolder(tmpDir.appending(component: Constants.tuistDirectoryName)) + try config.write( + toFile: tmpDir.appending( + component: Constants.SwiftPackageManager.packageSwiftName + ).pathString, + atomically: true, + encoding: .utf8 + ) + try await subject.run(path: tmpDir.pathString, manifest: .package) + let expected = """ + { + "baseSettings": { + "base": { + + }, + "configurations": [ + { + "name": { + "rawValue": "Debug" + }, + "settings": { + + }, + "variant": "debug" + }, + { + "name": { + "rawValue": "Release" + }, + "settings": { + + }, + "variant": "release" + } + ], + "defaultSettings": { + "recommended": { + "excluding": [ + + ] + } + } + }, """ XCTAssertPrinterOutputContains(expected) @@ -293,10 +394,6 @@ final class DumpServiceTests: TuistTestCase { try await assertLoadingRaisesWhenManifestNotFound(manifest: .template) } - func test_run_throws_when_dependencies_and_file_doesnt_exist() async throws { - try await assertLoadingRaisesWhenManifestNotFound(manifest: .dependencies) - } - func test_run_throws_when_plugin_and_file_doesnt_exist() async throws { try await assertLoadingRaisesWhenManifestNotFound(manifest: .plugin) } @@ -323,7 +420,7 @@ final class DumpServiceTests: TuistTestCase { private func assertLoadingRaisesWhenManifestNotFound(manifest: DumpableManifest) async throws { try await fileHandler.inTemporaryDirectory { tmpDir in var expectedDirectory = tmpDir - if manifest == .config || manifest == .dependencies { + if manifest == .config { expectedDirectory = expectedDirectory.appending(component: Constants.tuistDirectoryName) if !self.fileHandler.exists(expectedDirectory) { try self.fileHandler.createFolder(expectedDirectory) @@ -348,10 +445,10 @@ extension DumpableManifest { return .config case .template: return .template - case .dependencies: - return .dependencies case .plugin: return .plugin + case .package: + return .packageSettings } } } diff --git a/Tests/TuistKitIntegrationTests/Utils/ManifestGraphLoaderIntegrationTests.swift b/Tests/TuistKitIntegrationTests/Utils/ManifestGraphLoaderIntegrationTests.swift index b7173c76532..bc073b442f8 100644 --- a/Tests/TuistKitIntegrationTests/Utils/ManifestGraphLoaderIntegrationTests.swift +++ b/Tests/TuistKitIntegrationTests/Utils/ManifestGraphLoaderIntegrationTests.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistLoader import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @testable import TuistKit @@ -36,7 +36,7 @@ final class ManifestGraphLoaderIntegrationTests: TuistTestCase { let path = try temporaryFixture("WorkspaceWithPlugins") // When - let (result, _, _) = try await subject.load(path: path) + let (result, _, _, _) = try await subject.load(path: path) // Then XCTAssertEqual(result.workspace.name, "Workspace") @@ -53,7 +53,7 @@ final class ManifestGraphLoaderIntegrationTests: TuistTestCase { .appending(component: "App") // When - let (result, _, _) = try await subject.load(path: path) + let (result, _, _, _) = try await subject.load(path: path) // Then XCTAssertEqual(result.workspace.name, "App") diff --git a/Tests/TuistKitTests/CommandEnvironmentVariables/CommandEnvironmentVariableTests.swift b/Tests/TuistKitTests/CommandEnvironmentVariables/CommandEnvironmentVariableTests.swift new file mode 100644 index 00000000000..e2cecf65dd0 --- /dev/null +++ b/Tests/TuistKitTests/CommandEnvironmentVariables/CommandEnvironmentVariableTests.swift @@ -0,0 +1,884 @@ +import ArgumentParser +import Difference +import Foundation +import TSCUtility +import XCTest +@testable import Path +@testable import TuistCore +@testable import TuistKit +@testable import TuistSupport +@testable import TuistSupportTesting + +final class CommandEnvironmentVariableTests: XCTestCase { + private var mockEnvironment: MockEnvironment! + + override func setUp() { + super.setUp() + mockEnvironment = try! MockEnvironment() + Environment._shared.mutate { $0 = mockEnvironment } + } + + override func tearDown() { + mockEnvironment = nil + Environment._shared.mutate { $0 = Environment() } + super.tearDown() + } + + private var tuistVariables: [String: String] { + get { + return mockEnvironment.tuistVariables + } + set { + mockEnvironment.tuistVariables = newValue + } + } + + private func setVariable(_ key: EnvKey, value: String) { + mockEnvironment.tuistVariables[key.rawValue] = value + } + + func testBuildCommandUsesEnvVars() throws { + setVariable(.buildOptionsScheme, value: "Scheme1") + setVariable(.buildOptionsGenerate, value: "true") + setVariable(.buildOptionsClean, value: "true") + setVariable(.buildOptionsPath, value: "/path/to/project") + setVariable(.buildOptionsDevice, value: "iPhone") + setVariable(.buildOptionsPlatform, value: "ios") + setVariable(.buildOptionsOS, value: "14.5.0") + setVariable(.buildOptionsRosetta, value: "true") + setVariable(.buildOptionsConfiguration, value: "Debug") + setVariable(.buildOptionsOutputPath, value: "/path/to/output") + setVariable(.buildOptionsDerivedDataPath, value: "/path/to/derivedData") + setVariable(.buildOptionsGenerateOnly, value: "true") + setVariable(.buildOptionsPassthroughXcodeBuildArguments, value: "clean,-configuration,Release") + + let buildCommandWithEnvVars = try BuildCommand.parse([]) + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.scheme, "Scheme1") + XCTAssertTrue(buildCommandWithEnvVars.buildOptions.generate) + XCTAssertTrue(buildCommandWithEnvVars.buildOptions.clean) + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.path, "/path/to/project") + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.device, "iPhone") + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.platform, .iOS) + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.os, "14.5.0") + XCTAssertTrue(buildCommandWithEnvVars.buildOptions.rosetta) + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.configuration, "Debug") + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.buildOutputPath, "/path/to/output") + XCTAssertEqual(buildCommandWithEnvVars.buildOptions.derivedDataPath, "/path/to/derivedData") + XCTAssertTrue(buildCommandWithEnvVars.buildOptions.generateOnly) + XCTAssertEqual( + buildCommandWithEnvVars.buildOptions.passthroughXcodeBuildArguments, + ["clean", "-configuration", "Release"] + ) + + let buildCommandWithArgs = try BuildCommand.parse([ + "Scheme2", + "--generate", + "--no-clean", + "--path", "/new/path", + "--device", "iPad", + "--platform", "tvos", + "--no-rosetta", + "--configuration", "Release", + "--build-output-path", "/new/output", + "--derived-data-path", "/new/derivedData", + "--no-generate-only", + "--", + "-configuration", "Debug", + ]) + XCTAssertEqual(buildCommandWithArgs.buildOptions.scheme, "Scheme2") + XCTAssertTrue(buildCommandWithArgs.buildOptions.generate) + XCTAssertFalse(buildCommandWithArgs.buildOptions.clean) + XCTAssertEqual(buildCommandWithArgs.buildOptions.path, "/new/path") + XCTAssertEqual(buildCommandWithArgs.buildOptions.device, "iPad") + XCTAssertEqual(buildCommandWithArgs.buildOptions.platform, .tvOS) + XCTAssertFalse(buildCommandWithArgs.buildOptions.rosetta) + XCTAssertEqual(buildCommandWithArgs.buildOptions.configuration, "Release") + XCTAssertEqual(buildCommandWithArgs.buildOptions.buildOutputPath, "/new/output") + XCTAssertEqual(buildCommandWithArgs.buildOptions.derivedDataPath, "/new/derivedData") + XCTAssertFalse(buildCommandWithArgs.buildOptions.generateOnly) + XCTAssertEqual(buildCommandWithArgs.buildOptions.passthroughXcodeBuildArguments, ["-configuration", "Debug"]) + } + + func testCleanCommandUsesEnvVars() throws { + setVariable(.cleanCleanCategories, value: "dependencies") + setVariable(.cleanPath, value: "/path/to/clean") + + let cleanCommandWithEnvVars = try CleanCommand.parse([]) + XCTAssertEqual(cleanCommandWithEnvVars.cleanCategories, [TuistCleanCategory.dependencies]) + XCTAssertEqual(cleanCommandWithEnvVars.path, "/path/to/clean") + + let cleanCommandWithArgs = try CleanCommand.parse([ + "manifests", + "--path", "/new/clean/path", + ]) + XCTAssertEqual(cleanCommandWithArgs.cleanCategories, [TuistCleanCategory.global(.manifests)]) + XCTAssertEqual(cleanCommandWithArgs.path, "/new/clean/path") + } + + func testDumpCommandUsesEnvVars() throws { + setVariable(.dumpPath, value: "/path/to/dump") + setVariable(.dumpManifest, value: "Project") + + let dumpCommandWithEnvVars = try DumpCommand.parse([]) + XCTAssertEqual(dumpCommandWithEnvVars.path, "/path/to/dump") + XCTAssertEqual(dumpCommandWithEnvVars.manifest, .project) + + let dumpCommandWithArgs = try DumpCommand.parse([ + "workspace", + "--path", "/new/dump/path", + ]) + XCTAssertEqual(dumpCommandWithArgs.path, "/new/dump/path") + XCTAssertEqual(dumpCommandWithArgs.manifest, .workspace) + } + + func testEditCommandUsesEnvVars() throws { + setVariable(.editPath, value: "/path/to/edit") + setVariable(.editPermanent, value: "true") + setVariable(.editOnlyCurrentDirectory, value: "true") + + let editCommandWithEnvVars = try EditCommand.parse([]) + XCTAssertEqual(editCommandWithEnvVars.path, "/path/to/edit") + XCTAssertTrue(editCommandWithEnvVars.permanent) + XCTAssertTrue(editCommandWithEnvVars.onlyCurrentDirectory) + + let editCommandWithArgs = try EditCommand.parse([ + "--path", "/new/edit/path", + "--no-permanent", + "--no-only-current-directory", + ]) + XCTAssertEqual(editCommandWithArgs.path, "/new/edit/path") + XCTAssertFalse(editCommandWithArgs.permanent) + XCTAssertFalse(editCommandWithArgs.onlyCurrentDirectory) + } + + func testGenerateCommandUsesEnvVars() throws { + setVariable(.generatePath, value: "/path/to/generate") + setVariable(.generateOpen, value: "false") + setVariable(.generateBinaryCache, value: "false") + + let generateCommandWithEnvVars = try GenerateCommand.parse([]) + XCTAssertEqual(generateCommandWithEnvVars.path, "/path/to/generate") + XCTAssertFalse(generateCommandWithEnvVars.open) + XCTAssertFalse(generateCommandWithEnvVars.binaryCache) + + let generateCommandWithArgs = try GenerateCommand.parse([ + "--path", "/new/generate/path", + "--open", + "--binary-cache", + ]) + XCTAssertEqual(generateCommandWithArgs.path, "/new/generate/path") + XCTAssertTrue(generateCommandWithArgs.open) + XCTAssertTrue(generateCommandWithArgs.binaryCache) + } + + func testGraphCommandUsesEnvVars() throws { + setVariable(.graphSkipTestTargets, value: "true") + setVariable(.graphSkipExternalDependencies, value: "true") + setVariable(.graphPlatform, value: "ios") + setVariable(.graphFormat, value: "svg") + setVariable(.graphOpen, value: "false") + setVariable(.graphLayoutAlgorithm, value: "circo") + setVariable(.graphTargets, value: "Target1,Target2") + setVariable(.graphPath, value: "/path/to/graph") + setVariable(.graphOutputPath, value: "/path/to/output") + + let graphCommandWithEnvVars = try GraphCommand.parse([]) + XCTAssertTrue(graphCommandWithEnvVars.skipTestTargets) + XCTAssertTrue(graphCommandWithEnvVars.skipExternalDependencies) + XCTAssertEqual(graphCommandWithEnvVars.platform, .iOS) + XCTAssertEqual(graphCommandWithEnvVars.format, .svg) + XCTAssertFalse(graphCommandWithEnvVars.open) + XCTAssertEqual(graphCommandWithEnvVars.layoutAlgorithm, .circo) + XCTAssertEqual(graphCommandWithEnvVars.targets, ["Target1", "Target2"]) + XCTAssertEqual(graphCommandWithEnvVars.path, "/path/to/graph") + XCTAssertEqual(graphCommandWithEnvVars.outputPath, "/path/to/output") + + let graphCommandWithArgs = try GraphCommand.parse([ + "--no-skip-test-targets", + "--no-skip-external-dependencies", + "--platform", "macos", + "--format", "json", + "--open", + "--algorithm", "fdp", + "Target3", "Target4", + "--path", "/new/graph/path", + "--output-path", "/new/graph/output", + ]) + XCTAssertFalse(graphCommandWithArgs.skipTestTargets) + XCTAssertFalse(graphCommandWithArgs.skipExternalDependencies) + XCTAssertEqual(graphCommandWithArgs.platform, .macOS) + XCTAssertEqual(graphCommandWithArgs.format, .json) + XCTAssertTrue(graphCommandWithArgs.open) + XCTAssertEqual(graphCommandWithArgs.layoutAlgorithm, .fdp) + XCTAssertEqual(graphCommandWithArgs.targets, ["Target3", "Target4"]) + XCTAssertEqual(graphCommandWithArgs.path, "/new/graph/path") + XCTAssertEqual(graphCommandWithArgs.outputPath, "/new/graph/output") + } + + func testInitCommandUsesEnvVars() throws { + setVariable(.initPlatform, value: "macos") + setVariable(.initName, value: "MyProject") + setVariable(.initTemplate, value: "MyTemplate") + setVariable(.initPath, value: "/path/to/init") + + let initCommandWithEnvVars = try InitCommand.parse([]) + XCTAssertEqual(initCommandWithEnvVars.name, "MyProject") + XCTAssertEqual(initCommandWithEnvVars.template, "MyTemplate") + XCTAssertEqual(initCommandWithEnvVars.path, "/path/to/init") + + let initCommandWithArgs = try InitCommand.parse([ + "--platform", "ios", + "--name", "NewProject", + "--template", "NewTemplate", + "--path", "/new/init/path", + ]) + XCTAssertEqual(initCommandWithArgs.name, "NewProject") + XCTAssertEqual(initCommandWithArgs.template, "NewTemplate") + XCTAssertEqual(initCommandWithArgs.path, "/new/init/path") + } + + func testInstallCommandUsesEnvVars() throws { + setVariable(.installPath, value: "/path/to/install") + setVariable(.installUpdate, value: "true") + + let installCommandWithEnvVars = try InstallCommand.parse([]) + XCTAssertEqual(installCommandWithEnvVars.path, "/path/to/install") + XCTAssertTrue(installCommandWithEnvVars.update) + + let installCommandWithArgs = try InstallCommand.parse([ + "--path", "/new/install/path", + "--no-update", + ]) + XCTAssertEqual(installCommandWithArgs.path, "/new/install/path") + XCTAssertFalse(installCommandWithArgs.update) + } + + func testListCommandUsesEnvVars() throws { + setVariable(.scaffoldListJson, value: "true") + setVariable(.scaffoldListPath, value: "/path/to/list") + + let listCommandWithEnvVars = try ListCommand.parse([]) + XCTAssertTrue(listCommandWithEnvVars.json) + XCTAssertEqual(listCommandWithEnvVars.path, "/path/to/list") + + let listCommandWithArgs = try ListCommand.parse([ + "--no-json", + "--path", "/new/list/path", + ]) + XCTAssertFalse(listCommandWithArgs.json) + XCTAssertEqual(listCommandWithArgs.path, "/new/list/path") + } + + func testMigrationCheckEmptyBuildSettingsCommandUsesEnvVars() throws { + setVariable(.migrationCheckEmptySettingsXcodeprojPath, value: "/path/to/xcodeproj") + setVariable(.migrationCheckEmptySettingsTarget, value: "MyTarget") + + let migrationCommandWithEnvVars = try MigrationCheckEmptyBuildSettingsCommand.parse([]) + XCTAssertEqual(migrationCommandWithEnvVars.xcodeprojPath, "/path/to/xcodeproj") + XCTAssertEqual(migrationCommandWithEnvVars.target, "MyTarget") + + let migrationCommandWithArgs = try MigrationCheckEmptyBuildSettingsCommand.parse([ + "--xcodeproj-path", "/new/xcodeproj/path", + "--target", "NewTarget", + ]) + XCTAssertEqual(migrationCommandWithArgs.xcodeprojPath, "/new/xcodeproj/path") + XCTAssertEqual(migrationCommandWithArgs.target, "NewTarget") + } + + func testMigrationSettingsToXCConfigCommandUsesEnvVars() throws { + setVariable(.migrationSettingsToXcconfigXcodeprojPath, value: "/path/to/xcodeproj") + setVariable(.migrationSettingsToXcconfigXcconfigPath, value: "/path/to/xcconfig") + setVariable(.migrationSettingsToXcconfigTarget, value: "MyTarget") + + let migrationCommandWithEnvVars = try MigrationSettingsToXCConfigCommand.parse([]) + XCTAssertEqual(migrationCommandWithEnvVars.xcodeprojPath, "/path/to/xcodeproj") + XCTAssertEqual(migrationCommandWithEnvVars.xcconfigPath, "/path/to/xcconfig") + XCTAssertEqual(migrationCommandWithEnvVars.target, "MyTarget") + + let migrationCommandWithArgs = try MigrationSettingsToXCConfigCommand.parse([ + "--xcodeproj-path", "/new/xcodeproj/path", + "--xcconfig-path", "/new/xcconfig/path", + "--target", "NewTarget", + ]) + XCTAssertEqual(migrationCommandWithArgs.xcodeprojPath, "/new/xcodeproj/path") + XCTAssertEqual(migrationCommandWithArgs.xcconfigPath, "/new/xcconfig/path") + XCTAssertEqual(migrationCommandWithArgs.target, "NewTarget") + } + + func testMigrationTargetsByDependenciesCommandUsesEnvVars() throws { + setVariable(.migrationListTargetsXcodeprojPath, value: "/path/to/xcodeproj") + + let migrationCommandWithEnvVars = try MigrationTargetsByDependenciesCommand.parse([]) + XCTAssertEqual(migrationCommandWithEnvVars.xcodeprojPath, "/path/to/xcodeproj") + + let migrationCommandWithArgs = try MigrationTargetsByDependenciesCommand.parse([ + "--xcodeproj-path", "/new/xcodeproj/path", + ]) + XCTAssertEqual(migrationCommandWithArgs.xcodeprojPath, "/new/xcodeproj/path") + } + + func testPluginArchiveCommandUsesEnvVars() throws { + setVariable(.pluginArchivePath, value: "/path/to/plugin") + + let pluginCommandWithEnvVars = try PluginArchiveCommand.parse([]) + XCTAssertEqual(pluginCommandWithEnvVars.path, "/path/to/plugin") + + let pluginCommandWithArgs = try PluginArchiveCommand.parse([ + "--path", "/new/plugin/path", + ]) + XCTAssertEqual(pluginCommandWithArgs.path, "/new/plugin/path") + } + + func testPluginBuildCommandUsesEnvVars() throws { + setVariable(.pluginOptionsPath, value: "/path/to/plugin") + setVariable(.pluginOptionsConfiguration, value: "debug") + setVariable(.pluginBuildBuildTests, value: "true") + setVariable(.pluginBuildShowBinPath, value: "true") + setVariable(.pluginBuildTargets, value: "Target1,Target2") + setVariable(.pluginBuildProducts, value: "Product1,Product2") + + let pluginCommandWithEnvVars = try PluginBuildCommand.parse([]) + XCTAssertEqual(pluginCommandWithEnvVars.pluginOptions.path, "/path/to/plugin") + XCTAssertEqual(pluginCommandWithEnvVars.pluginOptions.configuration, .debug) + XCTAssertTrue(pluginCommandWithEnvVars.buildTests) + XCTAssertTrue(pluginCommandWithEnvVars.showBinPath) + XCTAssertEqual(pluginCommandWithEnvVars.targets, ["Target1", "Target2"]) + XCTAssertEqual(pluginCommandWithEnvVars.products, ["Product1", "Product2"]) + + let pluginCommandWithArgs = try PluginBuildCommand.parse([ + "--path", "/new/plugin/path", + "--configuration", "release", + "--no-build-tests", + "--no-show-bin-path", + "--targets", "Target3", "--targets", "Target4", + "--products", "Product3", "--products", "Product4", + ]) + XCTAssertEqual(pluginCommandWithArgs.pluginOptions.path, "/new/plugin/path") + XCTAssertEqual(pluginCommandWithArgs.pluginOptions.configuration, .release) + XCTAssertFalse(pluginCommandWithArgs.buildTests) + XCTAssertFalse(pluginCommandWithArgs.showBinPath) + XCTAssertEqual(pluginCommandWithArgs.targets, ["Target3", "Target4"]) + XCTAssertEqual(pluginCommandWithArgs.products, ["Product3", "Product4"]) + } + + func testPluginRunCommandUsesEnvVars() throws { + setVariable(.pluginOptionsPath, value: "/path/to/plugin") + setVariable(.pluginOptionsConfiguration, value: "debug") + setVariable(.pluginRunBuildTests, value: "true") + setVariable(.pluginRunSkipBuild, value: "true") + setVariable(.pluginRunTask, value: "myTask") + setVariable(.pluginRunArguments, value: "arg1,arg2,arg3") + + let pluginCommandWithEnvVars = try PluginRunCommand.parse([]) + XCTAssertEqual(pluginCommandWithEnvVars.pluginOptions.path, "/path/to/plugin") + XCTAssertEqual(pluginCommandWithEnvVars.pluginOptions.configuration, .debug) + XCTAssertTrue(pluginCommandWithEnvVars.buildTests) + XCTAssertTrue(pluginCommandWithEnvVars.skipBuild) + XCTAssertEqual(pluginCommandWithEnvVars.task, "myTask") + XCTAssertEqual(pluginCommandWithEnvVars.arguments, ["arg1", "arg2", "arg3"]) + + let pluginCommandWithArgs = try PluginRunCommand.parse([ + "--path", "/new/plugin/path", + "--configuration", "release", + "--no-build-tests", + "--no-skip-build", + "otherTask", + "arg4", "arg5", + ]) + XCTAssertEqual(pluginCommandWithArgs.pluginOptions.path, "/new/plugin/path") + XCTAssertEqual(pluginCommandWithArgs.pluginOptions.configuration, .release) + XCTAssertFalse(pluginCommandWithArgs.buildTests) + XCTAssertFalse(pluginCommandWithArgs.skipBuild) + XCTAssertEqual(pluginCommandWithArgs.task, "otherTask") + XCTAssertEqual(pluginCommandWithArgs.arguments, ["arg4", "arg5"]) + } + + func testPluginTestCommandUsesEnvVars() throws { + setVariable(.pluginOptionsPath, value: "/path/to/plugin") + setVariable(.pluginOptionsConfiguration, value: "debug") + setVariable(.pluginTestBuildTests, value: "true") + setVariable(.pluginTestTestProducts, value: "Product1,Product2") + + let pluginCommandWithEnvVars = try PluginTestCommand.parse([]) + XCTAssertEqual(pluginCommandWithEnvVars.pluginOptions.path, "/path/to/plugin") + XCTAssertEqual(pluginCommandWithEnvVars.pluginOptions.configuration, .debug) + XCTAssertTrue(pluginCommandWithEnvVars.buildTests) + XCTAssertEqual(pluginCommandWithEnvVars.testProducts, ["Product1", "Product2"]) + + let pluginCommandWithArgs = try PluginTestCommand.parse([ + "--path", "/new/plugin/path", + "--configuration", "release", + "--no-build-tests", + "--test-products", "Product3", "--test-products", "Product4", + ]) + XCTAssertEqual(pluginCommandWithArgs.pluginOptions.path, "/new/plugin/path") + XCTAssertEqual(pluginCommandWithArgs.pluginOptions.configuration, .release) + XCTAssertFalse(pluginCommandWithArgs.buildTests) + XCTAssertEqual(pluginCommandWithArgs.testProducts, ["Product3", "Product4"]) + } + + func testRunCommandUsesEnvVars() throws { + // Set environment variables for RunCommand + setVariable(.runGenerate, value: "true") + setVariable(.runClean, value: "true") + setVariable(.runOS, value: "14.5") + setVariable(.runScheme, value: "MyScheme") + setVariable(.runArguments, value: "arg1,arg2,arg3") + + // Execute RunCommand without command line arguments + let runCommandWithEnvVars = try RunCommand.parse([]) + XCTAssertTrue(runCommandWithEnvVars.generate) + XCTAssertTrue(runCommandWithEnvVars.clean) + XCTAssertEqual(runCommandWithEnvVars.os, "14.5") + XCTAssertEqual(runCommandWithEnvVars.runnable, .scheme("MyScheme")) + XCTAssertEqual(runCommandWithEnvVars.arguments, ["arg1", "arg2", "arg3"]) + + // Execute RunCommand with command line arguments + let runCommandWithArgs = try RunCommand.parse([ + "--no-generate", + "--no-clean", + "--path", "/new/run/path", + "--configuration", "Release", + "--device", "iPhone 12", + "--os", "15.0", + "--rosetta", + "AnotherScheme", + "arg4", "arg5", + ]) + XCTAssertFalse(runCommandWithArgs.generate) + XCTAssertFalse(runCommandWithArgs.clean) + XCTAssertEqual(runCommandWithArgs.path, "/new/run/path") + XCTAssertEqual(runCommandWithArgs.configuration, "Release") + XCTAssertEqual(runCommandWithArgs.device, "iPhone 12") + XCTAssertEqual(runCommandWithArgs.os, "15.0") + XCTAssertTrue(runCommandWithArgs.rosetta) + XCTAssertEqual(runCommandWithArgs.runnable, .scheme("AnotherScheme")) + XCTAssertEqual(runCommandWithArgs.arguments, ["arg4", "arg5"]) + } + + func testScaffoldCommandUsesEnvVars() throws { + // Set environment variables for ScaffoldCommand + setVariable(.scaffoldJson, value: "true") + setVariable(.scaffoldPath, value: "/path/to/scaffold") + setVariable(.scaffoldTemplate, value: "MyTemplate") + + // Execute ScaffoldCommand without command line arguments + let scaffoldCommandWithEnvVars = try ScaffoldCommand.parse([]) + XCTAssertTrue(scaffoldCommandWithEnvVars.json) + XCTAssertEqual(scaffoldCommandWithEnvVars.path, "/path/to/scaffold") + XCTAssertEqual(scaffoldCommandWithEnvVars.template, "MyTemplate") + + // Execute ScaffoldCommand with command line arguments + let scaffoldCommandWithArgs = try ScaffoldCommand.parse([ + "--no-json", + "--path", "/new/scaffold/path", + "AnotherTemplate", + ]) + XCTAssertFalse(scaffoldCommandWithArgs.json) + XCTAssertEqual(scaffoldCommandWithArgs.path, "/new/scaffold/path") + XCTAssertEqual(scaffoldCommandWithArgs.template, "AnotherTemplate") + } + + func testTestCommandWithEnvVars() throws { + // Set environment variables for TestCommand + setVariable(.testScheme, value: "MyScheme") + setVariable(.testClean, value: "true") + setVariable(.testPath, value: "/path/to/test") + setVariable(.testDevice, value: "iPhone") + setVariable(.testPlatform, value: "iOS") + setVariable(.testOS, value: "14.5") + setVariable(.testRosetta, value: "true") + setVariable(.testConfiguration, value: "Debug") + setVariable(.testSkipUITests, value: "true") + setVariable(.testResultBundlePath, value: "/path/to/resultBundle") + setVariable(.testDerivedDataPath, value: "/path/to/derivedData") + setVariable(.testRetryCount, value: "2") + setVariable(.testTestPlan, value: "MyTestPlan") + setVariable(.testSkipTestTargets, value: "SkipTarget1,SkipTarget2") + setVariable(.testConfigurations, value: "Config1,Config2") + setVariable(.testSkipConfigurations, value: "SkipConfig1,SkipConfig2") + setVariable(.testGenerateOnly, value: "true") + setVariable(.testBinaryCache, value: "false") + setVariable(.testSelectiveTesting, value: "false") + + // Execute TestCommand without command line arguments + let testCommandWithEnvVars = try TestCommand.parse([]) + XCTAssertEqual(testCommandWithEnvVars.scheme, "MyScheme") + XCTAssertTrue(testCommandWithEnvVars.clean) + XCTAssertEqual(testCommandWithEnvVars.path, "/path/to/test") + XCTAssertEqual(testCommandWithEnvVars.device, "iPhone") + XCTAssertEqual(testCommandWithEnvVars.platform, "iOS") + XCTAssertEqual(testCommandWithEnvVars.os, "14.5") + XCTAssertTrue(testCommandWithEnvVars.rosetta) + XCTAssertEqual(testCommandWithEnvVars.configuration, "Debug") + XCTAssertTrue(testCommandWithEnvVars.skipUITests) + XCTAssertEqual(testCommandWithEnvVars.resultBundlePath, "/path/to/resultBundle") + XCTAssertEqual(testCommandWithEnvVars.derivedDataPath, "/path/to/derivedData") + XCTAssertEqual(testCommandWithEnvVars.retryCount, 2) + XCTAssertEqual(testCommandWithEnvVars.testPlan, "MyTestPlan") + XCTAssertEqual(testCommandWithEnvVars.testTargets, []) + XCTAssertEqual(testCommandWithEnvVars.skipTestTargets, [ + try TestIdentifier(string: "SkipTarget1"), + try TestIdentifier(string: "SkipTarget2"), + ]) + XCTAssertEqual(testCommandWithEnvVars.configurations, ["Config1", "Config2"]) + XCTAssertEqual(testCommandWithEnvVars.skipConfigurations, ["SkipConfig1", "SkipConfig2"]) + XCTAssertTrue(testCommandWithEnvVars.generateOnly) + XCTAssertFalse(testCommandWithEnvVars.binaryCache) + XCTAssertFalse(testCommandWithEnvVars.selectiveTesting) + + // Execute TestCommand with command line arguments + let testCommandWithArgs = try TestCommand.parse([ + "NewScheme", + "--no-clean", + "--path", "/new/test/path", + "--device", "iPad", + "--platform", "macOS", + "--os", "15.0", + "--no-rosetta", + "--configuration", "Release", + "--no-skip-ui-tests", + "--result-bundle-path", "/new/resultBundle/path", + "--derived-data-path", "/new/derivedData/path", + "--retry-count", "3", + "--test-plan", "NewTestPlan", + "--skip-test-targets", "NewSkipTarget1", "NewSkipTarget2", + "--filter-configurations", "NewConfig1", "NewConfig2", + "--skip-configurations", "NewSkipConfig1", "NewSkipConfig2", + "--no-generate-only", + "--no-binary-cache", + "--no-selective-testing", + ]) + XCTAssertEqual(testCommandWithArgs.scheme, "NewScheme") + XCTAssertFalse(testCommandWithArgs.clean) + XCTAssertEqual(testCommandWithArgs.path, "/new/test/path") + XCTAssertEqual(testCommandWithArgs.device, "iPad") + XCTAssertEqual(testCommandWithArgs.platform, "macOS") + XCTAssertEqual(testCommandWithArgs.os, "15.0") + XCTAssertFalse(testCommandWithArgs.rosetta) + XCTAssertEqual(testCommandWithArgs.configuration, "Release") + XCTAssertFalse(testCommandWithArgs.skipUITests) + XCTAssertEqual(testCommandWithArgs.resultBundlePath, "/new/resultBundle/path") + XCTAssertEqual(testCommandWithArgs.derivedDataPath, "/new/derivedData/path") + XCTAssertEqual(testCommandWithArgs.retryCount, 3) + XCTAssertEqual(testCommandWithArgs.testPlan, "NewTestPlan") + XCTAssertEqual(testCommandWithArgs.testTargets, []) + XCTAssertEqual(testCommandWithArgs.skipTestTargets, [ + try TestIdentifier(string: "NewSkipTarget1"), + try TestIdentifier(string: "NewSkipTarget2"), + ]) + XCTAssertEqual(testCommandWithArgs.configurations, ["NewConfig1", "NewConfig2"]) + XCTAssertEqual(testCommandWithArgs.skipConfigurations, ["NewSkipConfig1", "NewSkipConfig2"]) + XCTAssertFalse(testCommandWithArgs.generateOnly) + XCTAssertFalse(testCommandWithArgs.binaryCache) + XCTAssertFalse(testCommandWithArgs.selectiveTesting) + } + + func testOrganizationBillingCommandUsesEnvVars() throws { + setVariable(.organizationBillingOrganizationName, value: "MyOrganization") + setVariable(.organizationBillingPath, value: "/path/to/billing") + + let commandWithEnvVars = try OrganizationBillingCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/billing") + + let commandWithArgs = try OrganizationBillingCommand.parse([ + "AnotherOrganization", + "--path", "/new/billing/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "AnotherOrganization") + XCTAssertEqual(commandWithArgs.path, "/new/billing/path") + } + + func testOrganizationCreateCommandUsesEnvVars() throws { + setVariable(.organizationCreateOrganizationName, value: "MyNewOrganization") + setVariable(.organizationCreatePath, value: "/path/to/create") + + let commandWithEnvVars = try OrganizationCreateCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyNewOrganization") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/create") + + let commandWithArgs = try OrganizationCreateCommand.parse([ + "AnotherNewOrganization", + "--path", "/new/create/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "AnotherNewOrganization") + XCTAssertEqual(commandWithArgs.path, "/new/create/path") + } + + func testOrganizationDeleteCommandUsesEnvVars() throws { + setVariable(.organizationDeleteOrganizationName, value: "OrganizationToDelete") + setVariable(.organizationDeletePath, value: "/path/to/delete") + + let commandWithEnvVars = try OrganizationDeleteCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "OrganizationToDelete") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/delete") + + let commandWithArgs = try OrganizationDeleteCommand.parse([ + "AnotherOrganizationToDelete", + "--path", "/new/delete/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "AnotherOrganizationToDelete") + XCTAssertEqual(commandWithArgs.path, "/new/delete/path") + } + + func testProjectTokensCreateCommandUsesEnvVars() throws { + setVariable(.projectTokenFullHandle, value: "tuist-org/tuist") + setVariable(.projectTokenPath, value: "/path/to/token") + + let commandWithEnvVars = try ProjectTokensCreateCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.fullHandle, "tuist-org/tuist") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/token") + + let commandWithArgs = try ProjectTokensCreateCommand.parse([ + "new-org/new-project", + "--path", "/new/token/path", + ]) + XCTAssertEqual(commandWithArgs.fullHandle, "new-org/new-project") + XCTAssertEqual(commandWithArgs.path, "/new/token/path") + } + + func testOrganizationListCommandUsesEnvVars() throws { + setVariable(.organizationListJson, value: "true") + setVariable(.organizationListPath, value: "/path/to/list") + + let commandWithEnvVars = try OrganizationListCommand.parse([]) + XCTAssertTrue(commandWithEnvVars.json) + XCTAssertEqual(commandWithEnvVars.path, "/path/to/list") + + let commandWithArgs = try OrganizationListCommand.parse([ + "--no-json", + "--path", "/new/list/path", + ]) + XCTAssertFalse(commandWithArgs.json) + XCTAssertEqual(commandWithArgs.path, "/new/list/path") + } + + func testOrganizationRemoveInviteCommandUsesEnvVars() throws { + setVariable(.organizationRemoveInviteOrganizationName, value: "MyOrganization") + setVariable(.organizationRemoveInviteEmail, value: "email@example.com") + setVariable(.organizationRemoveInvitePath, value: "/path/to/invite") + + let commandWithEnvVars = try OrganizationRemoveInviteCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertEqual(commandWithEnvVars.email, "email@example.com") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/invite") + + let commandWithArgs = try OrganizationRemoveInviteCommand.parse([ + "NewOrganization", + "newemail@example.com", + "--path", "/new/invite/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewOrganization") + XCTAssertEqual(commandWithArgs.email, "newemail@example.com") + XCTAssertEqual(commandWithArgs.path, "/new/invite/path") + } + + func testOrganizationRemoveMemberCommandUsesEnvVars() throws { + setVariable(.organizationRemoveMemberOrganizationName, value: "MyOrganization") + setVariable(.organizationRemoveMemberUsername, value: "username") + setVariable(.organizationRemoveMemberPath, value: "/path/to/member") + + let commandWithEnvVars = try OrganizationRemoveMemberCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertEqual(commandWithEnvVars.username, "username") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/member") + + let commandWithArgs = try OrganizationRemoveMemberCommand.parse([ + "NewOrganization", + "newusername", + "--path", "/new/member/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewOrganization") + XCTAssertEqual(commandWithArgs.username, "newusername") + XCTAssertEqual(commandWithArgs.path, "/new/member/path") + } + + func testOrganizationRemoveSSOCommandUsesEnvVars() throws { + setVariable(.organizationRemoveSSOOrganizationName, value: "MyOrganization") + setVariable(.organizationRemoveSSOPath, value: "/path/to/sso") + + let commandWithEnvVars = try OrganizationRemoveSSOCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/sso") + + let commandWithArgs = try OrganizationRemoveSSOCommand.parse([ + "NewOrganization", + "--path", "/new/sso/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewOrganization") + XCTAssertEqual(commandWithArgs.path, "/new/sso/path") + } + + func testOrganizationUpdateSSOCommandUsesEnvVars() throws { + setVariable(.organizationUpdateSSOOrganizationName, value: "MyOrganization") + setVariable(.organizationUpdateSSOProvider, value: "google") + setVariable(.organizationUpdateSSOOrganizationId, value: "1234") + setVariable(.organizationUpdateSSOPath, value: "/path/to/update/sso") + + let commandWithEnvVars = try OrganizationUpdateSSOCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertEqual(commandWithEnvVars.provider, .google) + XCTAssertEqual(commandWithEnvVars.organizationId, "1234") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/update/sso") + + let commandWithArgs = try OrganizationUpdateSSOCommand.parse([ + "NewOrganization", + "--provider", "google", + "--organization-id", "5678", + "--path", "/new/update/sso/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewOrganization") + XCTAssertEqual(commandWithArgs.provider, .google) + XCTAssertEqual(commandWithArgs.organizationId, "5678") + XCTAssertEqual(commandWithArgs.path, "/new/update/sso/path") + } + + func testProjectDeleteCommandUsesEnvVars() throws { + setVariable(.projectDeleteFullHandle, value: "tuist-org/tuist") + setVariable(.projectDeletePath, value: "/path/to/delete") + + let commandWithEnvVars = try ProjectDeleteCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.fullHandle, "tuist-org/tuist") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/delete") + + let commandWithArgs = try ProjectDeleteCommand.parse([ + "new-org/new-project", + "--path", "/new/delete/path", + ]) + XCTAssertEqual(commandWithArgs.fullHandle, "new-org/new-project") + XCTAssertEqual(commandWithArgs.path, "/new/delete/path") + } + + func testProjectCreateCommandUsesEnvVars() throws { + setVariable(.projectCreateFullHandle, value: "tuist-org/tuist") + setVariable(.projectCreatePath, value: "/path/to/create") + + let commandWithEnvVars = try ProjectCreateCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.fullHandle, "tuist-org/tuist") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/create") + + let commandWithArgs = try ProjectCreateCommand.parse([ + "new-org/new-project", + "--path", "/new/create/path", + ]) + XCTAssertEqual(commandWithArgs.fullHandle, "new-org/new-project") + XCTAssertEqual(commandWithArgs.path, "/new/create/path") + } + + func testOrganizationInviteCommandUsesEnvVars() throws { + setVariable(.organizationInviteOrganizationName, value: "InviteOrganization") + setVariable(.organizationInviteEmail, value: "email@example.com") + setVariable(.organizationInvitePath, value: "/path/to/invite") + + let commandWithEnvVars = try OrganizationInviteCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "InviteOrganization") + XCTAssertEqual(commandWithEnvVars.email, "email@example.com") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/invite") + + let commandWithArgs = try OrganizationInviteCommand.parse([ + "NewInviteOrganization", + "newemail@example.com", + "--path", "/new/invite/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewInviteOrganization") + XCTAssertEqual(commandWithArgs.email, "newemail@example.com") + XCTAssertEqual(commandWithArgs.path, "/new/invite/path") + } + + func testOrganizationShowCommandUsesEnvVars() throws { + setVariable(.organizationShowOrganizationName, value: "MyOrganization") + setVariable(.organizationShowJson, value: "true") + setVariable(.organizationShowPath, value: "/path/to/show") + + let commandWithEnvVars = try OrganizationShowCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertTrue(commandWithEnvVars.json) + XCTAssertEqual(commandWithEnvVars.path, "/path/to/show") + + let commandWithArgs = try OrganizationShowCommand.parse([ + "NewOrganization", + "--no-json", + "--path", "/new/show/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewOrganization") + XCTAssertFalse(commandWithArgs.json) + XCTAssertEqual(commandWithArgs.path, "/new/show/path") + } + + func testProjectListCommandUsesEnvVars() throws { + setVariable(.projectListJson, value: "true") + setVariable(.projectListPath, value: "/path/to/list") + + let commandWithEnvVars = try ProjectListCommand.parse([]) + XCTAssertTrue(commandWithEnvVars.json) + XCTAssertEqual(commandWithEnvVars.path, "/path/to/list") + + let commandWithArgs = try ProjectListCommand.parse([ + "--no-json", + "--path", "/new/list/path", + ]) + XCTAssertFalse(commandWithArgs.json) + XCTAssertEqual(commandWithArgs.path, "/new/list/path") + } + + func testOrganizationUpdateMemberCommandUsesEnvVars() throws { + setVariable(.organizationUpdateMemberOrganizationName, value: "MyOrganization") + setVariable(.organizationUpdateMemberUsername, value: "username") + setVariable(.organizationUpdateMemberRole, value: "admin") + setVariable(.organizationUpdateMemberPath, value: "/path/to/member") + + let commandWithEnvVars = try OrganizationUpdateMemberCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.organizationName, "MyOrganization") + XCTAssertEqual(commandWithEnvVars.username, "username") + XCTAssertEqual(commandWithEnvVars.role, "admin") + XCTAssertEqual(commandWithEnvVars.path, "/path/to/member") + + let commandWithArgs = try OrganizationUpdateMemberCommand.parse([ + "NewOrganization", + "newusername", + "--role", "user", + "--path", "/new/member/path", + ]) + XCTAssertEqual(commandWithArgs.organizationName, "NewOrganization") + XCTAssertEqual(commandWithArgs.username, "newusername") + XCTAssertEqual(commandWithArgs.role, "user") + XCTAssertEqual(commandWithArgs.path, "/new/member/path") + } + + func testAuthCommandUsesEnvVars() throws { + setVariable(.authPath, value: "/path/to/auth") + + let commandWithEnvVars = try AuthCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.path, "/path/to/auth") + + let commandWithArgs = try AuthCommand.parse([ + "--path", "/new/auth/path", + ]) + XCTAssertEqual(commandWithArgs.path, "/new/auth/path") + } + + func testSessionCommandUsesEnvVars() throws { + setVariable(.sessionPath, value: "/path/to/session") + + let commandWithEnvVars = try SessionCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.path, "/path/to/session") + + let commandWithArgs = try SessionCommand.parse([ + "--path", "/new/session/path", + ]) + XCTAssertEqual(commandWithArgs.path, "/new/session/path") + } + + func testLogoutCommandUsesEnvVars() throws { + setVariable(.logoutPath, value: "/path/to/logout") + + let commandWithEnvVars = try LogoutCommand.parse([]) + XCTAssertEqual(commandWithEnvVars.path, "/path/to/logout") + + let commandWithArgs = try LogoutCommand.parse([ + "--path", "/new/logout/path", + ]) + XCTAssertEqual(commandWithArgs.path, "/new/logout/path") + } +} diff --git a/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift b/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift index a96093d58c6..243bad066a3 100644 --- a/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift +++ b/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift @@ -1,6 +1,7 @@ import ArgumentParser import Foundation import TuistAnalytics +import TuistCore import TuistSupport import XCTest @@ -28,13 +29,16 @@ final class CommandEventFactoryTests: TuistUnitTestCase { func test_tagCommand_tagsExpectedCommand() throws { // Given let info = TrackableCommandInfo( + runId: "run-id", name: "cache", subcommand: "warm", parameters: ["foo": "bar"], commandArguments: ["cache", "warm"], - durationInMs: 5000 + durationInMs: 5000, + status: .failure("Failed!") ) let expectedEvent = CommandEvent( + runId: "run-id", name: "cache", subcommand: "warm", params: ["foo": "bar"], @@ -45,7 +49,8 @@ final class CommandEventFactoryTests: TuistUnitTestCase { swiftVersion: "5.1", macOSVersion: "10.15.0", machineHardwareName: "arm64", - isCI: false + isCI: false, + status: .failure("Failed!") ) // When diff --git a/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift b/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift index fe56aff609b..4505c8db95b 100644 --- a/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift +++ b/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift @@ -1,9 +1,9 @@ import AnyCodable import ArgumentParser -import Combine import Foundation import TuistAnalytics import TuistAsyncQueueTesting +import TuistCore import TuistSupport import XCTest @@ -25,9 +25,12 @@ final class TrackableCommandTests: TuistTestCase { super.tearDown() } - private func makeSubject(flag: Bool = true) { + private func makeSubject( + flag: Bool = true, + shouldFail: Bool = false + ) { subject = TrackableCommand( - command: TestCommand(flag: flag), + command: TestCommand(flag: flag, shouldFail: shouldFail), commandArguments: ["cache", "warm"], clock: WallClock(), asyncQueue: mockAsyncQueue @@ -64,18 +67,51 @@ final class TrackableCommandTests: TuistTestCase { XCTAssertEqual(event.name, "test") XCTAssertEqual(event.params, expectedParams) } + + func test_whenCommandFails_dispatchesEventWithExpectedInfo() async throws { + // Given + makeSubject(flag: false, shouldFail: true) + // When + await XCTAssertThrowsSpecific(try await subject.run(), TestCommand.TestError.commandFailed) + + // Then + XCTAssertEqual(mockAsyncQueue.invokedDispatchCount, 1) + let event = try XCTUnwrap(mockAsyncQueue.invokedDispatchParameters?.event as? CommandEvent) + XCTAssertEqual(event.name, "test") + XCTAssertEqual(event.status, .failure("Command failed")) + } } private struct TestCommand: ParsableCommand, HasTrackableParameters { + enum TestError: FatalError, Equatable { + case commandFailed + + var type: TuistSupport.ErrorType { + switch self { + case .commandFailed: + return .abort + } + } + + var description: String { + "Command failed" + } + } + static var configuration: CommandConfiguration { CommandConfiguration(commandName: "test") } var flag: Bool = false + var shouldFail: Bool = false static var analyticsDelegate: TrackableParametersDelegate? + var runId = "" func run() throws { + if shouldFail { + throw TestError.commandFailed + } TestCommand.analyticsDelegate?.addParameters(["flag": AnyCodable(flag)]) } } diff --git a/Tests/TuistKitTests/Dot/MockDotGraphGenerator.swift b/Tests/TuistKitTests/Dot/MockDotGraphGenerator.swift index 944afee749e..800311c7dde 100644 --- a/Tests/TuistKitTests/Dot/MockDotGraphGenerator.swift +++ b/Tests/TuistKitTests/Dot/MockDotGraphGenerator.swift @@ -1,14 +1,14 @@ import Foundation import GraphViz -import TSCBasic +import Path import TuistGenerator -import TuistGraph import TuistKit +import XcodeGraph final class MockGraphToGraphVizMapper: GraphToGraphVizMapping { var stubMap: GraphViz.Graph? func map( - graph _: TuistGraph.Graph, + graph _: XcodeGraph.Graph, targetsAndDependencies _: [GraphTarget: Set] ) -> GraphViz.Graph { stubMap ?? GraphViz.Graph() diff --git a/Tests/TuistKitTests/Generator/Mocks/MockGenerator.swift b/Tests/TuistKitTests/Generator/Mocks/MockGenerator.swift deleted file mode 100644 index f6a15ee1ff7..00000000000 --- a/Tests/TuistKitTests/Generator/Mocks/MockGenerator.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGenerator -import TuistGraph -import TuistGraphTesting -@testable import TuistKit - -final class MockGenerator: Generating { - enum MockError: Error { - case stubNotImplemented - } - - var generateCalls: [AbsolutePath] = [] - var generateStub: ((AbsolutePath) throws -> AbsolutePath)? - func generate(path: AbsolutePath) throws -> AbsolutePath { - guard let generateStub else { - throw MockError.stubNotImplemented - } - - generateCalls.append(path) - return try generateStub(path) - } - - var generateWithGraphCalls: [AbsolutePath] = [] - var generateWithGraphStub: ((AbsolutePath) throws -> (AbsolutePath, Graph))? - func generateWithGraph(path: AbsolutePath) throws -> (AbsolutePath, Graph) { - guard let generateWithGraphStub else { - throw MockError.stubNotImplemented - } - generateWithGraphCalls.append(path) - return try generateWithGraphStub(path) - } - - var invokedGenerateProjectWorkspace = false - var invokedGenerateProjectWorkspaceCount = 0 - var invokedGenerateProjectWorkspaceParameters: (path: AbsolutePath, Void)? - var invokedGenerateProjectWorkspaceParametersList = [(path: AbsolutePath, Void)]() - var stubbedGenerateProjectWorkspaceError: Error? - var stubbedGenerateProjectWorkspaceResult: (AbsolutePath, Graph)! - - func generateProjectWorkspace(path: AbsolutePath) throws -> (AbsolutePath, Graph) { - invokedGenerateProjectWorkspace = true - invokedGenerateProjectWorkspaceCount += 1 - invokedGenerateProjectWorkspaceParameters = (path, ()) - invokedGenerateProjectWorkspaceParametersList.append((path, ())) - if let error = stubbedGenerateProjectWorkspaceError { - throw error - } - return stubbedGenerateProjectWorkspaceResult - } - - var invokedLoadParameterPath: AbsolutePath? - var loadStub: ((AbsolutePath) throws -> Graph)? - func load(path: AbsolutePath) throws -> Graph { - invokedLoadParameterPath = path - if let loadStub { - return try loadStub(path) - } else { - return Graph.test() - } - } - - var loadProjectStub: ((AbsolutePath) throws -> (Project, Graph, [SideEffectDescriptor]))? - func loadProject(path: AbsolutePath) throws -> (Project, Graph, [SideEffectDescriptor]) { - if let loadProjectStub { - return try loadProjectStub(path) - } else { - return (Project.test(), Graph.test(), []) - } - } -} diff --git a/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift b/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift deleted file mode 100644 index 9f6cc5f9c16..00000000000 --- a/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -@testable import TuistKit - -final class MockGeneratorFactory: GeneratorFactorying { - var invokedTest = false - var invokedTestCount = 0 - var invokedTestParameters: ( - config: Config, - testsCacheDirectory: AbsolutePath, - testPlan: String?, - includedTargets: Set, - excludedTargets: Set, - skipUITests: Bool - )? - var invokedTestParametersList = - [( - config: Config, - testsCacheDirectory: AbsolutePath, - testPlan: String?, - includedTargets: Set, - excludedTargets: Set, - skipUITests: Bool - )]() - var stubbedTestResult: Generating! - - func test( - config: Config, - testsCacheDirectory: AbsolutePath, - testPlan: String?, - includedTargets: Set, - excludedTargets: Set, - skipUITests: Bool - ) -> Generating { - invokedTest = true - invokedTestCount += 1 - invokedTestParameters = ( - config, - testsCacheDirectory, - testPlan, - includedTargets, - excludedTargets, - skipUITests - ) - invokedTestParametersList - .append(( - config, - testsCacheDirectory, - testPlan, - includedTargets, - excludedTargets, - skipUITests - )) - return stubbedTestResult - } - - var invokedDefault = false - var invokedDefaultCount = 0 - var stubbedDefaultResult: 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 6450c11c2d8..835b2b27e83 100644 --- a/Tests/TuistKitTests/Mappers/Factories/GraphMapperFactoryTests.swift +++ b/Tests/TuistKitTests/Mappers/Factories/GraphMapperFactoryTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistAutomation import TuistCoreTesting -import TuistGraph import TuistLoader -import TuistSigning +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistGenerator @@ -53,4 +52,12 @@ final class GraphMapperFactoryTests: TuistUnitTestCase { // Then XCTAssertDoesntContainElementOfType(got, ExplicitDependencyGraphMapper.self) } + + func test_default_contains_the_modulemap_mapper() { + // When + let got = subject.default(config: .test()) + + // Then + XCTAssertContainsElementOfType(got, ModuleMapMapper.self) + } } diff --git a/Tests/TuistKitTests/Mappers/Factories/ProjectMapperFactoryTests.swift b/Tests/TuistKitTests/Mappers/Factories/ProjectMapperFactoryTests.swift index 8dc94644a27..0079ee98871 100644 --- a/Tests/TuistKitTests/Mappers/Factories/ProjectMapperFactoryTests.swift +++ b/Tests/TuistKitTests/Mappers/Factories/ProjectMapperFactoryTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistAutomation import TuistCoreTesting -import TuistGraph import TuistLoader -import TuistSigning +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistGenerator @@ -60,20 +59,24 @@ final class ProjectMapperFactoryTests: TuistUnitTestCase { XCTAssertContainsElementOfType(got, GenerateInfoPlistProjectMapper.self, after: DeleteDerivedDirectoryProjectMapper.self) } - func test_default_contains_the_ide_template_macros_mapper() { + func test_default_contains_the_generate_privacy_manifest_mapper() { // When let got = subject.default() // Then - XCTAssertContainsElementOfType(got, IDETemplateMacrosMapper.self) + XCTAssertContainsElementOfType( + got, + GeneratePrivacyManifestProjectMapper.self, + after: DeleteDerivedDirectoryProjectMapper.self + ) } - func test_default_contains_the_signing_mapper() { + func test_default_contains_the_ide_template_macros_mapper() { // When let got = subject.default() // Then - XCTAssertContainsElementOfType(got, SigningMapper.self) + XCTAssertContainsElementOfType(got, IDETemplateMacrosMapper.self) } func test_automation_contains_the_source_root_path_project_mapper() { diff --git a/Tests/TuistKitTests/Mappers/Factories/WorkspaceMapperFactoryTests.swift b/Tests/TuistKitTests/Mappers/Factories/WorkspaceMapperFactoryTests.swift index d66c341e21d..8522e4410a8 100644 --- a/Tests/TuistKitTests/Mappers/Factories/WorkspaceMapperFactoryTests.swift +++ b/Tests/TuistKitTests/Mappers/Factories/WorkspaceMapperFactoryTests.swift @@ -1,10 +1,9 @@ import Foundation -import TSCBasic +import Path import TSCUtility import TuistCoreTesting -import TuistGraph import TuistLoader -import TuistSigning +import XcodeGraph import XCTest @testable import TuistAutomation @testable import TuistCore @@ -82,17 +81,6 @@ final class WorkspaceMapperFactoryTests: TuistUnitTestCase { XCTAssertContainsElementOfType(got, AutogeneratedWorkspaceSchemeWorkspaceMapper.self) } - func test_default_contains_the_modulemap_mapper() { - // Given - subject = WorkspaceMapperFactory(projectMapper: SequentialProjectMapper(mappers: projectMapperFactory.default())) - - // When - let got = subject.default() - - // Then - XCTAssertContainsElementOfType(got, ModuleMapMapper.self) - } - func test_default_contains_the_last_upgrade_version_mapper() { // Given subject = WorkspaceMapperFactory(projectMapper: SequentialProjectMapper(mappers: projectMapperFactory.default())) diff --git a/Tests/TuistKitTests/Mappers/FocusTargetsGraphMappersTests.swift b/Tests/TuistKitTests/Mappers/FocusTargetsGraphMappersTests.swift index ac42112fa2b..5a45bba7e26 100644 --- a/Tests/TuistKitTests/Mappers/FocusTargetsGraphMappersTests.swift +++ b/Tests/TuistKitTests/Mappers/FocusTargetsGraphMappersTests.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistKit @@ -16,16 +16,9 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { let cTarget = Target.test(name: targetNames[2]) let subject = FocusTargetsGraphMappers(includedTargets: Set()) let path = try temporaryPath() - let project = Project.test(path: path) + let project = Project.test(path: path, targets: [aTarget, bTarget, cTarget]) let graph = Graph.test( projects: [project.path: project], - targets: [ - path: [ - aTarget.name: aTarget, - bTarget.name: bTarget, - cTarget.name: cTarget, - ], - ], dependencies: [ .target(name: bTarget.name, path: path): [ .target(name: aTarget.name, path: path), @@ -37,9 +30,9 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) + let pruningTargets = gotGraph.projects.values.flatMap(\.targets.values).sorted().filter(\.prune) - let pruningTargets = gotGraph.targets[path]?.values.filter(\.prune) ?? [] // Then XCTAssertEmpty(gotSideEffects) XCTAssertEmpty(pruningTargets.map(\.name)) @@ -56,24 +49,13 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { let subject = FocusTargetsGraphMappers(includedTargets: Set()) let projectPath = try temporaryPath().appending(component: "Project") let externalProjectPath = try temporaryPath().appending(component: "ExternalProject") - let project = Project.test(path: projectPath) - let externalProject = Project.test(path: externalProjectPath, isExternal: true) + let project = Project.test(path: projectPath, targets: [aTarget, bTarget, cTarget]) + let externalProject = Project.test(path: externalProjectPath, targets: [dTarget, eTarget], isExternal: true) let graph = Graph.test( projects: [ project.path: project, externalProject.path: externalProject, ], - targets: [ - projectPath: [ - aTarget.name: aTarget, - bTarget.name: bTarget, - cTarget.name: cTarget, - ], - externalProjectPath: [ - dTarget.name: dTarget, - eTarget.name: eTarget, - ], - ], dependencies: [ .target(name: bTarget.name, path: projectPath): [ .target(name: aTarget.name, path: projectPath), @@ -85,11 +67,10 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) - + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) + let pruningTargets = gotGraph.projects.values.flatMap(\.targets.values).sorted().filter(\.prune) let expectingTargets = [dTarget, eTarget] - let pruningTargets = (gotGraph.targets[projectPath]?.values.filter(\.prune) ?? []) + - (gotGraph.targets[externalProjectPath]?.values.filter(\.prune) ?? []) + // Then XCTAssertEmpty(gotSideEffects) XCTAssertEqual( @@ -106,16 +87,9 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { let cTarget = Target.test(name: targetNames[2]) let subject = FocusTargetsGraphMappers(includedTargets: [aTarget.name]) let path = try temporaryPath() - let project = Project.test(path: path) + let project = Project.test(path: path, targets: [aTarget, bTarget, cTarget]) let graph = Graph.test( projects: [project.path: project], - targets: [ - path: [ - aTarget.name: aTarget, - bTarget.name: bTarget, - cTarget.name: cTarget, - ], - ], dependencies: [ .target(name: bTarget.name, path: path): [ .target(name: aTarget.name, path: path), @@ -127,10 +101,11 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) let expectingTargets = [bTarget, cTarget] - let pruningTargets = gotGraph.targets[path]?.values.filter(\.prune) ?? [] + let pruningTargets = gotGraph.projects.values.flatMap(\.targets.values).sorted().filter(\.prune) + // Then XCTAssertEmpty(gotSideEffects) XCTAssertEqual( @@ -147,16 +122,9 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { let cTarget = Target.test(name: targetNames[2]) let subject = FocusTargetsGraphMappers(includedTargets: [bTarget.name]) let path = try temporaryPath() - let project = Project.test(path: path) + let project = Project.test(path: path, targets: [aTarget, bTarget, cTarget]) let graph = Graph.test( projects: [project.path: project], - targets: [ - path: [ - aTarget.name: aTarget, - bTarget.name: bTarget, - cTarget.name: cTarget, - ], - ], dependencies: [ .target(name: bTarget.name, path: path): [ .target(name: aTarget.name, path: path), @@ -168,10 +136,11 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) let expectingTargets = [cTarget] - let pruningTargets = gotGraph.targets[path]?.values.filter(\.prune) ?? [] + let pruningTargets = gotGraph.projects.values.flatMap(\.targets.values).sorted().filter(\.prune) + // Then XCTAssertEmpty(gotSideEffects) XCTAssertEqual( @@ -189,17 +158,9 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { let cTarget = Target.test(name: targetNames[2]) let subject = FocusTargetsGraphMappers(includedTargets: [aTarget.name]) let path = try temporaryPath() - let project = Project.test(path: path) + let project = Project.test(path: path, targets: [aTestTarget, aTarget, bTarget, cTarget]) let graph = Graph.test( projects: [project.path: project], - targets: [ - path: [ - aTestTarget.name: aTestTarget, - aTarget.name: aTarget, - bTarget.name: bTarget, - cTarget.name: cTarget, - ], - ], dependencies: [ .target(name: aTestTarget.name, path: path): [ .target(name: aTarget.name, path: path), @@ -214,10 +175,11 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) let expectingTargets = [bTarget, cTarget, aTestTarget] - let pruningTargets = gotGraph.targets[path]?.values.filter(\.prune) ?? [] + let pruningTargets = gotGraph.projects.values.flatMap(\.targets.values).sorted().filter(\.prune) + // Then XCTAssertEmpty(gotSideEffects) XCTAssertEqual( @@ -225,4 +187,32 @@ final class FocusTargetsGraphMappersTests: TuistUnitTestCase { expectingTargets.map(\.name).sorted() ) } + + func test_map_when_included_targets_do_not_exist() throws { + // Given + let targetNames = ["foo", "bar", "baz"] + let aTarget = Target.test(name: targetNames[0]) + let aTestTarget = Target.test(name: targetNames[0] + "Tests", product: .unitTests) + let cTarget = Target.test(name: targetNames[2]) + let subject = FocusTargetsGraphMappers(includedTargets: [aTarget.name, "bar"]) + let path = try temporaryPath() + let project = Project.test(path: path, targets: [aTestTarget, aTarget, cTarget]) + let graph = Graph.test( + projects: [project.path: project], + dependencies: [ + .target(name: aTestTarget.name, path: path): [ + .target(name: aTarget.name, path: path), + ], + .target(name: cTarget.name, path: path): [ + .target(name: aTarget.name, path: path), + ], + ] + ) + + // When + XCTAssertThrowsSpecific( + try subject.map(graph: graph, environment: MapperEnvironment()), + FocusTargetsGraphMappersError.targetsNotFound(["bar"]) + ) + } } diff --git a/Tests/TuistKitTests/Mappers/Graph/TreeShakePrunedTargetsGraphMapperTests.swift b/Tests/TuistKitTests/Mappers/Graph/TreeShakePrunedTargetsGraphMapperTests.swift index 425a973009c..e5c200d24d7 100644 --- a/Tests/TuistKitTests/Mappers/Graph/TreeShakePrunedTargetsGraphMapperTests.swift +++ b/Tests/TuistKitTests/Mappers/Graph/TreeShakePrunedTargetsGraphMapperTests.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistCoreTesting @@ -27,18 +27,16 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, - projects: [project.path: project], - targets: [project.path: [target.name: target]] + projects: [project.path: project] ) let expectedGraph = Graph.test( path: project.path, - projects: [:], - targets: [:] + projects: [:] ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEmpty(gotSideEffects) @@ -57,19 +55,18 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [firstTarget.name: firstTarget, secondTarget.name: secondTarget]], dependencies: [:] ) // When - let (gotGraph, gotValueSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotValueSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEmpty(gotValueSideEffects) XCTAssertEqual(gotGraph.projects.count, 1) - let valueTargets = gotGraph.targets.flatMap(\.value) + let valueTargets = gotGraph.projects.values.flatMap(\.targets.values).sorted() XCTAssertEqual(valueTargets.count, 1) - XCTAssertEqual(valueTargets.first?.value, firstTarget) + XCTAssertEqual(valueTargets.first, firstTarget) } func test_map_removes_project_schemes_with_whose_all_targets_have_been_removed() throws { @@ -85,12 +82,67 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [prunedTarget.name: prunedTarget, keptTarget.name: keptTarget]], dependencies: [:] ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) + + // Then + XCTAssertEmpty(gotSideEffects) + XCTAssertNotEmpty(gotGraph.projects) + XCTAssertEmpty(gotGraph.projects.values.flatMap(\.schemes)) + } + + func test_map_removes_project_schemes_with_whose_run_action_expand_variable_from_target_has_been_removed() throws { + // Given + let path = try AbsolutePath(validating: "/project") + let prunedTarget = Target.test(name: "first", prune: true) + let keptTarget = Target.test(name: "second", prune: false) + let schemes: [Scheme] = [ + .test( + buildAction: .test(targets: [.init(projectPath: path, name: keptTarget.name)]), + runAction: .test(expandVariableFromTarget: .init(projectPath: path, name: prunedTarget.name)) + ), + ] + let project = Project.test(path: path, targets: [prunedTarget, keptTarget], schemes: schemes) + + let graph = Graph.test( + path: project.path, + projects: [project.path: project], + dependencies: [:] + ) + + // When + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) + + // Then + XCTAssertEmpty(gotSideEffects) + XCTAssertNotEmpty(gotGraph.projects) + XCTAssertEmpty(gotGraph.projects.values.flatMap(\.schemes)) + } + + func test_map_removes_project_schemes_with_whose_test_action_expand_variable_from_target_has_been_removed() throws { + // Given + let path = try AbsolutePath(validating: "/project") + let prunedTarget = Target.test(name: "first", prune: true) + let keptTarget = Target.test(name: "second", prune: false) + let schemes: [Scheme] = [ + .test( + buildAction: .test(targets: [.init(projectPath: path, name: keptTarget.name)]), + testAction: .test(expandVariableFromTarget: .init(projectPath: path, name: prunedTarget.name)) + ), + ] + let project = Project.test(path: path, targets: [prunedTarget, keptTarget], schemes: schemes) + + let graph = Graph.test( + path: project.path, + projects: [project.path: project], + dependencies: [:] + ) + + // When + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEmpty(gotSideEffects) @@ -113,12 +165,11 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [prunedTarget.name: prunedTarget, keptTarget.name: keptTarget]], dependencies: [:] ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEmpty(gotSideEffects) @@ -153,12 +204,11 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { path: project.path, workspace: workspace, projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: [:] ) // When - let (gotGraph, _) = try subject.map(graph: graph) + let (gotGraph, _, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertFalse(gotGraph.workspace.projects.contains(removedProjectPath)) @@ -183,12 +233,11 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { path: project.path, workspace: workspace, projects: [project.path: project], - targets: [project.path: [target.name: target]], dependencies: [:] ) // When - let (gotGraph, _) = try subject.map(graph: graph) + let (gotGraph, _, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEmpty(gotGraph.workspace.schemes) @@ -214,8 +263,7 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let project = Project.test(path: path, targets: targets, schemes: [scheme]) let graph = Graph.test( path: project.path, - projects: [project.path: project], - targets: [project.path: Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) })] + projects: [project.path: project] ) let unprunedTargets = targets.filter { !$0.prune } @@ -231,12 +279,11 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let expectedProject = Project.test(path: path, targets: unprunedTargets, schemes: [schemeWithUnprunedTargets]) let expectedGraph = Graph.test( path: expectedProject.path, - projects: [expectedProject.path: expectedProject], - targets: [expectedProject.path: Dictionary(uniqueKeysWithValues: unprunedTargets.map { ($0.name, $0) })] + projects: [expectedProject.path: expectedProject] ) // When - let (gotGraph, _) = try subject.map(graph: graph) + let (gotGraph, _, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEqual(gotGraph, expectedGraph) @@ -253,19 +300,13 @@ final class TreeShakePrunedTargetsGraphMapperTests: TuistUnitTestCase { let graph = Graph.test( path: project.path, projects: [project.path: project], - targets: [project.path: [ - firstTarget.name: firstTarget, - secondTarget.name: secondTarget, - thirdTarget.name: thirdTarget, - prunedTarget.name: prunedTarget, - ]], dependencies: [:] ) let expectedProject = Project.test(targets: [firstTarget, secondTarget, thirdTarget]) // When - let (gotGraph, _) = try subject.map(graph: graph) + let (gotGraph, _, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEqual(gotGraph.projects.first?.value, expectedProject) diff --git a/Tests/TuistKitTests/Mappers/Graph/UpdateWorkspaceProjectsGraphMapperTests.swift b/Tests/TuistKitTests/Mappers/Graph/UpdateWorkspaceProjectsGraphMapperTests.swift index 3a1fa11b6b1..214f6998c79 100644 --- a/Tests/TuistKitTests/Mappers/Graph/UpdateWorkspaceProjectsGraphMapperTests.swift +++ b/Tests/TuistKitTests/Mappers/Graph/UpdateWorkspaceProjectsGraphMapperTests.swift @@ -1,15 +1,13 @@ import Foundation -import TSCBasic +import Path import TuistCoreTesting import TuistGenerator -import TuistGraph -import TuistGraphTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistCore @testable import TuistKit -@testable import TuistSigning @testable import TuistSupportTesting final class UpdateWorkspaceProjectsGraphMapperTests: TuistUnitTestCase { @@ -39,7 +37,7 @@ final class UpdateWorkspaceProjectsGraphMapperTests: TuistUnitTestCase { ) // When - let (gotGraph, gotSideEffects) = try subject.map(graph: graph) + let (gotGraph, gotSideEffects, _) = try subject.map(graph: graph, environment: MapperEnvironment()) // Then XCTAssertEmpty(gotSideEffects) diff --git a/Tests/TuistKitTests/Mappers/Workspace/TuistWorkspaceRenderMarkdownReadmeMapperTests.swift b/Tests/TuistKitTests/Mappers/Workspace/TuistWorkspaceRenderMarkdownReadmeMapperTests.swift index 6d99cd7f881..97c9aabd0dd 100644 --- a/Tests/TuistKitTests/Mappers/Workspace/TuistWorkspaceRenderMarkdownReadmeMapperTests.swift +++ b/Tests/TuistKitTests/Mappers/Workspace/TuistWorkspaceRenderMarkdownReadmeMapperTests.swift @@ -1,8 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraph +import XcodeGraph import XCTest @testable import TuistKit diff --git a/Tests/TuistKitTests/Mocks/MockManifestGraphLoader.swift b/Tests/TuistKitTests/Mocks/MockManifestGraphLoader.swift deleted file mode 100644 index d82a810d659..00000000000 --- a/Tests/TuistKitTests/Mocks/MockManifestGraphLoader.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -import TuistGraphTesting -@testable import TuistKit - -final class MockManifestGraphLoader: ManifestGraphLoading { - var stubLoadGraph: Graph? - func load(path _: AbsolutePath) async throws -> (Graph, [SideEffectDescriptor], [LintingIssue]) { - (stubLoadGraph ?? .test(), [], []) - } -} diff --git a/Tests/TuistKitTests/Mocks/MockTuistVersionLoader.swift b/Tests/TuistKitTests/Mocks/MockTuistVersionLoader.swift new file mode 100644 index 00000000000..815cd7b1102 --- /dev/null +++ b/Tests/TuistKitTests/Mocks/MockTuistVersionLoader.swift @@ -0,0 +1,10 @@ +import Foundation +@testable import TuistKit + +public final class MockTuistVersionLoader: TuistVersionLoading { + var getVersionStub: String = "4.0.1" + private(set) var getVersionCalls = 0 + public func getVersion() throws -> String { + getVersionStub + } +} diff --git a/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift b/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift index 347fa9106d6..aa0c2fa7ed5 100644 --- a/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift +++ b/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift @@ -1,9 +1,8 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistLoader +import XcodeGraph @testable import TuistCoreTesting @testable import TuistKit @@ -16,7 +15,6 @@ final class MockProjectEditorMapper: ProjectEditorMapping { sourceRootPath: AbsolutePath, destinationDirectory: AbsolutePath, configPath: AbsolutePath?, - dependenciesPath: AbsolutePath?, packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], @@ -35,7 +33,6 @@ final class MockProjectEditorMapper: ProjectEditorMapping { sourceRootPath: AbsolutePath, destinationDirectory: AbsolutePath, configPath: AbsolutePath?, - dependenciesPath: AbsolutePath?, packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], @@ -53,7 +50,6 @@ final class MockProjectEditorMapper: ProjectEditorMapping { sourceRootPath: sourceRootPath, destinationDirectory: destinationDirectory, configPath: configPath, - dependenciesPath: dependenciesPath, packageManifestPath: packageManifestPath, projectManifests: projectManifests, editablePluginManifests: editablePluginManifests, diff --git a/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift b/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift index 9648af52d85..e591a60224d 100644 --- a/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift +++ b/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift @@ -1,11 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TSCUtility import TuistCore -import TuistGraph -import TuistGraphTesting import TuistLoader import TuistSupport +import XcodeGraph import XCTest @testable import TuistKit @@ -17,7 +17,11 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { override func setUp() { super.setUp() - system.swiftVersionStub = { "5.2" } + + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + developerEnvironment.stubbedArchitecture = .arm64 swiftPackageManagerController = MockSwiftPackageManagerController() subject = ProjectEditorMapper( @@ -36,7 +40,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { let sourceRootPath = try temporaryPath() 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") } @@ -52,9 +55,11 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath.appending(component: "PluginThree"), ].map { EditablePluginManifest(name: $0.basename, path: $0) } swiftPackageManagerController.getToolsVersionStub = { _ in - Version("5.5.0") + .init(stringLiteral: "5.5.0") } - xcodeController.selectedStub = .success(.test(path: AbsolutePath("/Applications/Xcode.app"))) + given(xcodeController) + .selected() + .willReturn(.test(path: AbsolutePath("/Applications/Xcode.app"))) // When let graph = try subject.map( @@ -63,7 +68,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: configPath, - dependenciesPath: dependenciesPath, packageManifestPath: packageManifestPath, projectManifests: projectManifestPaths, editablePluginManifests: pluginPaths, @@ -77,18 +81,18 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ) let project = try XCTUnwrap(graph.projects.values.first(where: { $0.name == projectName })) - - let targets = graph.targets.values.lazy - .flatMap(\.values) - .sorted(by: { $0.name < $1.name }) + let targets = graph.projects.values.flatMap(\.targets.values).sorted(by: { $0.name < $1.name }) // Then XCTAssertEqual(graph.name, "TestManifests") - XCTAssertEqual(targets.count, 10) + XCTAssertEqual(targets.count, 9) // Generated Manifests target - let manifestsTarget = try XCTUnwrap(project.targets.first(where: { $0.name == sourceRootPath.basename + projectName })) + let manifestsTarget = try XCTUnwrap( + project.targets.values.sorted() + .first(where: { $0.name == sourceRootPath.basename + projectName }) + ) XCTAssertEqual(targets.last, manifestsTarget) XCTAssertEqual(manifestsTarget.destinations, .macOS) @@ -106,7 +110,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ])) // Generated Helpers target - let helpersTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "ProjectDescriptionHelpers" })) + let helpersTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "ProjectDescriptionHelpers" })) XCTAssertTrue(targets.contains(helpersTarget)) XCTAssertEqual(helpersTarget.name, "ProjectDescriptionHelpers") @@ -125,7 +129,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ])) // Generated Templates target - let templatesTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Templates" })) + let templatesTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "Templates" })) XCTAssertTrue(targets.contains(templatesTarget)) XCTAssertEqual(templatesTarget.name, "Templates") @@ -143,7 +147,10 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ])) // Generated ResourceSynthesizers target - let resourceSynthesizersTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "ResourceSynthesizers" })) + let resourceSynthesizersTarget = try XCTUnwrap( + project.targets.values.sorted() + .last(where: { $0.name == "ResourceSynthesizers" }) + ) XCTAssertTrue(targets.contains(resourceSynthesizersTarget)) XCTAssertEqual(resourceSynthesizersTarget.name, "ResourceSynthesizers") @@ -160,7 +167,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ])) // Generated Stencils target - let stencilsTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Stencils" })) + let stencilsTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "Stencils" })) XCTAssertTrue(targets.contains(stencilsTarget)) XCTAssertEqual(stencilsTarget.name, "Stencils") @@ -177,7 +184,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ])) // Generated Config target - let configTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Config" })) + let configTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "Config" })) XCTAssertTrue(targets.contains(configTarget)) XCTAssertEqual(configTarget.name, "Config") @@ -191,27 +198,8 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEqual(configTarget.filesGroup, projectsGroup) XCTAssertEmpty(configTarget.dependencies) - // Generated Dependencies target - let dependenciesTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Dependencies" })) - XCTAssertTrue(targets.contains(dependenciesTarget)) - - XCTAssertEqual(dependenciesTarget.name, "Dependencies") - XCTAssertEqual(dependenciesTarget.destinations, .macOS) - XCTAssertEqual(dependenciesTarget.product, .staticFramework) - XCTAssertEqual( - dependenciesTarget.settings, - expectedSettings(includePaths: [projectDescriptionPath, projectDescriptionPath.parentDirectory]) - ) - XCTAssertEqual(dependenciesTarget.sources.map(\.path), [dependenciesPath]) - XCTAssertEqual(dependenciesTarget.filesGroup, projectsGroup) - XCTAssertEqual(Set(dependenciesTarget.dependencies), Set([ - .target(name: "ProjectDescriptionHelpers"), - .target(name: "PluginTwo"), - .target(name: "PluginThree"), - ])) - // Generated Packages target - let packagesTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Packages" })) + let packagesTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "Packages" })) XCTAssertTrue(targets.contains(packagesTarget)) XCTAssertEqual(packagesTarget.name, "Packages") @@ -233,7 +221,14 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/ManifestAPI", ]), ], - uniquingKeysWith: { $1 } + uniquingKeysWith: { + switch ($0, $1) { + case let (.array(leftArray), .array(rightArray)): + return SettingValue.array(leftArray + rightArray) + default: + return $1 + } + } ) ) XCTAssertEqual( @@ -260,7 +255,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEqual(scheme.name, projectName) let buildAction = try XCTUnwrap(scheme.buildAction) - XCTAssertEqual(buildAction.targets.lazy.map(\.name).sorted(), project.targets.map(\.name).sorted()) + XCTAssertEqual(buildAction.targets.lazy.map(\.name).sorted(), project.targets.values.map(\.name).sorted()) let runAction = try XCTUnwrap(scheme.runAction) XCTAssertEqual(runAction.filePath, tuistPath) @@ -288,7 +283,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: nil, - dependenciesPath: nil, packageManifestPath: nil, projectManifests: projectManifestPaths, editablePluginManifests: [], @@ -302,17 +296,17 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ) let project = try XCTUnwrap(graph.projects.values.first) - - let targets = graph.targets.values.lazy - .flatMap(\.values) - .sorted(by: { $0.name < $1.name }) + let targets = graph.projects.values.flatMap(\.targets.values).sorted(by: { $0.name < $1.name }) // Then XCTAssertEqual(targets.count, 1) XCTAssertEmpty(targets.flatMap(\.dependencies)) // Generated Manifests target - let manifestsTarget = try XCTUnwrap(project.targets.last(where: { $0.name == sourceRootPath.basename + projectName })) + let manifestsTarget = try XCTUnwrap( + project.targets.values.sorted() + .last(where: { $0.name == sourceRootPath.basename + projectName }) + ) XCTAssertEqual(manifestsTarget.destinations, .macOS) XCTAssertEqual(manifestsTarget.product, .staticFramework) @@ -352,7 +346,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { // Given let sourceRootPath = try temporaryPath() let configPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Config.swift") - let dependenciesPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Dependencies.swift") let otherProjectPath = "Module" let projectManifestPaths = [ sourceRootPath.appending(component: "Project.swift"), @@ -373,7 +366,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: configPath, - dependenciesPath: dependenciesPath, packageManifestPath: nil, projectManifests: projectManifestPaths, editablePluginManifests: [], @@ -387,18 +379,15 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ) let project = try XCTUnwrap(graph.projects.values.first) - - let targets = graph.targets.values.lazy - .flatMap(\.values) - .sorted(by: { $0.name < $1.name }) + let targets = graph.projects.values.flatMap(\.targets.values).sorted(by: { $0.name < $1.name }) // Then - XCTAssertEqual(targets.count, 4) + XCTAssertEqual(targets.count, 3) XCTAssertEmpty(targets.flatMap(\.dependencies)) // Generated Manifests target - let manifestOneTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "ModuleManifests" })) + let manifestOneTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "ModuleManifests" })) XCTAssertEqual(manifestOneTarget.name, "ModuleManifests") XCTAssertEqual(manifestOneTarget.destinations, .macOS) @@ -412,7 +401,10 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEmpty(manifestOneTarget.dependencies) // Generated Manifests target - let manifestTwoTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "\(sourceRootPath.basename)Manifests" })) + let manifestTwoTarget = try XCTUnwrap( + project.targets.values.sorted() + .last(where: { $0.name == "\(sourceRootPath.basename)Manifests" }) + ) XCTAssertEqual(manifestTwoTarget.destinations, .macOS) XCTAssertEqual(manifestTwoTarget.product, .staticFramework) @@ -425,7 +417,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEmpty(manifestTwoTarget.dependencies) // Generated Config target - let configTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Config" })) + let configTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "Config" })) XCTAssertEqual(configTarget.name, "Config") XCTAssertEqual(configTarget.destinations, .macOS) @@ -484,7 +476,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: nil, - dependenciesPath: nil, packageManifestPath: nil, projectManifests: [], editablePluginManifests: editablePluginManifests, @@ -498,17 +489,14 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ) let project = try XCTUnwrap(graph.projects.values.first) - - let targets = graph.targets.values.lazy - .flatMap(\.values) - .sorted(by: { $0.name < $1.name }) + let targets = graph.projects.values.flatMap(\.targets.values).sorted(by: { $0.name < $1.name }) // Then XCTAssertEqual(targets.count, 1) XCTAssertEmpty(targets.flatMap(\.dependencies)) // Generated Plugin target - let pluginTarget = try XCTUnwrap(project.targets.last(where: { $0.name == sourceRootPath.basename })) + let pluginTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == sourceRootPath.basename })) XCTAssertEqual(pluginTarget.destinations, .macOS) XCTAssertEqual(pluginTarget.product, .staticFramework) @@ -567,7 +555,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: nil, - dependenciesPath: nil, packageManifestPath: nil, projectManifests: [], editablePluginManifests: editablePluginManifests, @@ -581,17 +568,14 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { ) let project = try XCTUnwrap(graph.projects.values.first) - - let targets = graph.targets.values.lazy - .flatMap(\.values) - .sorted(by: { $0.name < $1.name }) + let targets = graph.projects.values.flatMap(\.targets.values).sorted(by: { $0.name < $1.name }) // Then XCTAssertEqual(targets.count, 2) XCTAssertEmpty(targets.flatMap(\.dependencies)) // Generated first plugin target - let firstPluginTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "A" })) + let firstPluginTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "A" })) XCTAssertEqual(firstPluginTarget.destinations, .macOS) XCTAssertEqual(firstPluginTarget.product, .staticFramework) @@ -604,7 +588,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEmpty(firstPluginTarget.dependencies) // Generated second plugin target - let secondPluginTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "B" })) + let secondPluginTarget = try XCTUnwrap(project.targets.values.sorted().last(where: { $0.name == "B" })) XCTAssertEqual(secondPluginTarget.destinations, .macOS) XCTAssertEqual(secondPluginTarget.product, .staticFramework) @@ -684,7 +668,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: nil, - dependenciesPath: nil, packageManifestPath: nil, projectManifests: [], editablePluginManifests: editablePluginManifests, @@ -699,7 +682,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { // Then let project = try XCTUnwrap(graph.projects.values.first) - let pluginTarget = try XCTUnwrap(project.targets.first) + let pluginTarget = try XCTUnwrap(project.targets.values.first) XCTAssertEqual( pluginTarget.sources, @@ -731,7 +714,6 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath: sourceRootPath, destinationDirectory: sourceRootPath, configPath: nil, - dependenciesPath: nil, packageManifestPath: nil, projectManifests: projectManifestPaths, editablePluginManifests: [localPlugin], @@ -746,10 +728,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { let pluginsProject = try XCTUnwrap(graph.projects.values.first(where: { $0.name == pluginsProjectName })) let manifestsProject = try XCTUnwrap(graph.projects.values.first(where: { $0.name == manifestsProjectName })) - - let targets = graph.targets.values.lazy - .flatMap(\.values) - .sorted(by: { $0.name < $1.name }) + let targets = graph.projects.values.flatMap(\.targets.values).sorted(by: { $0.name < $1.name }) let localPluginTarget = try XCTUnwrap(targets.first(where: { $0.name == "ALocalPlugin" })) let helpersTarget = try XCTUnwrap(targets.first(where: { $0.name == "ProjectDescriptionHelpers" })) let manifestsTarget = try XCTUnwrap(targets.first(where: { $0 != localPluginTarget && $0 != helpersTarget })) diff --git a/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift b/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift index 4f9a5e308f8..7ddfd2d064d 100644 --- a/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift +++ b/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift @@ -1,12 +1,12 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph -import TuistGraphTesting import TuistLoader import TuistPlugin import TuistPluginTesting import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @@ -33,7 +33,7 @@ final class ProjectEditorTests: TuistUnitTestCase { private var generator: MockDescriptorGenerator! private var projectEditorMapper: MockProjectEditorMapper! private var resourceLocator: MockResourceLocator! - private var manifestFilesLocator: MockManifestFilesLocator! + private var manifestFilesLocator: MockManifestFilesLocating! private var helpersDirectoryLocator: MockHelpersDirectoryLocator! private var writer: MockXcodeProjWriter! private var templatesDirectoryLocator: MockTemplatesDirectoryLocator! @@ -46,7 +46,7 @@ final class ProjectEditorTests: TuistUnitTestCase { generator = MockDescriptorGenerator() projectEditorMapper = MockProjectEditorMapper() resourceLocator = MockResourceLocator() - manifestFilesLocator = MockManifestFilesLocator() + manifestFilesLocator = MockManifestFilesLocating() helpersDirectoryLocator = MockHelpersDirectoryLocator() writer = MockXcodeProjWriter() templatesDirectoryLocator = MockTemplatesDirectoryLocator() @@ -78,7 +78,7 @@ final class ProjectEditorTests: TuistUnitTestCase { super.tearDown() } - func test_edit() throws { + func test_edit() async throws { // Given let directory = try temporaryPath() let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework") @@ -95,7 +95,6 @@ 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.swift") let packageManifestPath = directory.appending(components: "Tuist", "Package.swift") try FileHandler.shared.createFolder(directory.appending(component: "a folder")) try FileHandler.shared.write( @@ -108,21 +107,23 @@ final class ProjectEditorTests: TuistUnitTestCase { ) resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locateProjectManifestsStub = { _, excluding, _ in - XCTAssertEqual( - excluding, + given(manifestFilesLocator).locateProjectManifests( + at: .any, + excluding: .value( [ - "**/Tuist/Dependencies/**", "**/.build/**", "\(directory.pathString)/a folder/**", "\(directory.pathString)/B.swift", ] - ) - return manifests - } - manifestFilesLocator.locateConfigStub = configPath - manifestFilesLocator.locateDependenciesStub = dependenciesPath - manifestFilesLocator.locatePackageManifestStub = packageManifestPath + ), + onlyCurrentDirectory: .any + ) + .willReturn(manifests) + given(manifestFilesLocator).locateConfig(at: .any).willReturn(configPath) + given(manifestFilesLocator).locatePackageManifest(at: .any).willReturn(packageManifestPath) + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([]) helpersDirectoryLocator.locateStub = helpersDirectory projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in @@ -130,7 +131,7 @@ final class ProjectEditorTests: TuistUnitTestCase { } // When - try _ = subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()) + try _ = await subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()) // Then XCTAssertEqual(projectEditorMapper.mapArgs.count, 1) @@ -140,11 +141,10 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.sourceRootPath, directory) 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 { + func test_edit_when_there_are_no_editable_files() async throws { // Given let directory = try temporaryPath() let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework") @@ -153,10 +153,18 @@ final class ProjectEditorTests: TuistUnitTestCase { try FileHandler.shared.createFolder(helpersDirectory) resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locateProjectManifestsStub = { _, _, _ in - [] - } - manifestFilesLocator.locatePluginManifestsStub = [] + given(manifestFilesLocator) + .locateProjectManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([]) + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([]) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateConfig(at: .any) + .willReturn(nil) helpersDirectoryLocator.locateStub = helpersDirectory projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in @@ -164,14 +172,14 @@ final class ProjectEditorTests: TuistUnitTestCase { } // Then - XCTAssertThrowsSpecific( + await XCTAssertThrowsSpecific( // When - try subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()), + try await subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()), ProjectEditorError.noEditableFiles(directory) ) } - func test_edit_with_plugin() throws { + func test_edit_with_plugin() async throws { // Given let directory = try temporaryPath() let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework") @@ -181,7 +189,18 @@ final class ProjectEditorTests: TuistUnitTestCase { // When resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locatePluginManifestsStub = [pluginManifest] + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([pluginManifest]) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateConfig(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateProjectManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([]) projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in @@ -189,7 +208,7 @@ final class ProjectEditorTests: TuistUnitTestCase { } // When - try _ = subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()) + try _ = await subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()) // Then XCTAssertEqual(projectEditorMapper.mapArgs.count, 1) @@ -201,7 +220,7 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.pluginProjectDescriptionHelpersModule, []) } - func test_edit_with_many_plugins() throws { + func test_edit_with_many_plugins() async throws { // Given let directory = try temporaryPath() let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework") @@ -216,7 +235,18 @@ final class ProjectEditorTests: TuistUnitTestCase { // When resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locatePluginManifestsStub = pluginManifests + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn(pluginManifests) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateConfig(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateProjectManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([]) projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in @@ -224,7 +254,7 @@ final class ProjectEditorTests: TuistUnitTestCase { } // When - try _ = subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()) + try _ = await subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: .test()) // Then XCTAssertEqual(projectEditorMapper.mapArgs.count, 1) @@ -236,7 +266,7 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.pluginProjectDescriptionHelpersModule, []) } - func test_edit_project_with_local_plugins() throws { + func test_edit_project_with_local_plugins() async throws { // Given let directory = try temporaryPath() let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework") @@ -261,10 +291,18 @@ final class ProjectEditorTests: TuistUnitTestCase { let tuistPath = try AbsolutePath(validating: ProcessInfo.processInfo.arguments.first!) resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locateProjectManifestsStub = { _, _, _ in - manifests - } - manifestFilesLocator.locatePluginManifestsStub = [pluginManifestPath] + given(manifestFilesLocator) + .locateProjectManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn(manifests) + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([pluginManifestPath]) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateConfig(at: .any) + .willReturn(nil) projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in .test(xcworkspacePath: directory.appending(component: "Edit.xcworkspacepath")) @@ -274,7 +312,7 @@ final class ProjectEditorTests: TuistUnitTestCase { let plugins = Plugins.test(projectDescriptionHelpers: [ .init(name: "LocalPlugin", path: pluginManifestPath, location: .local), ]) - try _ = subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: plugins) + try _ = await subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: plugins) // Then XCTAssertEqual(projectEditorMapper.mapArgs.count, 1) @@ -287,7 +325,7 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.pluginProjectDescriptionHelpersModule, []) } - func test_edit_project_with_local_plugin_outside_editing_path() throws { + func test_edit_project_with_local_plugin_outside_editing_path() async throws { // Given let rootPath = try temporaryPath() let editingPath = rootPath.appending(component: "Editing") @@ -308,10 +346,18 @@ final class ProjectEditorTests: TuistUnitTestCase { let tuistPath = try AbsolutePath(validating: ProcessInfo.processInfo.arguments.first!) resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locateProjectManifestsStub = { _, _, _ in - manifests - } - manifestFilesLocator.locatePluginManifestsStub = [pluginManifestPath] + given(manifestFilesLocator) + .locateProjectManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn(manifests) + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([pluginManifestPath]) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateConfig(at: .any) + .willReturn(nil) projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in .test(xcworkspacePath: editingPath.appending(component: "Edit.xcworkspacepath")) @@ -322,7 +368,7 @@ final class ProjectEditorTests: TuistUnitTestCase { .init(name: "LocalPlugin", path: pluginManifestPath, location: .local), ]) - try _ = subject.edit(at: editingPath, in: editingPath, onlyCurrentDirectory: false, plugins: plugins) + try _ = await subject.edit(at: editingPath, in: editingPath, onlyCurrentDirectory: false, plugins: plugins) // Then XCTAssertEqual(projectEditorMapper.mapArgs.count, 1) @@ -335,7 +381,7 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.pluginProjectDescriptionHelpersModule, []) } - func test_edit_project_with_remote_plugin() throws { + func test_edit_project_with_remote_plugin() async throws { // Given let directory = try temporaryPath() let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework") @@ -351,10 +397,18 @@ final class ProjectEditorTests: TuistUnitTestCase { let tuistPath = try AbsolutePath(validating: ProcessInfo.processInfo.arguments.first!) resourceLocator.projectDescriptionStub = { projectDescriptionPath } - manifestFilesLocator.locateProjectManifestsStub = { _, _, _ in - manifests - } - manifestFilesLocator.locatePluginManifestsStub = [] + given(manifestFilesLocator) + .locateProjectManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn(manifests) + given(manifestFilesLocator) + .locatePluginManifests(at: .any, excluding: .any, onlyCurrentDirectory: .any) + .willReturn([]) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + given(manifestFilesLocator) + .locateConfig(at: .any) + .willReturn(nil) projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in .test(xcworkspacePath: directory.appending(component: "Edit.xcworkspacepath")) @@ -368,7 +422,7 @@ final class ProjectEditorTests: TuistUnitTestCase { let plugins = Plugins.test(projectDescriptionHelpers: [ .init(name: "RemotePlugin", path: try AbsolutePath(validating: "/Some/Path/To/Plugin"), location: .remote), ]) - try _ = subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: plugins) + try _ = await subject.edit(at: directory, in: directory, onlyCurrentDirectory: false, plugins: plugins) // Then XCTAssertEqual(projectEditorMapper.mapArgs.count, 1) diff --git a/Tests/TuistKitTests/Services/AuthServiceTests.swift b/Tests/TuistKitTests/Services/AuthServiceTests.swift new file mode 100644 index 00000000000..c8778318606 --- /dev/null +++ b/Tests/TuistKitTests/Services/AuthServiceTests.swift @@ -0,0 +1,205 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistCoreTesting +import TuistLoader +import TuistLoaderTesting +import TuistServer +import TuistSupport +import XcodeGraph +import XCTest +@testable import TuistKit +@testable import TuistSupportTesting + +final class AuthServiceTests: TuistUnitTestCase { + private var serverSessionController: MockServerSessionControlling! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + private var authenticateService: MockAuthenticateServicing! + private var serverCredentialsStore: MockServerCredentialsStoring! + private var serverURLService: MockServerURLServicing! + private var userInputReader: MockUserInputReading! + private var subject: AuthService! + + override func setUp() { + super.setUp() + serverSessionController = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + authenticateService = .init() + serverCredentialsStore = .init() + serverURLService = .init() + userInputReader = .init() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(url: serverURL)) + + given(serverURLService) + .url(configServerURL: .any) + .willReturn(serverURL) + + subject = AuthService( + serverSessionController: serverSessionController, + serverURLService: serverURLService, + configLoader: configLoader, + userInputReader: userInputReader, + authenticateService: authenticateService, + serverCredentialsStore: serverCredentialsStore + ) + } + + override func tearDown() { + serverSessionController = nil + configLoader = nil + serverURL = nil + authenticateService = nil + serverCredentialsStore = nil + serverURLService = nil + userInputReader = nil + subject = nil + super.tearDown() + } + + func test_authenticate() async throws { + // Given + given(serverSessionController) + .authenticate(serverURL: .value(serverURL)) + .willReturn(()) + + // When / Then + try await subject.authenticate( + email: nil, + password: nil, + directory: nil + ) + } + + func test_authenticate_when_password_is_provided() async throws { + // Given + given(userInputReader) + .readString(asking: .value("Email:")) + .willReturn("email@tuist.io") + + given(serverCredentialsStore) + .store( + credentials: .value( + ServerCredentials( + token: nil, + accessToken: "access-token", + refreshToken: "refresh-token" + ) + ), + serverURL: .any + ) + .willReturn() + + given(authenticateService) + .authenticate( + email: .value("email@tuist.io"), + password: .value("password"), + serverURL: .value(serverURL) + ) + .willReturn( + ServerAuthenticationTokens( + accessToken: "access-token", + refreshToken: "refresh-token" + ) + ) + + // When + try await subject.authenticate( + email: nil, + password: "password", + directory: nil + ) + + // Then + XCTAssertStandardOutput(pattern: "Credentials stored successfully.") + } + + func test_authenticate_when_email_is_provided() async throws { + // Given + given(userInputReader) + .readString(asking: .value("Password:")) + .willReturn("password") + + given(serverCredentialsStore) + .store( + credentials: .value( + ServerCredentials( + token: nil, + accessToken: "access-token", + refreshToken: "refresh-token" + ) + ), + serverURL: .any + ) + .willReturn() + + given(authenticateService) + .authenticate( + email: .value("email@tuist.io"), + password: .value("password"), + serverURL: .value(serverURL) + ) + .willReturn( + ServerAuthenticationTokens( + accessToken: "access-token", + refreshToken: "refresh-token" + ) + ) + + // When + try await subject.authenticate( + email: "email@tuist.io", + password: nil, + directory: nil + ) + + // Then + XCTAssertStandardOutput(pattern: "Credentials stored successfully.") + } + + func test_authenticate_when_email_and_password_are_provided() async throws { + // Given + given(serverCredentialsStore) + .store( + credentials: .value( + ServerCredentials( + token: nil, + accessToken: "access-token", + refreshToken: "refresh-token" + ) + ), + serverURL: .any + ) + .willReturn() + + given(authenticateService) + .authenticate( + email: .value("email@tuist.io"), + password: .value("password"), + serverURL: .value(serverURL) + ) + .willReturn( + ServerAuthenticationTokens( + accessToken: "access-token", + refreshToken: "refresh-token" + ) + ) + + // When + try await subject.authenticate( + email: "email@tuist.io", + password: "password", + directory: nil + ) + + // Then + XCTAssertStandardOutput(pattern: "Credentials stored successfully.") + verify(userInputReader) + .readString(asking: .any) + .called(0) + } +} diff --git a/Tests/TuistKitTests/Services/BuildServiceTests.swift b/Tests/TuistKitTests/Services/BuildServiceTests.swift index 16abed287e2..2b9f1568b52 100644 --- a/Tests/TuistKitTests/Services/BuildServiceTests.swift +++ b/Tests/TuistKitTests/Services/BuildServiceTests.swift @@ -1,10 +1,12 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TSCUtility +import TuistAutomation import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistServer import TuistSupport +import XcodeGraph import XCTest @testable import TuistAutomationTesting @@ -36,21 +38,38 @@ final class BuildServiceErrorTests: TuistUnitTestCase { } final class BuildServiceTests: TuistUnitTestCase { - var generator: MockGenerator! - var generatorFactory: MockGeneratorFactory! - var buildGraphInspector: MockBuildGraphInspector! - var targetBuilder: MockTargetBuilder! - var subject: BuildService! + private var generator: MockGenerating! + private var generatorFactory: MockGeneratorFactorying! + private var buildGraphInspector: MockBuildGraphInspecting! + private var targetBuilder: MockTargetBuilder! + private var cacheStorageFactory: MockCacheStorageFactorying! + private var subject: BuildService! override func setUp() { super.setUp() - generator = MockGenerator() - generatorFactory = MockGeneratorFactory() - generatorFactory.stubbedDefaultResult = generator - buildGraphInspector = MockBuildGraphInspector() + generator = .init() + generatorFactory = .init() + given(generatorFactory) + .building( + config: .any, + configuration: .any, + ignoreBinaryCache: .any, + cacheStorage: .any + ) + .willReturn(generator) + buildGraphInspector = .init() + given(buildGraphInspector) + .buildableEntrySchemes(graphTraverser: .any) + .willReturn([]) + targetBuilder = MockTargetBuilder() + cacheStorageFactory = .init() + given(cacheStorageFactory) + .cacheStorage(config: .any) + .willReturn(MockCacheStoring()) subject = BuildService( generatorFactory: generatorFactory, + cacheStorageFactory: cacheStorageFactory, buildGraphInspector: buildGraphInspector, targetBuilder: targetBuilder ) @@ -61,6 +80,7 @@ final class BuildServiceTests: TuistUnitTestCase { generatorFactory = nil buildGraphInspector = nil targetBuilder = nil + cacheStorageFactory = nil subject = nil super.tearDown() } @@ -76,29 +96,28 @@ final class BuildServiceTests: TuistUnitTestCase { let buildArguments: [XcodeBuildArgument] = [.sdk("iphoneos")] let skipSigning = false - generator.generateWithGraphStub = { _path in - XCTAssertEqual(_path, path) - return (path, graph) - } - buildGraphInspector.buildableSchemesStub = { _ in - [scheme] - } - buildGraphInspector.buildableTargetStub = { _scheme, _ in - XCTAssertEqual(_scheme, scheme) - return GraphTarget.test(path: project.path, target: target, project: project) - } - buildGraphInspector.workspacePathStub = { _path in - XCTAssertEqual(_path, path) - return workspacePath - } - buildGraphInspector.buildArgumentsStub = { _project, _target, _, _skipSigning in - XCTAssertEqual(_project, project) - XCTAssertEqual(_target, target) - XCTAssertEqual(_skipSigning, skipSigning) - return buildArguments - } + given(generator) + .load(path: .value(path)) + .willReturn(graph) + given(buildGraphInspector) + .buildableSchemes(graphTraverser: .any) + .willReturn([scheme]) + given(buildGraphInspector) + .buildableTarget(scheme: .value(scheme), graphTraverser: .any) + .willReturn(GraphTarget.test(path: project.path, target: target, project: project)) + given(buildGraphInspector) + .workspacePath(directory: .value(path)) + .willReturn(workspacePath) + given(buildGraphInspector) + .buildArguments( + project: .value(project), + target: .value(target), + configuration: .any, + skipSigning: .value(skipSigning) + ) + .willReturn(buildArguments) targetBuilder - .buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _ in + .buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _, _ in XCTAssertEqual(_workspacePath, workspacePath) XCTAssertEqual(_scheme, scheme) XCTAssertTrue(_clean) @@ -124,28 +143,31 @@ final class BuildServiceTests: TuistUnitTestCase { let buildArguments: [XcodeBuildArgument] = [.sdk("iphoneos")] let skipSigning = false - generator.loadStub = { _path in - XCTAssertEqual(_path, path) - return graph - } - buildGraphInspector.buildableSchemesStub = { _ in - [scheme] - } - buildGraphInspector.buildableTargetStub = { _scheme, _ in - XCTAssertEqual(_scheme, scheme) - return GraphTarget.test(path: project.path, target: target, project: project) - } - buildGraphInspector.workspacePathStub = { _path in - XCTAssertEqual(_path, path) - return workspacePath - } - buildGraphInspector.buildArgumentsStub = { _project, _target, _, _skipSigning in - XCTAssertEqual(_project, project) - XCTAssertEqual(_target, target) - XCTAssertEqual(_skipSigning, skipSigning) - return buildArguments - } - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _ in + given(generator) + .load(path: .value(path)) + .willReturn(graph) + given(buildGraphInspector) + .buildableSchemes(graphTraverser: .any) + .willReturn( + [ + scheme, + ] + ) + given(buildGraphInspector) + .buildableTarget(scheme: .value(scheme), graphTraverser: .any) + .willReturn(GraphTarget.test(path: project.path, target: target, project: project)) + given(buildGraphInspector) + .workspacePath(directory: .value(path)) + .willReturn(workspacePath) + given(buildGraphInspector) + .buildArguments( + project: .value(project), + target: .value(target), + configuration: .any, + skipSigning: .value(skipSigning) + ) + .willReturn(buildArguments) + targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _, _ in XCTAssertEqual(_workspacePath, workspacePath) XCTAssertEqual(_scheme, scheme) XCTAssertTrue(_clean) @@ -171,28 +193,31 @@ final class BuildServiceTests: TuistUnitTestCase { let buildArguments: [XcodeBuildArgument] = [.sdk("iphoneos")] let skipSigning = false - generator.loadStub = { _path in - XCTAssertEqual(_path, path) - return graph - } - buildGraphInspector.buildableSchemesStub = { _ in - [schemeA, schemeB] - } - buildGraphInspector.buildableTargetStub = { _scheme, _ in - if _scheme == schemeA { return GraphTarget.test(path: project.path, target: targetA, project: project) } - else if _scheme == schemeB { return GraphTarget.test(path: project.path, target: targetB, project: project) } - else { XCTFail("unexpected scheme"); return GraphTarget.test(path: project.path, target: targetA, project: project) } - } - buildGraphInspector.workspacePathStub = { _path in - XCTAssertEqual(_path, path) - return workspacePath - } - buildGraphInspector.buildArgumentsStub = { _, _, _, _skipSigning in - XCTAssertEqual(_skipSigning, skipSigning) - return buildArguments - } + given(generator) + .load(path: .value(path)) + .willReturn(graph) + given(buildGraphInspector) + .buildableSchemes(graphTraverser: .any) + .willReturn([schemeA, schemeB]) + given(buildGraphInspector) + .buildableTarget(scheme: .matching { + $0 == schemeA || $0 == schemeB + }, graphTraverser: .any) + .willProduce { scheme, _ in + if scheme == schemeA { + return GraphTarget.test(path: project.path, target: targetA, project: project) + } else { + return GraphTarget.test(path: project.path, target: targetB, project: project) + } + } + given(buildGraphInspector) + .workspacePath(directory: .value(path)) + .willReturn(workspacePath) + given(buildGraphInspector) + .buildArguments(project: .any, target: .any, configuration: .any, skipSigning: .value(skipSigning)) + .willReturn(buildArguments) targetBuilder - .buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _ in + .buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _, _ in XCTAssertEqual(_workspacePath, workspacePath) XCTAssertNil(_device) XCTAssertNil(_osVersion) @@ -228,27 +253,35 @@ final class BuildServiceTests: TuistUnitTestCase { let buildArguments: [XcodeBuildArgument] = [.sdk("iphoneos")] let skipSigning = false - generator.loadStub = { _path in - XCTAssertEqual(_path, path) - return graph - } - buildGraphInspector.buildableSchemesStub = { _ in - [schemeA, schemeB] - } - buildGraphInspector.buildableTargetStub = { _scheme, _ in - if _scheme == schemeA { return GraphTarget.test(path: project.path, target: targetA, project: project) } - else if _scheme == schemeB { return GraphTarget.test(path: project.path, target: targetB, project: project) } - else { XCTFail("unexpected scheme"); return GraphTarget.test(path: project.path, target: targetA, project: project) } - } - buildGraphInspector.workspacePathStub = { _path in - XCTAssertEqual(_path, path) - return workspacePath - } - buildGraphInspector.buildArgumentsStub = { _, _, _, _skipSigning in - XCTAssertEqual(_skipSigning, skipSigning) - return buildArguments - } - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _ in + given(generator) + .load(path: .value(path)) + .willReturn(graph) + given(buildGraphInspector) + .buildableSchemes(graphTraverser: .any) + .willReturn( + [ + schemeA, + schemeB, + ] + ) + given(buildGraphInspector) + .buildableTarget(scheme: .matching { + $0 == schemeA || $0 == schemeB + }, graphTraverser: .any) + .willProduce { scheme, _ in + if scheme == schemeA { + return GraphTarget.test(path: project.path, target: targetA, project: project) + } else { + return GraphTarget.test(path: project.path, target: targetB, project: project) + } + } + given(buildGraphInspector) + .workspacePath(directory: .value(path)) + .willReturn(workspacePath) + given(buildGraphInspector) + .buildArguments(project: .any, target: .any, configuration: .any, skipSigning: .value(skipSigning)) + .willReturn(buildArguments) + targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _, _ in XCTAssertEqual(_workspacePath, workspacePath) if _scheme.name == "A" { XCTAssertEqual(_scheme, schemeA) @@ -272,20 +305,20 @@ final class BuildServiceTests: TuistUnitTestCase { let graph = Graph.test() let schemeA = Scheme.test(name: "A") let schemeB = Scheme.test(name: "B") - generator.loadStub = { _path in - XCTAssertEqual(_path, path) - return graph - } - buildGraphInspector.workspacePathStub = { _path in - XCTAssertEqual(_path, path) - return workspacePath - } - buildGraphInspector.buildableSchemesStub = { _ in - [ - schemeA, - schemeB, - ] - } + given(generator) + .load(path: .value(path)) + .willReturn(graph) + given(buildGraphInspector) + .workspacePath(directory: .value(path)) + .willReturn(workspacePath) + given(buildGraphInspector) + .buildableSchemes(graphTraverser: .any) + .willReturn( + [ + schemeA, + schemeB, + ] + ) // When try await subject.testRun( @@ -305,20 +338,23 @@ extension BuildService { generate: Bool = false, clean: Bool = true, configuration: String? = nil, + ignoreBinaryCache: Bool = false, buildOutputPath: AbsolutePath? = nil, derivedDataPath: String? = nil, path: AbsolutePath, device: String? = nil, - platform: String? = nil, + platform: XcodeGraph.Platform? = nil, osVersion: String? = nil, rosetta: Bool = false, - generateOnly: Bool = false + generateOnly: Bool = false, + passthroughXcodeBuildArguments: [String] = [] ) async throws { try await run( schemeName: schemeName, generate: generate, clean: clean, configuration: configuration, + ignoreBinaryCache: ignoreBinaryCache, buildOutputPath: buildOutputPath, derivedDataPath: derivedDataPath, path: path, @@ -326,7 +362,8 @@ extension BuildService { platform: platform, osVersion: osVersion, rosetta: rosetta, - generateOnly: generateOnly + generateOnly: generateOnly, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) } } diff --git a/Tests/TuistKitTests/Services/Cache/CachePrintHashesServiceTests.swift b/Tests/TuistKitTests/Services/Cache/CachePrintHashesServiceTests.swift new file mode 100644 index 00000000000..5966315e86c --- /dev/null +++ b/Tests/TuistKitTests/Services/Cache/CachePrintHashesServiceTests.swift @@ -0,0 +1,215 @@ +import Foundation +import Mockable +import MockableTest +import Path +import TuistCache +import TuistCore +import TuistLoader +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistKit + +final class CachePrintHashesServiceTests: TuistUnitTestCase { + var subject: CachePrintHashesService! + var generator: MockGenerating! + var generatorFactory: MockGeneratorFactorying! + var cacheGraphContentHasher: MockCacheGraphContentHashing! + var clock: Clock! + var path: String! + var configLoader: MockConfigLoading! + + override func setUp() { + super.setUp() + path = "/Test" + generatorFactory = MockGeneratorFactorying() + generator = .init() + given(generatorFactory) + .defaultGenerator(config: .any) + .willReturn(generator) + + cacheGraphContentHasher = MockCacheGraphContentHashing() + clock = StubClock() + + configLoader = .init() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + } + + override func tearDown() { + generator = nil + cacheGraphContentHasher = nil + clock = nil + subject = nil + super.tearDown() + } + + func test_run_withFullPath_loads_the_graph() async throws { + // Given + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + let fullPath = FileHandler.shared.currentPath.pathString + "/full/path" + given(cacheGraphContentHasher) + .contentHashes(for: .any, configuration: .any, config: .any, excludedTargets: .any) + .willReturn([:]) + given(generator) + .load(path: .any) + .willReturn(.test()) + + // When + _ = try await subject.run(path: fullPath, configuration: nil) + + // Then + verify(generator) + .load(path: .value(try AbsolutePath(validating: fullPath))) + .called(1) + } + + func test_run_withoutPath_loads_the_graph() async throws { + // Given + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + given(cacheGraphContentHasher) + .contentHashes(for: .any, configuration: .any, config: .any, excludedTargets: .any) + .willReturn([:]) + given(generator) + .load(path: .any) + .willReturn(.test()) + + // When + _ = try await subject.run(path: nil, configuration: nil) + + // Then + verify(generator) + .load(path: .value(FileHandler.shared.currentPath)) + .called(1) + } + + func test_run_withRelativePath__loads_the_graph() async throws { + // Given + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + given(cacheGraphContentHasher) + .contentHashes(for: .any, configuration: .any, config: .any, excludedTargets: .any) + .willReturn([:]) + given(generator) + .load(path: .any) + .willReturn(.test()) + + // When + _ = try await subject.run(path: "RelativePath", configuration: nil) + + // Then + verify(generator) + .load(path: .value(try AbsolutePath(validating: "RelativePath", relativeTo: FileHandler.shared.currentPath))) + .called(1) + } + + func test_run_loads_the_graph() async throws { + // Given + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + given(cacheGraphContentHasher) + .contentHashes(for: .any, configuration: .any, config: .any, excludedTargets: .any) + .willReturn([:]) + given(generator) + .load(path: .any) + .willReturn(.test()) + + // When + _ = try await subject.run(path: path, configuration: nil) + + // Then + verify(generator) + .load(path: .value(try AbsolutePath(validating: "/Test"))) + .called(1) + } + + func test_run_content_hasher_gets_correct_graph() async throws { + // Given + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + let graph = Graph.test() + given(generator) + .load(path: .any) + .willReturn(graph) + + given(cacheGraphContentHasher) + .contentHashes(for: .value(graph), configuration: .any, config: .any, excludedTargets: .any) + .willReturn([:]) + + // When / Then + _ = try await subject.run(path: path, configuration: nil) + } + + func test_run_outputs_correct_hashes() async throws { + // Given + let target1 = GraphTarget.test(target: .test(name: "ShakiOne")) + let target2 = GraphTarget.test(target: .test(name: "ShakiTwo")) + given(cacheGraphContentHasher) + .contentHashes(for: .any, configuration: .any, config: .any, excludedTargets: .any) + .willReturn([target1: "hash1", target2: "hash2"]) + + given(generator) + .load(path: .any) + .willReturn(.test()) + + subject = CachePrintHashesService( + generatorFactory: generatorFactory, + cacheGraphContentHasher: cacheGraphContentHasher, + clock: clock, + configLoader: configLoader + ) + + // When + _ = try await subject.run(path: path, configuration: nil) + + // Then + XCTAssertPrinterOutputContains("ShakiOne - hash1") + XCTAssertPrinterOutputContains("ShakiTwo - hash2") + } + + func test_run_gives_correct_configuration_type_to_hasher() async throws { + // Given + given(cacheGraphContentHasher) + .contentHashes(for: .any, configuration: .value("Debug"), config: .any, excludedTargets: .any) + .willReturn([:]) + + given(generator) + .load(path: .any) + .willReturn(.test()) + + // When / Then + _ = try await subject.run(path: path, configuration: "Debug") + } +} diff --git a/Tests/TuistKitTests/Services/CleanServiceTests.swift b/Tests/TuistKitTests/Services/CleanServiceTests.swift index 4dcec31830c..b4a70ff67d0 100644 --- a/Tests/TuistKitTests/Services/CleanServiceTests.swift +++ b/Tests/TuistKitTests/Services/CleanServiceTests.swift @@ -1,6 +1,12 @@ +import FileSystem import Foundation +import MockableTest +import Path import TuistCore import TuistCoreTesting +import TuistLoader +import TuistLoaderTesting +import TuistServer import TuistSupport import XCTest @@ -9,104 +15,219 @@ import XCTest final class CleanServiceTests: TuistUnitTestCase { private var subject: CleanService! - private var cacheDirectoriesProvider: MockCacheDirectoriesProvider! + private var rootDirectoryLocator: MockRootDirectoryLocating! + private var cacheDirectoriesProvider: MockCacheDirectoriesProviding! + private var manifestFilesLocator: MockManifestFilesLocating! + private var configLoader: MockConfigLoading! + private var serverURLService: MockServerURLServicing! + private var cleanCacheService: MockCleanCacheServicing! - override func setUp() { + override func setUpWithError() throws { super.setUp() - let mockCacheDirectoriesProvider = try! MockCacheDirectoriesProvider() - cacheDirectoriesProvider = mockCacheDirectoriesProvider + rootDirectoryLocator = .init() + cacheDirectoriesProvider = .init() + manifestFilesLocator = MockManifestFilesLocating() + configLoader = .init() + serverURLService = .init() + cleanCacheService = .init() subject = CleanService( - cacheDirectoryProviderFactory: MockCacheDirectoriesProviderFactory(provider: mockCacheDirectoriesProvider) + fileHandler: FileHandler.shared, + rootDirectoryLocator: rootDirectoryLocator, + cacheDirectoriesProvider: cacheDirectoriesProvider, + manifestFilesLocator: manifestFilesLocator, + configLoader: configLoader, + serverURLService: serverURLService, + cleanCacheService: cleanCacheService, + fileSystem: FileSystem() ) } override func tearDown() { + rootDirectoryLocator = nil + cacheDirectoriesProvider = nil + manifestFilesLocator = nil + configLoader = nil + serverURLService = nil + cleanCacheService = nil subject = nil super.tearDown() } - func test_run_with_category_cleans_category() throws { + func test_run_with_category_cleans_category() async throws { // Given - 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) - XCTAssertTrue(correctlyCreated, "Test setup is not properly done. Folder \(path.pathString) should exist") - } - cacheDirectoriesProvider.cacheDirectoryStub = cachePath + let cachePaths = try createFolders(["tuist/Manifests", "tuist/ProjectDescriptionHelpers"]) + + let cachePath = cachePaths[0].parentDirectory.parentDirectory + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.manifests)) + .willReturn(cachePaths[0]) + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.projectDescriptionHelpers)) + .willReturn(cachePaths[1]) + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(cachePath) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) // When - try subject.run(categories: [.global(.builds), .global(.tests)], path: nil) + try await subject.run( + categories: [TuistCleanCategory.global(.manifests)], + remote: false, + path: nil + ) // Then - let buildsExists = FileManager.default.fileExists(atPath: cachePaths[1].pathString) - XCTAssertFalse(buildsExists, "Cache folder at path \(cachePaths[1]) should have been deleted by the test.") - let manifestsExists = FileManager.default.fileExists(atPath: cachePaths[2].pathString) - XCTAssertTrue( - manifestsExists, - "Cache folder at path \(cachePaths[2].pathString) should not have been deleted by the test." - ) - let testsExists = FileManager.default.fileExists(atPath: cachePaths[3].pathString) - XCTAssertFalse(testsExists, "Cache folder at path \(cachePaths[3].pathString) should not have been deleted by the test.") + XCTAssertFalse(FileHandler.shared.exists(cachePaths[0])) + XCTAssertTrue(FileHandler.shared.exists(cachePaths[1])) } - func test_run_without_category_cleans_all() throws { + func test_run_with_dependencies_cleans_dependencies() async throws { // Given - 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) - XCTAssertTrue(correctlyCreated, "Test setup is not properly done. Folder \(path.pathString) should exist") - } - cacheDirectoriesProvider.cacheDirectoryStub = cachePath - let projectPath = try temporaryPath() - let dependenciesPath = projectPath.appending( - components: - Constants.tuistDirectoryName, - Constants.DependenciesDirectory.name - ) - let lockfilesPath = projectPath.appending( - components: - Constants.tuistDirectoryName, - Constants.DependenciesDirectory.lockfilesDirectoryName - ) - let carthageDependenciesPath = projectPath.appending( - components: Constants.tuistDirectoryName, - Constants.DependenciesDirectory.name, - Constants.DependenciesDirectory.carthageDirectoryName - ) - let spmDependenciesPath = projectPath.appending( - components: Constants.tuistDirectoryName, - Constants.DependenciesDirectory.name, - Constants.DependenciesDirectory.carthageDirectoryName + let localPaths = try createFolders(["Tuist/.build", "Tuist/ProjectDescriptionHelpers"]) + + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(localPaths[0].parentDirectory) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn( + localPaths[1].parentDirectory + .appending(component: Constants.SwiftPackageManager.packageSwiftName) + ) + + let cachePath = localPaths[0].parentDirectory.parentDirectory + given(cacheDirectoriesProvider) + .cacheDirectory() + .willReturn(cachePath) + + // When + try await subject.run( + categories: [TuistCleanCategory.dependencies], + remote: false, + path: nil ) - try fileHandler.createFolder(dependenciesPath) - try fileHandler.createFolder(lockfilesPath) - try fileHandler.createFolder(carthageDependenciesPath) - try fileHandler.createFolder(spmDependenciesPath) + + // Then + XCTAssertFalse(FileHandler.shared.exists(localPaths[0])) + XCTAssertTrue(FileHandler.shared.exists(localPaths[1])) + } + + func test_run_with_dependencies_cleans_dependencies_when_package_is_in_root() async throws { + // Given + let localPaths = try createFolders([".build", "Tuist/ProjectDescriptionHelpers"]) + + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(localPaths[0].parentDirectory) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn( + localPaths[0].parentDirectory + .appending(component: Constants.SwiftPackageManager.packageSwiftName) + ) + + let cachePath = localPaths[0].parentDirectory.parentDirectory + given(cacheDirectoriesProvider) + .cacheDirectory() + .willReturn(cachePath) // When - try subject.run(categories: CleanCategory.allCases, path: nil) + try await subject.run( + categories: [TuistCleanCategory.dependencies], + remote: false, + path: nil + ) // Then - let buildsExists = FileManager.default.fileExists(atPath: cachePaths[1].pathString) - XCTAssertFalse(buildsExists, "Cache folder at path \(cachePaths[1]) should have been deleted by the test.") - let manifestsExists = FileManager.default.fileExists(atPath: cachePaths[2].pathString) - XCTAssertFalse(manifestsExists, "Cache folder at path \(cachePaths[2].pathString) should have been deleted by the test.") - let testsExists = FileManager.default.fileExists(atPath: cachePaths[3].pathString) - XCTAssertFalse(testsExists, "Cache folder at path \(cachePaths[3].pathString) should not have been deleted by the test.") - XCTAssertTrue( - FileManager.default.fileExists(atPath: lockfilesPath.pathString), - "Cache folder at path \(lockfilesPath) should not have been deleted by the test." + XCTAssertFalse(FileHandler.shared.exists(localPaths[0])) + XCTAssertTrue(FileHandler.shared.exists(localPaths[1])) + } + + func test_run_without_category_cleans_all() async throws { + // Given + let cachePaths = try createFolders(["tuist/Manifests"]) + + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(cachePaths[0]) + + let projectPath = try temporaryPath() + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(projectPath) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn( + projectPath + .appending(component: Constants.SwiftPackageManager.packageSwiftName) + ) + let swiftPackageManagerBuildPath = projectPath.appending( + components: Constants.SwiftPackageManager.packageBuildDirectoryName ) - XCTAssertFalse( - FileManager.default.fileExists(atPath: carthageDependenciesPath.pathString), - "Cache folder at path \(carthageDependenciesPath) should have been deleted by the test." + try fileHandler.createFolder(swiftPackageManagerBuildPath) + + // When + try await subject.run( + categories: TuistCleanCategory.allCases, + remote: false, + path: nil ) - XCTAssertFalse( - FileManager.default.fileExists(atPath: spmDependenciesPath.pathString), - "Cache folder at path \(spmDependenciesPath) should have been deleted by the test." + + // Then + XCTAssertFalse(FileHandler.shared.exists(cachePaths[0])) + XCTAssertFalse(FileHandler.shared.exists(swiftPackageManagerBuildPath)) + } + + func test_run_with_remote() async throws { + // Given + let url = URL(string: "https://cloud.com")! + + given(configLoader) + .loadConfig(path: .any) + .willReturn( + Config.test( + fullHandle: "tuist/tuist", + url: url + ) + ) + + given(serverURLService) + .url(configServerURL: .any) + .willReturn(url) + + given(cleanCacheService) + .cleanCache( + serverURL: .value(url), + fullHandle: .value("tuist/tuist") + ) + .willReturn(()) + + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) + + let projectPath = try temporaryPath() + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(projectPath) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + + // When + try await subject.run( + categories: TuistCleanCategory.allCases, + remote: true, + path: nil ) + + // Then + verify(cleanCacheService) + .cleanCache(serverURL: .any, fullHandle: .any) + .called(1) + XCTAssertStandardOutput(pattern: "Successfully cleaned the remote storage.") } } diff --git a/Tests/TuistKitTests/Services/DecryptServiceTests.swift b/Tests/TuistKitTests/Services/DecryptServiceTests.swift deleted file mode 100644 index fd0df5cd205..00000000000 --- a/Tests/TuistKitTests/Services/DecryptServiceTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -import TSCBasic -import TuistSigningTesting -import XCTest -@testable import TuistKit -@testable import TuistSupportTesting - -final class DecryptServiceTests: TuistUnitTestCase { - var subject: DecryptService! - var signingCipher: MockSigningCipher! - - override func setUp() { - super.setUp() - - signingCipher = MockSigningCipher() - subject = DecryptService(signingCipher: signingCipher) - } - - override func tearDown() { - signingCipher = nil - subject = nil - super.tearDown() - } - - func test_calls_decrypt_with_provided_path() throws { - // Given - let expectedPath = try AbsolutePath(validating: "/path") - var path: AbsolutePath? - signingCipher.decryptSigningStub = { decryptPath, _ in - path = decryptPath - } - - // When - try subject.run(path: expectedPath.pathString) - - // Then - XCTAssertEqual(path, expectedPath) - } -} diff --git a/Tests/TuistKitTests/Services/EditServiceTests.swift b/Tests/TuistKitTests/Services/EditServiceTests.swift new file mode 100644 index 00000000000..19ef5c13433 --- /dev/null +++ b/Tests/TuistKitTests/Services/EditServiceTests.swift @@ -0,0 +1,122 @@ +import MockableTest +import Path +import TuistCore +import TuistLoader +import TuistSupport +import XcodeGraph +import XcodeProj +import XCTest +@testable import TuistCoreTesting +@testable import TuistKit +@testable import TuistLoaderTesting +@testable import TuistPluginTesting +@testable import TuistSupportTesting + +final class EditServiceTests: XCTestCase { + var subject: EditService! + var opener: MockOpening! + var configLoader: MockConfigLoading! + var pluginService: MockPluginService! + var cacheDirectoriesProvider: MockCacheDirectoriesProviding! + var cacheDirectoriesProviderFactory: MockCacheDirectoriesProviderFactoring! + var projectEditor: MockProjectEditing! + + override func setUpWithError() throws { + super.setUp() + opener = MockOpening() + configLoader = MockConfigLoading() + pluginService = MockPluginService() + + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + + let mockCacheDirectoriesProvider = MockCacheDirectoriesProviding() + cacheDirectoriesProvider = mockCacheDirectoriesProvider + + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.editProjects)) + .willReturn("/Users/tuist/cache/EditProjects") + + let cacheDirectoryProviderFactory = MockCacheDirectoriesProviderFactoring() + cacheDirectoriesProviderFactory = cacheDirectoryProviderFactory + given(cacheDirectoryProviderFactory) + .cacheDirectories() + .willReturn(mockCacheDirectoriesProvider) + + projectEditor = MockProjectEditing() + + subject = EditService( + projectEditor: projectEditor, + opener: opener, + configLoader: configLoader, + cacheDirectoryProviderFactory: cacheDirectoriesProviderFactory + ) + } + + func test_edit_uses_caches_directory() async throws { + // Given + let path: AbsolutePath = "/private/tmp" + let cacheDirectory = try cacheDirectoriesProvider.cacheDirectory(for: .editProjects) + let projectDirectory = cacheDirectory.appending(component: path.pathString.md5) + + given(projectEditor!) + .edit(at: .any, in: .any, onlyCurrentDirectory: .any, plugins: .any) + .willReturn(projectDirectory) + + given(opener) + .open(path: .any, application: .any, wait: .any) + .willReturn() + + // When + try await subject.run( + path: path.pathString, + permanent: false, + onlyCurrentDirectory: false + ) + + // Then + verify(opener) + .open( + path: .value(projectDirectory), + application: .any, + wait: .value(false) + ) + .called(1) + + verify(projectEditor) + .edit(at: .value(path), in: .value(projectDirectory), onlyCurrentDirectory: .value(false), plugins: .any) + .called(1) + } + + func test_edit_permanent_does_not_open_workspace() async throws { + // Given + let path: AbsolutePath = "/private/tmp" + let cacheDirectory = try cacheDirectoriesProvider.cacheDirectory(for: .editProjects) + let projectDirectory = cacheDirectory.appending(component: path.pathString.md5) + + given(projectEditor!) + .edit(at: .any, in: .any, onlyCurrentDirectory: .any, plugins: .any) + .willReturn(projectDirectory) + + // When + try await subject.run( + path: path.pathString, + permanent: true, + onlyCurrentDirectory: true + ) + + // Then + verify(opener) + .open( + path: .any, + application: .any, + wait: .any + ) + .called(0) + + verify(projectEditor) + .edit(at: .value(path), in: .value(path), onlyCurrentDirectory: .value(true), plugins: .any) + .called(1) + } +} diff --git a/Tests/TuistKitTests/Services/EncryptServiceTests.swift b/Tests/TuistKitTests/Services/EncryptServiceTests.swift deleted file mode 100644 index a4e2e3331dc..00000000000 --- a/Tests/TuistKitTests/Services/EncryptServiceTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -import TSCBasic -import TuistSigningTesting -import XCTest -@testable import TuistKit -@testable import TuistSupportTesting - -final class EncryptServiceTests: TuistUnitTestCase { - var subject: EncryptService! - var signingCipher: MockSigningCipher! - - override func setUp() { - super.setUp() - - signingCipher = MockSigningCipher() - subject = EncryptService(signingCipher: signingCipher) - } - - override func tearDown() { - signingCipher = nil - subject = nil - super.tearDown() - } - - func test_calls_encrypt_with_provided_path() throws { - // Given - let expectedPath = try AbsolutePath(validating: "/path") - var path: AbsolutePath? - signingCipher.encryptSigningStub = { encryptPath, _ in - path = encryptPath - } - - // When - try subject.run(path: expectedPath.pathString) - - // Then - XCTAssertEqual(path, expectedPath) - } -} diff --git a/Tests/TuistKitTests/Services/FetchServiceTests.swift b/Tests/TuistKitTests/Services/FetchServiceTests.swift deleted file mode 100644 index 1ba83568f4c..00000000000 --- a/Tests/TuistKitTests/Services/FetchServiceTests.swift +++ /dev/null @@ -1,298 +0,0 @@ -import Foundation -import TSCBasic -import TSCUtility -import TuistCore -import TuistCoreTesting -import TuistDependenciesTesting -import TuistGraph -import TuistGraphTesting -import TuistLoader -import TuistLoaderTesting -import TuistPluginTesting -import TuistSupport -import TuistSupportTesting -import XCTest - -@testable import TuistKit - -final class FetchServiceTests: TuistUnitTestCase { - private var pluginService: MockPluginService! - private var configLoader: MockConfigLoader! - private var manifestLoader: MockManifestLoader! - private var dependenciesController: MockDependenciesController! - private var packageSettingsLoader: MockPackageSettingsLoader! - private var dependenciesModelLoader: MockDependenciesModelLoader! - - private var subject: FetchService! - - override func setUp() { - super.setUp() - - pluginService = MockPluginService() - configLoader = MockConfigLoader() - manifestLoader = MockManifestLoader() - manifestLoader.manifestsAtStub = { _ in [.project] } - dependenciesController = MockDependenciesController() - dependenciesModelLoader = MockDependenciesModelLoader() - packageSettingsLoader = MockPackageSettingsLoader() - - subject = FetchService( - pluginService: pluginService, - configLoader: configLoader, - manifestLoader: manifestLoader, - dependenciesController: dependenciesController, - dependenciesModelLoader: dependenciesModelLoader, - packageSettingsLoader: packageSettingsLoader - ) - } - - override func tearDown() { - subject = nil - - pluginService = nil - configLoader = nil - dependenciesController = nil - dependenciesModelLoader = nil - - super.tearDown() - } - - func test_run_when_updating_dependencies() async throws { - // Given - let stubbedPath = try temporaryPath() - let stubbedDependencies = Dependencies( - carthage: .init( - [ - .git(path: "Dependency1", requirement: .exact("1.1.1")), - ] - ), - swiftPackageManager: .init( - .packages([ - .remote(url: "Dependency1/Dependency1", requirement: .upToNextMajor("1.2.3")), - ]), - productTypes: [:], baseSettings: .default, - targetSettings: [:] - ), - platforms: [.iOS, .macOS] - ) - dependenciesModelLoader.loadDependenciesStub = { _, _ in stubbedDependencies } - - let stubbedSwiftVersion = TSCUtility.Version(5, 3, 0) - configLoader.loadConfigStub = { _ in Config.test(swiftVersion: stubbedSwiftVersion) } - - dependenciesController.legacyUpdateStub = { path, dependencies, swiftVersion in - XCTAssertEqual(path, stubbedPath) - XCTAssertEqual(dependencies, stubbedDependencies) - XCTAssertEqual(swiftVersion, stubbedSwiftVersion) - return .none - } - dependenciesController.saveStub = { dependenciesGraph, path in - XCTAssertEqual(dependenciesGraph, .none) - XCTAssertEqual(path, stubbedPath) - } - pluginService.fetchRemotePluginsStub = { _ in - _ = Plugins.test() - } - - try fileHandler.touch( - stubbedPath.appending( - components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(stubbedPath) - ) - ) - - // When - try await subject.run( - path: stubbedPath.pathString, - update: true - ) - - // Then - XCTAssertTrue(dependenciesController.invokedUpdate) - XCTAssertTrue(dependenciesModelLoader.invokedLoadDependencies) - XCTAssertTrue(dependenciesController.invokedSave) - - XCTAssertFalse(dependenciesController.invokedFetch) - } - - func test_run_when_fetching_plugins() async throws { - // Given - let config = Config.test( - plugins: [ - .git(url: "url", gitReference: .tag("tag"), directory: nil, releaseUrl: nil), - ] - ) - configLoader.loadConfigStub = { _ in - config - } - var invokedConfig: Config? - pluginService.loadPluginsStub = { config in - invokedConfig = config - return .test() - } - - // When - try await subject.run( - path: nil, - update: false - ) - - // Then - XCTAssertEqual(invokedConfig, config) - } - - func test_run_when_fetching_dependencies() async throws { - // Given - let stubbedPath = try temporaryPath() - let stubbedDependencies = Dependencies( - carthage: .init( - [ - .github(path: "Dependency1", requirement: .exact("1.1.1")), - ] - ), - swiftPackageManager: .init( - .packages([ - .remote(url: "Dependency1/Dependency1", requirement: .upToNextMajor("1.2.3")), - ]), - productTypes: [:], - baseSettings: .default, - targetSettings: [:] - ), - platforms: [.iOS, .macOS] - ) - dependenciesModelLoader.loadDependenciesStub = { _, _ in stubbedDependencies } - - let stubbedSwiftVersion = TSCUtility.Version(5, 3, 0) - configLoader.loadConfigStub = { _ in Config.test(swiftVersion: stubbedSwiftVersion) } - - dependenciesController.legacyFetchStub = { path, dependencies, swiftVersion in - XCTAssertEqual(path, stubbedPath) - XCTAssertEqual(dependencies, stubbedDependencies) - XCTAssertEqual(swiftVersion, stubbedSwiftVersion) - return .none - } - dependenciesController.saveStub = { dependenciesGraph, path in - XCTAssertEqual(dependenciesGraph, .none) - XCTAssertEqual(path, stubbedPath) - } - pluginService.fetchRemotePluginsStub = { _ in } - - try fileHandler.touch( - stubbedPath.appending( - components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(stubbedPath) - ) - ) - - // When - try await subject.run( - path: stubbedPath.pathString, - update: false - ) - - // Then - XCTAssertTrue(dependenciesModelLoader.invokedLoadDependencies) - XCTAssertTrue(dependenciesController.invokedFetch) - XCTAssertTrue(dependenciesController.invokedSave) - - 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/FullHandleServiceTests.swift b/Tests/TuistKitTests/Services/FullHandleServiceTests.swift new file mode 100644 index 00000000000..ce817f81a2e --- /dev/null +++ b/Tests/TuistKitTests/Services/FullHandleServiceTests.swift @@ -0,0 +1,45 @@ +import Foundation +import TuistSupportTesting +import XCTest + +@testable import TuistServer + +final class FullHandleServiceTests: TuistUnitTestCase { + private var subject: FullHandleServicing! + override func setUp() { + super.setUp() + + subject = FullHandleService() + } + + override func tearDown() { + subject = nil + + super.tearDown() + } + + func test_parsing_full_handle_when_valid() throws { + // When + let got = try subject.parse("tuist-org/tuist") + + // Then + XCTAssertEqual(got.accountHandle, "tuist-org") + XCTAssertEqual(got.projectHandle, "tuist") + } + + func test_parsing_full_handle_when_only_account_or_project_handle_is_present() throws { + // When / Then + XCTAssertThrowsSpecific( + try subject.parse("tuist"), + FullHandleServiceError.invalidHandle("tuist") + ) + } + + func test_parsing_full_handle_when_extra_components_are_present() throws { + // When / Then + XCTAssertThrowsSpecific( + try subject.parse("tuist-org/tuist/extra"), + FullHandleServiceError.invalidHandle("tuist-org/tuist/extra") + ) + } +} diff --git a/Tests/TuistKitTests/Services/GenerateServiceTests.swift b/Tests/TuistKitTests/Services/GenerateServiceTests.swift index 62452493b93..d1994374b6a 100644 --- a/Tests/TuistKitTests/Services/GenerateServiceTests.swift +++ b/Tests/TuistKitTests/Services/GenerateServiceTests.swift @@ -1,8 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph import TuistLoader +import TuistServer +import TuistSupport +import XcodeGraph import XcodeProj import XCTest @testable import TuistCoreTesting @@ -10,50 +13,65 @@ import XCTest @testable import TuistLoaderTesting @testable import TuistSupportTesting -private typealias GeneratorParameters = ( - sources: Set, - cacheOutputType: CacheOutputType, - cacheProfile: TuistGraph.Cache.Profile, - ignoreCache: Bool -) - final class GenerateServiceTests: TuistUnitTestCase { - var subject: GenerateService! - var opener: MockOpener! - var generator: MockGenerator! - var generatorFactory: MockGeneratorFactory! - var clock: StubClock! + private var subject: GenerateService! + private var opener: MockOpening! + private var generator: MockGenerating! + private var generatorFactory: MockGeneratorFactorying! + private var cacheStorageFactory: MockCacheStorageFactorying! + private var clock: StubClock! override func setUp() { super.setUp() - opener = MockOpener() - generator = MockGenerator() - generatorFactory = MockGeneratorFactory() - generatorFactory.stubbedDefaultResult = generator + opener = .init() + generator = .init() + generatorFactory = .init() + given(generatorFactory) + .generation( + config: .any, + sources: .any, + configuration: .any, + ignoreBinaryCache: .any, + cacheStorage: .any + ) + .willReturn(generator) + cacheStorageFactory = .init() + given(cacheStorageFactory) + .cacheStorage(config: .any) + .willReturn(MockCacheStoring()) clock = StubClock() - subject = GenerateService(clock: clock, opener: opener, generatorFactory: generatorFactory) + subject = GenerateService( + cacheStorageFactory: cacheStorageFactory, + generatorFactory: generatorFactory, + clock: clock, + opener: opener + ) } override func tearDown() { opener = nil generator = nil - subject = nil generatorFactory = nil + cacheStorageFactory = nil clock = nil + subject = nil super.tearDown() } func test_run_fatalErrors_when_theworkspaceGenerationFails() async throws { let expectedError = NSError.test() - generator.generateStub = { _ in - throw expectedError - } + given(generator) + .generate(path: .any) + .willThrow(expectedError) do { try await subject .run( path: nil, - noOpen: true + sources: [], + noOpen: true, + configuration: nil, + ignoreBinaryCache: false ) XCTFail("Must throw") } catch { @@ -62,27 +80,43 @@ final class GenerateServiceTests: TuistUnitTestCase { } func test_run() async throws { + // Given let workspacePath = try AbsolutePath(validating: "/test.xcworkspace") - generator.generateStub = { _ in - workspacePath - } + given(generator) + .generate(path: .any) + .willReturn(workspacePath) + + given(opener) + .open(path: .any) + .willReturn() + // When try await subject.run( path: nil, - noOpen: false + sources: [], + noOpen: false, + configuration: nil, + ignoreBinaryCache: false ) - XCTAssertEqual(opener.openArgs.last?.0, workspacePath.pathString) + // Then + verify(opener) + .open(path: .value(workspacePath)) + .called(1) } func test_run_timeIsPrinted() async throws { // Given let workspacePath = try AbsolutePath(validating: "/test.xcworkspace") - generator.generateStub = { _ in - workspacePath - } + given(opener) + .open(path: .any) + .willReturn() + + given(generator) + .generate(path: .any) + .willReturn(workspacePath) clock.assertOnUnexpectedCalls = true clock.primedTimers = [ 0.234, @@ -91,7 +125,10 @@ final class GenerateServiceTests: TuistUnitTestCase { // When try await subject.run( path: nil, - noOpen: false + sources: [], + noOpen: false, + configuration: nil, + ignoreBinaryCache: false ) // Then diff --git a/Tests/TuistKitTests/Services/GraphServiceTests.swift b/Tests/TuistKitTests/Services/GraphServiceTests.swift index 2cbedf1ed44..40eeccfbae9 100644 --- a/Tests/TuistKitTests/Services/GraphServiceTests.swift +++ b/Tests/TuistKitTests/Services/GraphServiceTests.swift @@ -1,11 +1,13 @@ import DOT import Foundation import GraphViz +import MockableTest +import Path import ProjectAutomation -import TSCBasic -import TuistGraph +import TuistCore import TuistPlugin import TuistSupport +import XcodeGraph import XcodeProj import XCTest @@ -15,14 +17,14 @@ import XCTest @testable import TuistSupportTesting final class GraphServiceTests: TuistUnitTestCase { - var manifestGraphLoader: MockManifestGraphLoader! + var manifestGraphLoader: MockManifestGraphLoading! var graphVizMapper: MockGraphToGraphVizMapper! var subject: GraphService! override func setUp() { super.setUp() graphVizMapper = MockGraphToGraphVizMapper() - manifestGraphLoader = MockManifestGraphLoader() + manifestGraphLoader = .init() subject = GraphService( graphVizGenerator: graphVizMapper, @@ -47,6 +49,10 @@ final class GraphServiceTests: TuistUnitTestCase { try FileHandler.shared.touch(projectManifestPath) graphVizMapper.stubMap = Graph() + given(manifestGraphLoader) + .load(path: .any) + .willReturn((.test(), [], MapperEnvironment(), [])) + // When try await subject.run( format: .dot, @@ -78,6 +84,10 @@ final class GraphServiceTests: TuistUnitTestCase { try FileHandler.shared.touch(graphPath) try FileHandler.shared.touch(projectManifestPath) + given(manifestGraphLoader) + .load(path: .any) + .willReturn((.test(), [], MapperEnvironment(), [])) + // When try await subject.run( format: .json, diff --git a/Tests/TuistKitTests/Services/InitServiceTests.swift b/Tests/TuistKitTests/Services/InitServiceTests.swift index c32a367af18..d82b6b54c54 100644 --- a/Tests/TuistKitTests/Services/InitServiceTests.swift +++ b/Tests/TuistKitTests/Services/InitServiceTests.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistScaffold import TuistSupport import XCTest @@ -16,6 +16,7 @@ final class InitServiceTests: TuistUnitTestCase { var templateGenerator: MockTemplateGenerator! var templateLoader: MockTemplateLoader! var templateGitLoader: MockTemplateGitLoader! + var tuistVersionLoader: MockTuistVersionLoader! override func setUp() { super.setUp() @@ -23,11 +24,13 @@ final class InitServiceTests: TuistUnitTestCase { templateGenerator = MockTemplateGenerator() templateLoader = MockTemplateLoader() templateGitLoader = MockTemplateGitLoader() + tuistVersionLoader = MockTuistVersionLoader() subject = InitService( templateLoader: templateLoader, templatesDirectoryLocator: templatesDirectoryLocator, templateGenerator: templateGenerator, - templateGitLoader: templateGitLoader + templateGitLoader: templateGitLoader, + tuistVersionLoader: tuistVersionLoader ) } @@ -40,53 +43,104 @@ final class InitServiceTests: TuistUnitTestCase { super.tearDown() } - func test_fails_when_directory_not_empty() throws { + func test_fails_when_directory_not_empty() async throws { // Given let path = FileHandler.shared.currentPath try FileHandler.shared.touch(path.appending(component: "dummy")) // Then - XCTAssertThrowsSpecific(try subject.testRun(), InitServiceError.nonEmptyDirectory(path)) + await XCTAssertThrowsSpecific({ try await self.subject.testRun() }, InitServiceError.nonEmptyDirectory(path)) } - func test_init_fails_when_template_not_found() throws { + func test_init_fails_when_template_not_found() async throws { let templateName = "template" - XCTAssertThrowsSpecific(try subject.testRun(templateName: templateName), InitServiceError.templateNotFound(templateName)) + await XCTAssertThrowsSpecific( + { try await self.subject.testRun(templateName: templateName) }, + InitServiceError.templateNotFound(templateName) + ) } - func test_init_default_when_no_template() throws { + func test_init_default_when_no_template() async throws { // Given let defaultTemplatePath = try temporaryPath().appending(component: "default") templatesDirectoryLocator.templateDirectoriesStub = { _ in [defaultTemplatePath] } - let expectedAttributes = ["name": "Name", "platform": "macOS"] - var generatorAttributes: [String: String] = [:] + + let tuistVersion = "4.0.3" + tuistVersionLoader.getVersionStub = tuistVersion + + let expectedAttributes: [String: Template.Attribute.Value] = [ + "name": .string("Name"), + "platform": .string("macOS"), + "tuist_version": .string(tuistVersion), + "class_name": .string("Name"), + "bundle_identifier": .string("Name"), + ] + var generatorAttributes: [String: Template.Attribute.Value] = [:] templateGenerator.generateStub = { _, _, attributes in generatorAttributes = attributes } // When - try subject.testRun(name: "Name", platform: "macos") + try await subject.testRun(name: "Name", platform: "macos") // Then XCTAssertEqual(expectedAttributes, generatorAttributes) } - func test_init_default_platform() throws { + func test_init_default_platform() async throws { // Given let defaultTemplatePath = try temporaryPath().appending(component: "default") templatesDirectoryLocator.templateDirectoriesStub = { _ in [defaultTemplatePath] } - let expectedAttributes = ["name": "Name", "platform": "iOS"] - var generatorAttributes: [String: String] = [:] + + let tuistVersion = "4.0.3" + tuistVersionLoader.getVersionStub = tuistVersion + + let expectedAttributes: [String: Template.Attribute.Value] = [ + "name": .string("Name"), + "platform": .string("iOS"), + "tuist_version": .string(tuistVersion), + "class_name": .string("Name"), + "bundle_identifier": .string("Name"), + ] + var generatorAttributes: [String: Template.Attribute.Value] = [:] templateGenerator.generateStub = { _, _, attributes in generatorAttributes = attributes } // When - try subject.testRun(name: "Name") + try await subject.testRun(name: "Name") + + // Then + XCTAssertEqual(expectedAttributes, generatorAttributes) + } + + func test_init_default_with_unusual_name() async throws { + // Given + let defaultTemplatePath = try temporaryPath().appending(component: "default") + templatesDirectoryLocator.templateDirectoriesStub = { _ in + [defaultTemplatePath] + } + let tuistVersion = "4.0.3" + tuistVersionLoader.getVersionStub = tuistVersion + + let expectedAttributes: [String: TuistCore.Template.Attribute.Value] = [ + "name": .string("unusual name"), + "platform": .string("iOS"), + "tuist_version": .string(tuistVersion), + "class_name": .string("UnusualName"), + "bundle_identifier": .string("unusual-name"), + ] + var generatorAttributes: [String: TuistCore.Template.Attribute.Value] = [:] + templateGenerator.generateStub = { _, _, attributes in + generatorAttributes = attributes + } + + // When + try await subject.testRun(name: "unusual name") // Then XCTAssertEqual(expectedAttributes, generatorAttributes) @@ -99,24 +153,31 @@ final class InitServiceTests: TuistUnitTestCase { description: "test", attributes: [ .required("required"), - .optional("optional", default: "optionalValue"), + .optional("optional", default: .string("optionalValue")), ], items: [] ) } - let expectedAttributes = [ - "name": "Name", - "platform": "macOS", - "required": "requiredValue", - "optional": "optionalValue", + + let tuistVersion = "4.0.3" + tuistVersionLoader.getVersionStub = tuistVersion + + let expectedAttributes: [String: Template.Attribute.Value] = [ + "name": .string("Name"), + "platform": .string("macOS"), + "tuist_version": .string(tuistVersion), + "class_name": .string("Name"), + "bundle_identifier": .string("Name"), + "required": .string("requiredValue"), + "optional": .string("optionalValue"), ] - var generatorAttributes: [String: String] = [:] + var generatorAttributes: [String: Template.Attribute.Value] = [:] templateGenerator.generateStub = { _, _, attributes in generatorAttributes = attributes } // When - try subject.testRun( + try await subject.testRun( name: "Name", platform: "macos", templateName: "https://url/to/repo.git", @@ -128,6 +189,87 @@ final class InitServiceTests: TuistUnitTestCase { // Then XCTAssertEqual(expectedAttributes, generatorAttributes) } + + func test_optional_dictionary_attribute_is_taken_from_template() async throws { + // Given + let context: Template.Attribute.Value = .dictionary([ + "key1": .string("value1"), + "key2": .string("value2"), + ]) + + templateLoader.loadTemplateStub = { _ in + Template.test(attributes: [ + .optional("optional", default: context), + ]) + } + + let tuistVersion = "4.0.3" + tuistVersionLoader.getVersionStub = tuistVersion + + let defaultTemplatePath = try temporaryPath().appending(component: "default") + templatesDirectoryLocator.templateDirectoriesStub = { _ in + [defaultTemplatePath] + } + + let expectedAttributes: [String: Template.Attribute.Value] = [ + "name": .string("Name"), + "platform": .string("iOS"), + "tuist_version": .string(tuistVersion), + "class_name": .string("Name"), + "bundle_identifier": .string("Name"), + "optional": context, + ] + + var generatorAttributes: [String: Template.Attribute.Value] = [:] + templateGenerator.generateStub = { _, _, attributes in + generatorAttributes = attributes + } + + // When + try await subject.testRun(name: "Name") + + // Then + XCTAssertEqual(expectedAttributes, generatorAttributes) + } + + func test_optional_integer_attribute_is_taken_from_template() async throws { + // Given + let defaultIntegerValue: Template.Attribute.Value = .integer(999) + + templateLoader.loadTemplateStub = { _ in + Template.test(attributes: [ + .optional("optional", default: defaultIntegerValue), + ]) + } + + let tuistVersion = "4.0.3" + tuistVersionLoader.getVersionStub = tuistVersion + + let defaultTemplatePath = try temporaryPath().appending(component: "default") + templatesDirectoryLocator.templateDirectoriesStub = { _ in + [defaultTemplatePath] + } + + let expectedAttributes: [String: Template.Attribute.Value] = [ + "name": .string("Name"), + "platform": .string("iOS"), + "tuist_version": .string(tuistVersion), + "class_name": .string("Name"), + "bundle_identifier": .string("Name"), + "optional": defaultIntegerValue, + ] + + var generatorAttributes: [String: Template.Attribute.Value] = [:] + templateGenerator.generateStub = { _, _, attributes in + generatorAttributes = attributes + } + + // When + try await subject.testRun(name: "Name") + + // Then + XCTAssertEqual(expectedAttributes, generatorAttributes) + } } extension InitService { @@ -138,8 +280,8 @@ extension InitService { templateName: String? = nil, requiredTemplateOptions: [String: String] = [:], optionalTemplateOptions: [String: String?] = [:] - ) throws { - try run( + ) async throws { + try await run( name: name, platform: platform, path: path, diff --git a/Tests/TuistKitTests/Services/InstallServiceTests.swift b/Tests/TuistKitTests/Services/InstallServiceTests.swift new file mode 100644 index 00000000000..e4a3b874e9e --- /dev/null +++ b/Tests/TuistKitTests/Services/InstallServiceTests.swift @@ -0,0 +1,172 @@ +import Foundation +import MockableTest +import Path +import TSCUtility +import TuistCore +import TuistCoreTesting +import TuistLoader +import TuistPluginTesting +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistKit + +final class InstallServiceTests: TuistUnitTestCase { + private var pluginService: MockPluginService! + private var configLoader: MockConfigLoading! + private var swiftPackageManagerController: MockSwiftPackageManagerController! + private var manifestFilesLocator: MockManifestFilesLocating! + + private var subject: InstallService! + + override func setUp() { + super.setUp() + + pluginService = MockPluginService() + configLoader = MockConfigLoading() + swiftPackageManagerController = MockSwiftPackageManagerController() + manifestFilesLocator = MockManifestFilesLocating() + + subject = InstallService( + pluginService: pluginService, + configLoader: configLoader, + swiftPackageManagerController: swiftPackageManagerController, + manifestFilesLocator: manifestFilesLocator + ) + } + + override func tearDown() { + subject = nil + + pluginService = nil + configLoader = nil + swiftPackageManagerController = nil + manifestFilesLocator = nil + + super.tearDown() + } + + func test_run_when_updating_dependencies() async throws { + // Given + let stubbedPath = try temporaryPath() + + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(stubbedPath.appending(components: "Tuist", "Package.swift")) + + let stubbedSwiftVersion = TSCUtility.Version(5, 3, 0) + given(configLoader) + .loadConfig(path: .any) + .willReturn( + Config.test(swiftVersion: .init(stringLiteral: stubbedSwiftVersion.description)) + ) + + pluginService.fetchRemotePluginsStub = { _ in + _ = Plugins.test() + } + + try fileHandler.touch( + stubbedPath.appending( + component: Manifest.package.fileName(stubbedPath) + ) + ) + + // When + try await subject.run( + path: stubbedPath.pathString, + update: true + ) + + // Then + XCTAssertTrue(swiftPackageManagerController.invokedUpdate) + XCTAssertFalse(swiftPackageManagerController.invokedResolve) + } + + func test_run_when_installing_plugins() async throws { + // Given + let config = Config.test( + plugins: [ + .git(url: "url", gitReference: .tag("tag"), directory: nil, releaseUrl: nil), + ] + ) + given(configLoader) + .loadConfig(path: .any) + .willReturn(config) + var invokedConfig: Config? + pluginService.loadPluginsStub = { config in + invokedConfig = config + return .test() + } + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(nil) + + // When + try await subject.run( + path: nil, + update: false + ) + + // Then + XCTAssertEqual(invokedConfig, config) + } + + func test_run_when_installing_dependencies() async throws { + // Given + let stubbedPath = try temporaryPath() + + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(stubbedPath.appending(components: "Tuist", "Package.swift")) + + let stubbedSwiftVersion = TSCUtility.Version(5, 3, 0) + given(configLoader) + .loadConfig(path: .any) + .willReturn( + Config.test(swiftVersion: .init(stringLiteral: stubbedSwiftVersion.description)) + ) + + pluginService.fetchRemotePluginsStub = { _ in } + + try fileHandler.touch( + stubbedPath.appending( + component: Manifest.package.fileName(stubbedPath) + ) + ) + + // When + try await subject.run( + path: stubbedPath.pathString, + update: false + ) + + // Then + XCTAssertTrue(swiftPackageManagerController.invokedResolve) + XCTAssertFalse(swiftPackageManagerController.invokedUpdate) + } + + func test_install_when_from_a_tuist_project_directory() async throws { + // Given + let temporaryDirectory = try temporaryPath() + let expectedFoundPackageLocation = temporaryDirectory.appending( + components: Constants.tuistDirectoryName, Manifest.package.fileName(temporaryDirectory) + ) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(expectedFoundPackageLocation) + + // Dependencies.swift in root + try fileHandler.touch(expectedFoundPackageLocation) + + // When - This will cause the `loadDependenciesStub` closure to be called and assert if needed + try await subject.run( + path: temporaryDirectory.pathString, + update: false + ) + } +} diff --git a/Tests/TuistKitTests/Services/ListServiceTests.swift b/Tests/TuistKitTests/Services/ListServiceTests.swift index 822839d2a95..c7347b5aed8 100644 --- a/Tests/TuistKitTests/Services/ListServiceTests.swift +++ b/Tests/TuistKitTests/Services/ListServiceTests.swift @@ -1,7 +1,7 @@ -import TSCBasic -import TuistGraph -import TuistGraphTesting +import Path +import TuistCore import TuistPluginTesting +import XcodeGraph import XCTest @testable import TuistCore @@ -58,7 +58,7 @@ final class ListServiceTests: TuistUnitTestCase { try await subject.run(path: nil, outputFormat: .table) // Then - XCTAssertPrinterContains(expectedOutput, at: .info, ==) + XCTAssertPrinterContains(expectedOutput, at: .notice, ==) } func test_lists_available_templates_json_format() async throws { @@ -89,7 +89,7 @@ final class ListServiceTests: TuistUnitTestCase { try await subject.run(path: nil, outputFormat: .json) // Then - XCTAssertPrinterContains(expectedOutput, at: .info, ==) + XCTAssertPrinterContains(expectedOutput, at: .notice, ==) } func test_lists_available_templates_with_plugins() async throws { @@ -120,6 +120,6 @@ final class ListServiceTests: TuistUnitTestCase { try await subject.run(path: nil, outputFormat: .table) // Then - XCTAssertPrinterContains(expectedOutput, at: .info, ==) + XCTAssertPrinterContains(expectedOutput, at: .notice, ==) } } diff --git a/Tests/TuistKitTests/Services/LogoutServiceTests.swift b/Tests/TuistKitTests/Services/LogoutServiceTests.swift new file mode 100644 index 00000000000..3f16a42a7c0 --- /dev/null +++ b/Tests/TuistKitTests/Services/LogoutServiceTests.swift @@ -0,0 +1,51 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistCoreTesting +import TuistLoader +import TuistLoaderTesting +import TuistServer +import TuistSupport +import XcodeGraph +import XCTest + +@testable import TuistKit +@testable import TuistSupportTesting + +final class LogoutServiceTests: TuistUnitTestCase { + private var serverSessionController: MockServerSessionControlling! + private var subject: LogoutService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + serverSessionController = MockServerSessionControlling() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + subject = LogoutService( + serverSessionController: serverSessionController, + configLoader: configLoader + ) + } + + override func tearDown() { + serverSessionController = nil + serverURL = nil + configLoader = nil + subject = nil + super.tearDown() + } + + func test_logout() async throws { + // Given + given(serverSessionController) + .logout(serverURL: .value(serverURL)) + .willReturn(()) + + // When / Then + try await subject.logout(directory: nil) + } +} diff --git a/Tests/TuistKitTests/Services/Migration/MigrationCheckEmptyBuildSettingsServiceTests.swift b/Tests/TuistKitTests/Services/Migration/MigrationCheckEmptyBuildSettingsServiceTests.swift index 9d284ceb7ca..f67e253bad0 100644 --- a/Tests/TuistKitTests/Services/Migration/MigrationCheckEmptyBuildSettingsServiceTests.swift +++ b/Tests/TuistKitTests/Services/Migration/MigrationCheckEmptyBuildSettingsServiceTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistKitTests/Services/Organization/OrganizationInviteServiceTests.swift b/Tests/TuistKitTests/Services/Organization/OrganizationInviteServiceTests.swift new file mode 100644 index 00000000000..829e7968d8c --- /dev/null +++ b/Tests/TuistKitTests/Services/Organization/OrganizationInviteServiceTests.swift @@ -0,0 +1,67 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistKit + +final class OrganizationInviteServiceTests: TuistUnitTestCase { + private var createOrganizationInviteService: MockCreateOrganizationInviteServicing! + private var subject: OrganizationInviteService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + + createOrganizationInviteService = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + subject = OrganizationInviteService( + createOrganizationInviteService: createOrganizationInviteService, + configLoader: configLoader + ) + } + + override func tearDown() { + createOrganizationInviteService = nil + configLoader = nil + serverURL = nil + subject = nil + super.tearDown() + } + + func test_invite() async throws { + // Given + given(createOrganizationInviteService) + .createOrganizationInvite( + organizationName: .value("tuist"), + email: .value("tuist@test.io"), + serverURL: .value(serverURL) + ) + .willReturn( + .test( + inviteeEmail: "tuist@test.io", + token: "invitation-token" + ) + ) + + // When + try await subject.run( + organizationName: "tuist", + email: "tuist@test.io", + directory: nil + ) + + // Then + XCTAssertPrinterOutputContains(""" + tuist@test.io was successfully invited to the tuist organization 🎉 + + You can also share with them the invite link directly: \(serverURL.absoluteString)/auth/invitations/invitation-token + """) + } +} diff --git a/Tests/TuistKitTests/Services/Organization/OrganizationListServiceTests.swift b/Tests/TuistKitTests/Services/Organization/OrganizationListServiceTests.swift new file mode 100644 index 00000000000..e4d922cac7e --- /dev/null +++ b/Tests/TuistKitTests/Services/Organization/OrganizationListServiceTests.swift @@ -0,0 +1,72 @@ +import Foundation +import Mockable +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistKit + +final class OrganizationListServiceTests: TuistUnitTestCase { + private var listOrganizationsService: MockListOrganizationsServicing! + private var subject: OrganizationListService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + + listOrganizationsService = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + + subject = OrganizationListService( + listOrganizationsService: listOrganizationsService, + configLoader: configLoader + ) + } + + override func tearDown() { + listOrganizationsService = nil + subject = nil + + super.tearDown() + } + + func test_organization_list() async throws { + // Given + given(listOrganizationsService).listOrganizations(serverURL: .any) + .willReturn( + [ + "test-one", + "test-two", + ] + ) + + // When + try await subject.run(json: false, directory: nil) + + // Then + XCTAssertPrinterOutputContains(""" + Listing all your organizations: + • test-one + • test-two + """) + } + + func test_organization_list_when_none() async throws { + // Given + given(listOrganizationsService).listOrganizations(serverURL: .any) + .willReturn([]) + + // When + try await subject.run(json: false, directory: nil) + + // Then + XCTAssertPrinterOutputContains( + "You currently have no Cloud organizations. Create one by running `tuist organization create`." + ) + } +} diff --git a/Tests/TuistKitTests/Services/Organization/OrganizationRemoveSSOServiceTests.swift b/Tests/TuistKitTests/Services/Organization/OrganizationRemoveSSOServiceTests.swift new file mode 100644 index 00000000000..77f81695669 --- /dev/null +++ b/Tests/TuistKitTests/Services/Organization/OrganizationRemoveSSOServiceTests.swift @@ -0,0 +1,60 @@ +import Foundation +import Mockable +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistKit + +final class OrganizationRemoveSSOServiceTests: TuistUnitTestCase { + private var updateOrganizationService: MockUpdateOrganizationServicing! + private var subject: OrganizationRemoveSSOService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + + updateOrganizationService = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + + subject = OrganizationRemoveSSOService( + updateOrganizationService: updateOrganizationService, + configLoader: configLoader + ) + } + + override func tearDown() { + updateOrganizationService = nil + configLoader = nil + subject = nil + + super.tearDown() + } + + func test_organization_remove_sso() async throws { + // Given + given(updateOrganizationService) + .updateOrganization( + organizationName: .value("tuist"), + serverURL: .value(serverURL), + ssoOrganization: .value(nil) + ) + .willReturn(.test()) + + // When + try await subject.run( + organizationName: "tuist", + directory: nil + ) + + // Then + XCTAssertPrinterOutputContains(""" + SSO for tuist was removed. + """) + } +} diff --git a/Tests/TuistKitTests/Services/Organization/OrganizationShowServiceTests.swift b/Tests/TuistKitTests/Services/Organization/OrganizationShowServiceTests.swift new file mode 100644 index 00000000000..bbfc33a04e7 --- /dev/null +++ b/Tests/TuistKitTests/Services/Organization/OrganizationShowServiceTests.swift @@ -0,0 +1,134 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistKit + +final class OrganizationShowServiceTests: TuistUnitTestCase { + private var getOrganizationService: MockGetOrganizationServicing! + private var getOrganizationUsageService: MockGetOrganizationUsageServicing! + private var subject: OrganizationShowService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + getOrganizationService = .init() + getOrganizationUsageService = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + subject = OrganizationShowService( + getOrganizationService: getOrganizationService, + getOrganizationUsageService: getOrganizationUsageService, + configLoader: configLoader + ) + } + + override func tearDown() { + getOrganizationService = nil + getOrganizationUsageService = nil + configLoader = nil + serverURL = nil + subject = nil + + super.tearDown() + } + + func test_organization_show() async throws { + // Given + given(getOrganizationService) + .getOrganization(organizationName: .any, serverURL: .any) + .willReturn( + .test( + name: "test-one", + plan: .air, + members: [ + .test( + name: "name-one", + email: "name-one@email.io", + role: .user + ), + .test( + name: "name-two", + email: "name-two@email.io", + role: .admin + ), + ], + invitations: [ + .test( + inviteeEmail: "invitee@email.io", + inviter: .test(name: "some-inviter") + ), + ] + ) + ) + + given(getOrganizationUsageService) + .getOrganizationUsage(organizationName: .any, serverURL: .any) + .willReturn(.test(currentMonthRemoteCacheHits: 210)) + + // When + try await subject.run( + organizationName: "tuist", + json: false, + directory: nil + ) + + // Then + XCTAssertPrinterOutputContains(""" + \(TerminalStyle.bold.open)Organization\(TerminalStyle.reset.open) + Name: test-one + Plan: Air + + \(TerminalStyle.bold.open)Usage\(TerminalStyle.reset.open) (current calendar month) + Remote cache hits: 210 + + \(TerminalStyle.bold.open)Organization members\(TerminalStyle.reset.open) (total number: 2) + username email role + name-one name-one@email.io user + name-two name-two@email.io admin + + \(TerminalStyle.bold.open)Invitations\(TerminalStyle.reset.open) (total number: 1) + inviter invitee email + some-inviter invitee@email.io + """) + } + + func test_organization_show_when_has_sso_provider() async throws { + // Given + given(getOrganizationService) + .getOrganization(organizationName: .any, serverURL: .any) + .willReturn( + .test( + name: "test-one", + plan: .pro, + ssoOrganization: .google("tuist.io") + ) + ) + given(getOrganizationUsageService) + .getOrganizationUsage(organizationName: .any, serverURL: .any) + .willReturn(.test()) + + // When + try await subject.run( + organizationName: "tuist", + json: false, + directory: nil + ) + + // Then + XCTAssertPrinterOutputContains( + """ + \(TerminalStyle.bold.open)Organization\(TerminalStyle.reset.open) + Name: test-one + Plan: Pro + SSO: Google (tuist.io) + """ + ) + } +} diff --git a/Tests/TuistKitTests/Services/Organization/OrganizationUpdateSSOServiceTests.swift b/Tests/TuistKitTests/Services/Organization/OrganizationUpdateSSOServiceTests.swift new file mode 100644 index 00000000000..b2f4e9932a9 --- /dev/null +++ b/Tests/TuistKitTests/Services/Organization/OrganizationUpdateSSOServiceTests.swift @@ -0,0 +1,62 @@ +import Foundation +import Mockable +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistKit + +final class OrganizationUpdateSSOServiceTests: TuistUnitTestCase { + private var updateOrganizationService: MockUpdateOrganizationServicing! + private var subject: OrganizationUpdateSSOService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + + updateOrganizationService = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + + subject = OrganizationUpdateSSOService( + updateOrganizationService: updateOrganizationService, + configLoader: configLoader + ) + } + + override func tearDown() { + updateOrganizationService = nil + configLoader = nil + subject = nil + + super.tearDown() + } + + func test_organization_update_sso() async throws { + // Given + given(updateOrganizationService) + .updateOrganization( + organizationName: .value("tuist"), + serverURL: .value(serverURL), + ssoOrganization: .value(.google("tuist.io")) + ) + .willReturn(.test()) + + // When + try await subject.run( + organizationName: "tuist", + provider: .google, + organizationId: "tuist.io", + directory: nil + ) + + // Then + XCTAssertPrinterOutputContains(""" + tuist now uses Google SSO with tuist.io. Users authenticated with the tuist.io SSO organization will automatically have access to the tuist projects. + """) + } +} diff --git a/Tests/TuistKitTests/Services/Plugin/PluginArchiveServiceTests.swift b/Tests/TuistKitTests/Services/Plugin/PluginArchiveServiceTests.swift index 1afa34748af..270da6c9367 100644 --- a/Tests/TuistKitTests/Services/Plugin/PluginArchiveServiceTests.swift +++ b/Tests/TuistKitTests/Services/Plugin/PluginArchiveServiceTests.swift @@ -1,22 +1,22 @@ -import TSCBasic +import MockableTest +import Path +import TuistLoader import TuistSupport import TuistSupportTesting import XCTest - -import TuistLoaderTesting @testable import TuistKit final class PluginArchiveServiceTests: TuistUnitTestCase { private var subject: PluginArchiveService! private var swiftPackageManagerController: MockSwiftPackageManagerController! - private var manifestLoader: MockManifestLoader! - private var fileArchiverFactory: MockFileArchivingFactory! + private var manifestLoader: MockManifestLoading! + private var fileArchiverFactory: MockFileArchivingFactorying! override func setUp() { super.setUp() swiftPackageManagerController = MockSwiftPackageManagerController() - manifestLoader = MockManifestLoader() - fileArchiverFactory = MockFileArchivingFactory() + manifestLoader = .init() + fileArchiverFactory = MockFileArchivingFactorying() subject = PluginArchiveService( swiftPackageManagerController: swiftPackageManagerController, manifestLoader: manifestLoader, @@ -32,7 +32,7 @@ final class PluginArchiveServiceTests: TuistUnitTestCase { super.tearDown() } - func test_run_when_no_task_products_defined() throws { + func test_run_when_no_task_products_defined() async throws { // Given swiftPackageManagerController.loadPackageInfoStub = { _ in PackageInfo.test( @@ -47,7 +47,7 @@ final class PluginArchiveServiceTests: TuistUnitTestCase { } // When - try subject.run(path: nil) + try await subject.run(path: nil) // Then XCTAssertPrinterContains( @@ -57,7 +57,7 @@ final class PluginArchiveServiceTests: TuistUnitTestCase { ) } - func test_run() throws { + func test_run() async throws { // Given let path = try temporaryPath() var invokedPackagePath: AbsolutePath? @@ -88,27 +88,32 @@ final class PluginArchiveServiceTests: TuistUnitTestCase { ] ) } - manifestLoader.loadPluginStub = { _ in - .test(name: "TestPlugin") - } + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn(.test(name: "TestPlugin")) var builtProducts: [String] = [] swiftPackageManagerController.loadBuildFatReleaseBinaryStub = { _, product, _, _ in builtProducts.append(product) } - let fileArchiver = MockFileArchiver() - fileArchiverFactory.stubbedMakeFileArchiverResult = fileArchiver + let fileArchiver = MockFileArchiving() + given(fileArchiverFactory).makeFileArchiver(for: .any).willReturn(fileArchiver) let zipPath = path.appending(components: "test-zip") - fileArchiver.stubbedZipResult = zipPath + given(fileArchiver).zip(name: .any).willReturn(zipPath) + given(fileArchiver) + .delete() + .willReturn() try fileHandler.createFolder(zipPath) // When - try subject.run(path: path.pathString) + try await subject.run(path: path.pathString) // Then XCTAssertEqual(invokedPackagePath, path) XCTAssertEqual(builtProducts, ["tuist-one", "tuist-two"]) - XCTAssertEqual(fileArchiver.invokedZipParameters?.name, "TestPlugin.tuist-plugin.zip") + + _ = verify(fileArchiver).zip(name: .value("TestPlugin.tuist-plugin.zip")) + XCTAssertTrue( fileHandler.isFolder( path.appending(component: "TestPlugin.tuist-plugin.zip") diff --git a/Tests/TuistKitTests/Services/Plugin/PluginBuildServiceTests.swift b/Tests/TuistKitTests/Services/Plugin/PluginBuildServiceTests.swift index 948ea77b6db..bb95bf4ac3c 100644 --- a/Tests/TuistKitTests/Services/Plugin/PluginBuildServiceTests.swift +++ b/Tests/TuistKitTests/Services/Plugin/PluginBuildServiceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport import TuistSupportTesting import XCTest diff --git a/Tests/TuistKitTests/Services/Plugin/PluginRunServiceTests.swift b/Tests/TuistKitTests/Services/Plugin/PluginRunServiceTests.swift index ebc2f8d8896..972ce85125d 100644 --- a/Tests/TuistKitTests/Services/Plugin/PluginRunServiceTests.swift +++ b/Tests/TuistKitTests/Services/Plugin/PluginRunServiceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport import TuistSupportTesting import XCTest diff --git a/Tests/TuistKitTests/Services/Plugin/PluginTestServiceTests.swift b/Tests/TuistKitTests/Services/Plugin/PluginTestServiceTests.swift index 02b24785b58..b21ef78768d 100644 --- a/Tests/TuistKitTests/Services/Plugin/PluginTestServiceTests.swift +++ b/Tests/TuistKitTests/Services/Plugin/PluginTestServiceTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import TuistSupport import TuistSupportTesting import XCTest diff --git a/Tests/TuistKitTests/Services/Project/ProjectDeleteServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectDeleteServiceTests.swift new file mode 100644 index 00000000000..64014f1cae1 --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectDeleteServiceTests.swift @@ -0,0 +1,72 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistKit + +final class ProjectDeleteServiceTests: TuistUnitTestCase { + private var getProjectService: MockGetProjectServicing! + private var deleteProjectService: MockDeleteProjectServicing! + private var credentialsStore: MockServerCredentialsStoring! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + private var subject: ProjectDeleteService! + + override func setUp() { + super.setUp() + + getProjectService = .init() + deleteProjectService = .init() + credentialsStore = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + subject = ProjectDeleteService( + deleteProjectService: deleteProjectService, + getProjectService: getProjectService, + credentialsStore: credentialsStore, + configLoader: configLoader + ) + } + + override func tearDown() { + deleteProjectService = nil + getProjectService = nil + credentialsStore = nil + configLoader = nil + serverURL = nil + subject = nil + + super.tearDown() + } + + func test_project_delete() async throws { + // Given + given(getProjectService) + .getProject( + fullHandle: .value("tuist-org/tuist"), + serverURL: .value(serverURL) + ) + .willReturn( + .test(id: 0, fullName: "tuist-org/tuist") + ) + given(deleteProjectService) + .deleteProject( + projectId: .value(0), + serverURL: .value(serverURL) + ) + .willReturn(()) + + given(credentialsStore) + .get(serverURL: .value(serverURL)) + .willReturn(.init(token: nil, accessToken: "access-token", refreshToken: "refresh-token")) + + // When / Then + try await subject.run(fullHandle: "tuist-org/tuist", directory: nil) + } +} diff --git a/Tests/TuistKitTests/Services/Project/ProjectListServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectListServiceTests.swift new file mode 100644 index 00000000000..5b847363d3b --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectListServiceTests.swift @@ -0,0 +1,73 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XcodeGraph +import XCTest +@testable import TuistKit + +final class ProjectListServiceTests: TuistUnitTestCase { + private var listProjectsService: MockListProjectsServicing! + private var subject: ProjectListService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + listProjectsService = .init() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + subject = ProjectListService( + listProjectsService: listProjectsService, + configLoader: configLoader + ) + } + + override func tearDown() { + listProjectsService = nil + configLoader = nil + serverURL = nil + subject = nil + + super.tearDown() + } + + func test_project_list() async throws { + // Given + given(listProjectsService) + .listProjects(serverURL: .value(serverURL)) + .willReturn( + [ + .test(id: 0, fullName: "tuist/test-one"), + .test(id: 1, fullName: "tuist/test-two"), + ] + ) + + // When + try await subject.run(json: false, directory: nil) + + // Then + XCTAssertPrinterOutputContains(""" + Listing all your projects: + • tuist/test-one + • tuist/test-two + """) + } + + func test_project_list_when_none() async throws { + // Given + given(listProjectsService) + .listProjects(serverURL: .value(serverURL)) + .willReturn([]) + + // When + try await subject.run(json: false, directory: nil) + + // Then + XCTAssertPrinterOutputContains( + "You currently have no Tuist projects. Create one by running `tuist project create`." + ) + } +} diff --git a/Tests/TuistKitTests/Services/Project/ProjectShowServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectShowServiceTests.swift new file mode 100644 index 00000000000..659a6f66423 --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectShowServiceTests.swift @@ -0,0 +1,154 @@ +import Foundation +import MockableTest +import TuistCore +import TuistLoader +import TuistServer +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistKit + +final class ProjectShowServiceTests: TuistUnitTestCase { + private var opener: MockOpening! + private var configLoader: MockConfigLoading! + private var serverURLService: MockServerURLServicing! + private var getProjectService: MockGetProjectServicing! + private var subject: ProjectShowService! + + override func setUp() { + super.setUp() + opener = MockOpening() + configLoader = MockConfigLoading() + serverURLService = MockServerURLServicing() + getProjectService = MockGetProjectServicing() + subject = ProjectShowService( + opener: opener, + configLoader: configLoader, + serverURLService: serverURLService, + getProjectService: getProjectService + ) + + given(serverURLService) + .url(configServerURL: .any) + .willReturn(Constants.URLs.production) + } + + override func tearDown() { + opener = nil + configLoader = nil + serverURLService = nil + getProjectService = nil + subject = nil + super.tearDown() + } + + func test_run_with_web_when_theFullHandleIsProvided() async throws { + // Given + var expectedURLComponents = URLComponents(url: Constants.URLs.production, resolvingAgainstBaseURL: false)! + expectedURLComponents.path = "/tuist/tuist" + given(opener).open(url: .value(expectedURLComponents.url!)).willReturn() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + + // When + try await subject.run(fullHandle: "tuist/tuist", web: true, path: nil) + + // Then + verify(opener).open(url: .value(expectedURLComponents.url!)).called(1) + } + + func test_run_with_web_when_theFullHandleIsNotProvided_and_aConfigWithFullHandleCanBeLoaded() async throws { + // Given + let path = try temporaryPath() + var expectedURLComponents = URLComponents(url: Constants.URLs.production, resolvingAgainstBaseURL: false)! + expectedURLComponents.path = "/tuist/tuist" + let config = Config.test(fullHandle: "tuist/tuist") + given(configLoader).loadConfig(path: .value(path)).willReturn(config) + given(opener).open(url: .any).willReturn() + + // When + try await subject.run(fullHandle: nil, web: true, path: path.pathString) + + // Then + verify(opener).open(url: .value(expectedURLComponents.url!)).called(1) + } + + func test_run_with_web_when_theFullHandleIsNotProvided_and_aConfigWithoutFullHandleCanBeLoaded() async throws { + // Given + let path = try temporaryPath() + var expectedURLComponents = URLComponents(url: Constants.URLs.production, resolvingAgainstBaseURL: false)! + expectedURLComponents.path = "/tuist/tuist" + given(opener).open(url: .value(expectedURLComponents.url!)).willReturn() + let config = Config.test(fullHandle: nil) + given(configLoader).loadConfig(path: .value(path)).willReturn(config) + + // When/Then + await XCTAssertThrowsSpecific({ + try await subject.run(fullHandle: nil, web: false, path: path.pathString) + }, ProjectShowServiceError.missingFullHandle) + } + + func test_run_when_full_handle_is_provided() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(getProjectService) + .getProject( + fullHandle: .value("tuist/tuist"), + serverURL: .any + ) + .willReturn( + .test( + fullName: "tuist/tuist", + defaultBranch: "main" + ) + ) + + // When + try await subject.run(fullHandle: "tuist/tuist", web: false, path: nil) + + // Then + XCTAssertStandardOutput( + pattern: """ + Full handle: tuist/tuist + Default branch: main + """ + ) + } + + func test_run_when_full_handle_is_not_provided() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn( + .test( + fullHandle: "tuist/tuist" + ) + ) + given(getProjectService) + .getProject( + fullHandle: .value("tuist/tuist"), + serverURL: .any + ) + .willReturn( + .test( + fullName: "tuist/tuist", + defaultBranch: "main" + ) + ) + + // When + try await subject.run(fullHandle: nil, web: false, path: nil) + + // Then + XCTAssertStandardOutput( + pattern: """ + Full handle: tuist/tuist + Default branch: main + """ + ) + } +} diff --git a/Tests/TuistKitTests/Services/Project/ProjectTokensCreateServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectTokensCreateServiceTests.swift new file mode 100644 index 00000000000..a98d4d29b64 --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectTokensCreateServiceTests.swift @@ -0,0 +1,62 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XCTest + +@testable import TuistKit + +final class ProjectTokensCreateServiceTests: TuistUnitTestCase { + private var createProjectTokenService: MockCreateProjectTokenServicing! + private var serverURLService: MockServerURLServicing! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + private var subject: ProjectTokensCreateService! + + override func setUp() { + super.setUp() + + createProjectTokenService = .init() + serverURLService = .init() + configLoader = .init() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(url: serverURL)) + given(serverURLService) + .url(configServerURL: .value(serverURL)) + .willReturn(serverURL) + subject = ProjectTokensCreateService( + createProjectTokenService: createProjectTokenService, + serverURLService: serverURLService, + configLoader: configLoader + ) + } + + override func tearDown() { + createProjectTokenService = nil + serverURLService = nil + configLoader = nil + serverURL = nil + subject = nil + + super.tearDown() + } + + func test_create_project_token() async throws { + // Given + given(createProjectTokenService) + .createProjectToken( + fullHandle: .value("tuist-org/tuist"), + serverURL: .any + ) + .willReturn("new-token") + + // When + try await subject.run(fullHandle: "tuist-org/tuist", directory: nil) + + // Then + XCTAssertStandardOutput(pattern: "new-token") + } +} diff --git a/Tests/TuistKitTests/Services/Project/ProjectTokensListServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectTokensListServiceTests.swift new file mode 100644 index 00000000000..caad2da524b --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectTokensListServiceTests.swift @@ -0,0 +1,98 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XCTest + +@testable import TuistKit + +final class ProjectTokensListServiceTests: TuistUnitTestCase { + private var listProjectTokensService: MockListProjectTokensServicing! + private var serverURLService: MockServerURLServicing! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + private var subject: ProjectTokensListService! + + override func setUp() { + super.setUp() + + listProjectTokensService = .init() + serverURLService = .init() + configLoader = .init() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(url: serverURL)) + given(serverURLService) + .url(configServerURL: .value(serverURL)) + .willReturn(serverURL) + subject = ProjectTokensListService( + listProjectTokensService: listProjectTokensService, + serverURLService: serverURLService, + configLoader: configLoader + ) + } + + override func tearDown() { + listProjectTokensService = nil + serverURLService = nil + configLoader = nil + serverURL = nil + subject = nil + + super.tearDown() + } + + func test_list_project_tokens() async throws { + // Given + given(listProjectTokensService) + .listProjectTokens( + fullHandle: .value("tuist-org/tuist"), + serverURL: .any + ) + .willReturn( + [ + .test( + id: "project-token-one", + insertedAt: Date(timeIntervalSince1970: 0) + ), + .test( + id: "project-token-two", + insertedAt: Date(timeIntervalSince1970: 10) + ), + ] + ) + + // When + try await subject.run(fullHandle: "tuist-org/tuist", directory: nil) + + // Then + XCTAssertStandardOutput( + pattern: """ + ID Created at + ───────────────── ───────────────────────── + project-token-one 1970-01-01 00:00:00 +0000 + project-token-two 1970-01-01 00:00:10 +0000 + """ + ) + } + + func test_list_project_tokens_when_none_present() async throws { + // Given + given(listProjectTokensService) + .listProjectTokens( + fullHandle: .value("tuist-org/tuist"), + serverURL: .any + ) + .willReturn([]) + + // When + try await subject.run(fullHandle: "tuist-org/tuist", directory: nil) + + // Then + XCTAssertStandardOutput( + pattern: "No project tokens found. Create one by running `tuist project tokens create tuist-org/tuist." + ) + } +} diff --git a/Tests/TuistKitTests/Services/Project/ProjectTokensRevokeServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectTokensRevokeServiceTests.swift new file mode 100644 index 00000000000..fcf5424a192 --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectTokensRevokeServiceTests.swift @@ -0,0 +1,67 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupportTesting +import XCTest + +@testable import TuistKit + +final class ProjectTokensRevokeServiceTests: TuistUnitTestCase { + private var revokeProjectTokenService: MockRevokeProjectTokenServicing! + private var serverURLService: MockServerURLServicing! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + private var subject: ProjectTokensRevokeService! + + override func setUp() { + super.setUp() + + revokeProjectTokenService = .init() + serverURLService = .init() + configLoader = .init() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(url: serverURL)) + given(serverURLService) + .url(configServerURL: .value(serverURL)) + .willReturn(serverURL) + subject = ProjectTokensRevokeService( + revokeProjectTokenService: revokeProjectTokenService, + serverURLService: serverURLService, + configLoader: configLoader + ) + } + + override func tearDown() { + revokeProjectTokenService = nil + serverURLService = nil + configLoader = nil + serverURL = nil + subject = nil + + super.tearDown() + } + + func test_revoke_project_token() async throws { + // Given + given(revokeProjectTokenService) + .revokeProjectToken( + projectTokenId: .value("project-token-id"), + fullHandle: .value("tuist-org/tuist"), + serverURL: .any + ) + .willReturn() + + // When + try await subject.run( + projectTokenId: "project-token-id", + fullHandle: "tuist-org/tuist", + directory: nil + ) + + // Then + XCTAssertStandardOutput(pattern: "The project token project-token-id was successfully revoked.") + } +} diff --git a/Tests/TuistKitTests/Services/Project/ProjectUpdateServiceTests.swift b/Tests/TuistKitTests/Services/Project/ProjectUpdateServiceTests.swift new file mode 100644 index 00000000000..dbab543e23f --- /dev/null +++ b/Tests/TuistKitTests/Services/Project/ProjectUpdateServiceTests.swift @@ -0,0 +1,126 @@ +import Foundation +import MockableTest +import TuistLoader +import TuistServer +import TuistSupport +import TuistSupportTesting + +@testable import TuistKit + +final class ProjectUpdateServiceTests: TuistUnitTestCase { + private var opener: MockOpening! + private var configLoader: MockConfigLoading! + private var serverURLService: MockServerURLServicing! + private var updateProjectService: MockUpdateProjectServicing! + private var subject: ProjectUpdateService! + + override func setUp() async throws { + try await super.setUp() + + opener = MockOpening() + configLoader = MockConfigLoading() + serverURLService = MockServerURLServicing() + updateProjectService = MockUpdateProjectServicing() + subject = ProjectUpdateService( + opener: opener, + configLoader: configLoader, + serverURLService: serverURLService, + updateProjectService: updateProjectService + ) + + given(serverURLService) + .url(configServerURL: .any) + .willReturn(Constants.URLs.production) + } + + override func tearDown() { + opener = nil + configLoader = nil + serverURLService = nil + updateProjectService = nil + subject = nil + super.tearDown() + } + + func test_run_when_full_handle_is_not_provided() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn( + .test( + fullHandle: "tuist/tuist" + ) + ) + given(updateProjectService) + .updateProject( + fullHandle: .any, + serverURL: .any, + defaultBranch: .any + ) + .willReturn(.test()) + + // When + try await subject.run(fullHandle: nil, defaultBranch: "new-default-branch", path: nil) + + // Then + verify(updateProjectService) + .updateProject( + fullHandle: .value("tuist/tuist"), + serverURL: .any, + defaultBranch: .value("new-default-branch") + ) + .called(1) + XCTAssertStandardOutput(pattern: "The project tuist/tuist was successfully updated 🎉") + } + + func test_run_when_full_handle_is_not_provided_and_is_not_in_config() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn( + .test( + fullHandle: nil + ) + ) + given(updateProjectService) + .updateProject( + fullHandle: .any, + serverURL: .any, + defaultBranch: .any + ) + .willReturn(.test()) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run(fullHandle: nil, defaultBranch: "new-default-branch", path: nil), + ProjectUpdateServiceError.missingFullHandle + ) + } + + func test_run_when_full_handle_is_provided() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(updateProjectService) + .updateProject( + fullHandle: .any, + serverURL: .any, + defaultBranch: .any + ) + .willReturn(.test()) + + // When + try await subject.run(fullHandle: "tuist/tuist", defaultBranch: "new-default-branch", path: nil) + + // Then + verify(updateProjectService) + .updateProject( + fullHandle: .value("tuist/tuist"), + serverURL: .any, + defaultBranch: .value("new-default-branch") + ) + .called(1) + XCTAssertStandardOutput(pattern: "The project tuist/tuist was successfully updated 🎉") + } +} diff --git a/Tests/TuistKitTests/Services/RunServiceTests.swift b/Tests/TuistKitTests/Services/RunServiceTests.swift index c69d4455d2a..4b9a0fc25cd 100644 --- a/Tests/TuistKitTests/Services/RunServiceTests.swift +++ b/Tests/TuistKitTests/Services/RunServiceTests.swift @@ -1,10 +1,14 @@ +import FileSystem import Foundation -import TSCBasic +import MockableTest +import Path import struct TSCUtility.Version +import TuistAutomation import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph import XCTest @testable import TuistAutomationTesting @@ -36,28 +40,51 @@ final class RunServiceErrorTests: TuistUnitTestCase { } final class RunServiceTests: TuistUnitTestCase { - var generator: MockGenerator! - var generatorFactory: MockGeneratorFactory! - var buildGraphInspector: MockBuildGraphInspector! - var targetBuilder: MockTargetBuilder! - var targetRunner: MockTargetRunner! - var subject: RunService! + private var generator: MockGenerating! + private var generatorFactory: MockGeneratorFactorying! + private var buildGraphInspector: MockBuildGraphInspecting! + private var targetBuilder: MockTargetBuilder! + private var targetRunner: MockTargetRunner! + private var configLoader: MockConfigLoading! + private var downloadPreviewService: MockDownloadPreviewServicing! + private var serverURLService: MockServerURLServicing! + private var appRunner: MockAppRunning! + private var subject: RunService! + private var remoteArtifactDownloader: MockRemoteArtifactDownloading! + private var appBundleLoader: MockAppBundleLoading! + private var fileArchiverFactory: MockFileArchivingFactorying! private struct TestError: Equatable, Error {} override func setUp() { super.setUp() - generator = MockGenerator() - generatorFactory = MockGeneratorFactory() - generatorFactory.stubbedDefaultResult = generator - buildGraphInspector = MockBuildGraphInspector() + generator = .init() + generatorFactory = MockGeneratorFactorying() + given(generatorFactory) + .defaultGenerator(config: .any) + .willReturn(generator) + buildGraphInspector = .init() targetBuilder = MockTargetBuilder() targetRunner = MockTargetRunner() + configLoader = .init() + downloadPreviewService = .init() + appRunner = .init() + remoteArtifactDownloader = .init() + appBundleLoader = .init() + fileArchiverFactory = .init() subject = RunService( generatorFactory: generatorFactory, buildGraphInspector: buildGraphInspector, targetBuilder: targetBuilder, - targetRunner: targetRunner + targetRunner: targetRunner, + configLoader: configLoader, + downloadPreviewService: downloadPreviewService, + fileHandler: fileHandler, + fileSystem: FileSystem(), + appRunner: appRunner, + remoteArtifactDownloader: remoteArtifactDownloader, + appBundleLoader: appBundleLoader, + fileArchiverFactory: fileArchiverFactory ) } @@ -73,73 +100,93 @@ final class RunServiceTests: TuistUnitTestCase { func test_run_generates_when_generateIsTrue() async throws { // Given - let expectation = expectation(description: "generates when required") - generator.generateWithGraphStub = { _ in - expectation.fulfill() - return (try AbsolutePath(validating: "/path/to/project.xcworkspace"), .test()) - } - buildGraphInspector.workspacePathStub = { _ in try! AbsolutePath(validating: "/path/to/project.xcworkspace") } - buildGraphInspector.runnableSchemesStub = { _ in [.test()] } - buildGraphInspector.runnableTargetStub = { _, _ in .test() } + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(generator) + .generateWithGraph(path: .any) + .willReturn((try AbsolutePath(validating: "/path/to/project.xcworkspace"), .test(), MapperEnvironment())) + given(buildGraphInspector) + .workspacePath(directory: .any) + .willReturn(try! AbsolutePath(validating: "/path/to/project.xcworkspace")) + given(buildGraphInspector) + .runnableSchemes(graphTraverser: .any) + .willReturn([.test()]) + given(buildGraphInspector) + .runnableTarget(scheme: .any, graphTraverser: .any) + .willReturn(.test()) try await subject.run(generate: true) - await fulfillment(of: [expectation], timeout: 1) } func test_run_generates_when_workspaceNotFound() async throws { // Given let workspacePath = try temporaryPath().appending(component: "App.xcworkspace") - let expectation = expectation(description: "generates when required") - generator.generateWithGraphStub = { _ in - // Then - self.buildGraphInspector.workspacePathStub = { _ in workspacePath } - expectation.fulfill() - return (workspacePath, .test()) - } - buildGraphInspector.workspacePathStub = { _ in nil } - buildGraphInspector.runnableSchemesStub = { _ in [.test()] } - buildGraphInspector.runnableTargetStub = { _, _ in .test() } + given(generator) + .generateWithGraph(path: .any) + .willReturn((workspacePath, .test(), MapperEnvironment())) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(generator) + .load(path: .any) + .willReturn(.test()) + given(buildGraphInspector) + .workspacePath(directory: .any) + .willReturn(workspacePath) + given(buildGraphInspector) + .runnableSchemes(graphTraverser: .any) + .willReturn([.test()]) + given(buildGraphInspector) + .runnableTarget(scheme: .any, graphTraverser: .any) + .willReturn(.test()) // When try await subject.run() - await fulfillment(of: [expectation], timeout: 1) } func test_run_buildsTarget() async throws { // Given let workspacePath = try temporaryPath().appending(component: "App.xcworkspace") - let expectation = expectation(description: "builds target") let schemeName = "AScheme" let clean = true let configuration = "Test" targetBuilder - .buildTargetStub = { _, _workspacePath, _scheme, _clean, _configuration, _, _, _, _, _, _ in + .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()) } + given(generator) + .load(path: .any) + .willReturn(.test()) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) targetRunner.assertCanRunTargetStub = { _ in } - buildGraphInspector.workspacePathStub = { _ in workspacePath } - buildGraphInspector.runnableSchemesStub = { _ in [.test(name: schemeName)] } - buildGraphInspector.runnableTargetStub = { _, _ in .test() } + given(buildGraphInspector) + .workspacePath(directory: .any) + .willReturn(workspacePath) + given(buildGraphInspector) + .runnableSchemes(graphTraverser: .any) + .willReturn([.test(name: schemeName)]) + given(buildGraphInspector) + .runnableTarget(scheme: .any, graphTraverser: .any) + .willReturn(.test()) // When try await subject.run( - schemeName: schemeName, + runnable: .scheme(schemeName), clean: clean, configuration: configuration ) - await fulfillment(of: [expectation], timeout: 1) } func test_run_runsTarget() async throws { // Given let workspacePath = try AbsolutePath(validating: "/path/to/project.xcworkspace") - let expectation = expectation(description: "runs target") let schemeName = "AScheme" let configuration = "Test" let minVersion = Target.test().deploymentTargets.configuredVersions.first?.versionString.version() @@ -156,24 +203,32 @@ final class RunServiceTests: TuistUnitTestCase { XCTAssertEqual(_version, version) XCTAssertEqual(_deviceName, deviceName) XCTAssertEqual(_arguments, arguments) - expectation.fulfill() } - generator.generateWithGraphStub = { _ in (workspacePath, .test()) } + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(generator) + .load(path: .any) + .willReturn(.test()) targetRunner.assertCanRunTargetStub = { _ in } - buildGraphInspector.workspacePathStub = { _ in workspacePath } - buildGraphInspector.runnableSchemesStub = { _ in [.test(name: schemeName)] } - buildGraphInspector.runnableTargetStub = { _, _ in .test() } + given(buildGraphInspector) + .workspacePath(directory: .any) + .willReturn(workspacePath) + given(buildGraphInspector) + .runnableSchemes(graphTraverser: .any) + .willReturn([.test(name: schemeName)]) + given(buildGraphInspector) + .runnableTarget(scheme: .any, graphTraverser: .any) + .willReturn(.test()) // When try await subject.run( - schemeName: schemeName, + runnable: .scheme(schemeName), configuration: configuration, device: deviceName, - version: version.description, + osVersion: version.description, arguments: arguments ) - - await fulfillment(of: [expectation], timeout: 1) } func test_run_throws_beforeBuilding_if_cantRunTarget() async throws { @@ -181,11 +236,22 @@ final class RunServiceTests: TuistUnitTestCase { let workspacePath = try temporaryPath().appending(component: "App.xcworkspace") let expectation = expectation(description: "does not run target builder") expectation.isInverted = true - generator.generateWithGraphStub = { _ in (workspacePath, .test()) } - buildGraphInspector.workspacePathStub = { _ in workspacePath } - buildGraphInspector.runnableSchemesStub = { _ in [.test()] } - buildGraphInspector.runnableTargetStub = { _, _ in .test() } - targetBuilder.buildTargetStub = { _, _, _, _, _, _, _, _, _, _, _ in expectation.fulfill() } + given(generator) + .load(path: .any) + .willReturn(.test()) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(buildGraphInspector) + .workspacePath(directory: .any) + .willReturn(workspacePath) + given(buildGraphInspector) + .runnableSchemes(graphTraverser: .any) + .willReturn([.test()]) + given(buildGraphInspector) + .runnableTarget(scheme: .any, graphTraverser: .any) + .willReturn(.test()) + targetBuilder.buildTargetStub = { _, _, _, _, _, _, _, _, _, _, _, _ in expectation.fulfill() } targetRunner.assertCanRunTargetStub = { _ in throw TestError() } // Then @@ -196,27 +262,190 @@ final class RunServiceTests: TuistUnitTestCase { ) await fulfillment(of: [expectation], timeout: 1) } + + func test_run_share_link_when_download_url_is_invalid() async throws { + // Given + given(downloadPreviewService) + .downloadPreview( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn("https://example com/page") + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run(runnable: .url(URL(string: "https://tuist.io/tuist/tuist/preview/some-id")!)), + RunServiceError.invalidDownloadBuildURL("https://example com/page") + ) + } + + func test_run_share_link_when_app_build_artifact_not_found() async throws { + // Given + given(downloadPreviewService) + .downloadPreview( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn("https://example.com") + + given(remoteArtifactDownloader) + .download(url: .any) + .willReturn(nil) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run(runnable: .url(URL(string: "https://tuist.io/tuist/tuist/preview/some-id")!)), + RunServiceError.appNotFound("https://tuist.io/tuist/tuist/preview/some-id") + ) + } + + func test_run_share_link_when_version_is_invalid() async throws { + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + runnable: .url(URL(string: "https://tuist.io/tuist/tuist/preview/some-id")!), + osVersion: "invalid-version" + ), + RunServiceError.invalidVersion("invalid-version") + ) + } + + func test_run_share_link_runs_app() async throws { + // Given + given(downloadPreviewService) + .downloadPreview( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn("https://example.com") + + let downloadedArchive = try temporaryPath().appending(component: "archive") + + given(remoteArtifactDownloader) + .download(url: .any) + .willReturn(downloadedArchive) + + let fileUnarchiver = MockFileUnarchiving() + given(fileArchiverFactory) + .makeFileUnarchiver(for: .any) + .willReturn(fileUnarchiver) + + let unarchivedPath = try temporaryPath().appending(component: "unarchived") + + given(fileUnarchiver) + .unzip() + .willReturn(unarchivedPath) + + try fileHandler.touch(unarchivedPath.appending(component: "App.app")) + + given(appRunner) + .runApp( + .any, + version: .any, + device: .any + ) + .willReturn() + + let appBundle: AppBundle = .test() + given(appBundleLoader) + .load(.any) + .willReturn(appBundle) + + // When + try await subject.run(runnable: .url(URL(string: "https://tuist.io/tuist/tuist/preview/some-id")!)) + + // Then + verify(appRunner) + .runApp( + .value([appBundle]), + version: .value(nil), + device: .value(nil) + ) + .called(1) + } + + func test_run_share_link_runs_with_destination_and_version() async throws { + // Given + given(downloadPreviewService) + .downloadPreview( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn("https://example.com") + + let downloadedArchive = try temporaryPath().appending(component: "archive") + + given(remoteArtifactDownloader) + .download(url: .any) + .willReturn(downloadedArchive) + + let fileUnarchiver = MockFileUnarchiving() + given(fileArchiverFactory) + .makeFileUnarchiver(for: .any) + .willReturn(fileUnarchiver) + + let unarchivedPath = try temporaryPath().appending(component: "unarchived") + + given(fileUnarchiver) + .unzip() + .willReturn(unarchivedPath) + + try fileHandler.touch(unarchivedPath.appending(component: "App.app")) + + given(appRunner) + .runApp( + .any, + version: .any, + device: .any + ) + .willReturn() + + let appBundle: AppBundle = .test() + given(appBundleLoader) + .load(.any) + .willReturn(appBundle) + + // When + try await subject.run( + runnable: .url(URL(string: "https://tuist.io/tuist/tuist/preview/some-id")!), + osVersion: "18.0", + arguments: ["-destination", "iPhone 15 Pro"] + ) + + // Then + verify(appRunner) + .runApp( + .value([appBundle]), + version: .value("18.0.0"), + device: .value("iPhone 15 Pro") + ) + .called(1) + } } extension RunService { fileprivate func run( - schemeName: String = Scheme.test().name, + runnable: Runnable = .scheme(Scheme.test().name), generate: Bool = false, clean: Bool = false, configuration: String? = nil, device: String? = nil, - version: String? = nil, + osVersion: String? = nil, rosetta: Bool = false, arguments: [String] = [] ) async throws { try await run( path: nil, - schemeName: schemeName, + runnable: runnable, generate: generate, clean: clean, configuration: configuration, device: device, - version: version, + osVersion: osVersion, rosetta: rosetta, arguments: arguments ) diff --git a/Tests/TuistKitTests/Services/ScaffoldServiceTests.swift b/Tests/TuistKitTests/Services/ScaffoldServiceTests.swift index 83a19240e25..f0ac7dfc5e5 100644 --- a/Tests/TuistKitTests/Services/ScaffoldServiceTests.swift +++ b/Tests/TuistKitTests/Services/ScaffoldServiceTests.swift @@ -1,10 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistLoader import TuistScaffold import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @@ -19,7 +20,7 @@ final class ScaffoldServiceTests: TuistUnitTestCase { var templateLoader: MockTemplateLoader! var templatesDirectoryLocator: MockTemplatesDirectoryLocator! var templateGenerator: MockTemplateGenerator! - var configLoader: MockConfigLoader! + var configLoader: MockConfigLoading! var pluginService: MockPluginService! override func setUp() { @@ -27,8 +28,11 @@ final class ScaffoldServiceTests: TuistUnitTestCase { templateLoader = MockTemplateLoader() templatesDirectoryLocator = MockTemplatesDirectoryLocator() templateGenerator = MockTemplateGenerator() - configLoader = MockConfigLoader() + configLoader = MockConfigLoading() pluginService = MockPluginService() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) subject = ScaffoldService( templateLoader: templateLoader, templatesDirectoryLocator: templatesDirectoryLocator, @@ -53,7 +57,7 @@ final class ScaffoldServiceTests: TuistUnitTestCase { description: "test", attributes: [ .required("required"), - .optional("optional", default: ""), + .optional("optional", default: .string("")), ], items: [] ) @@ -83,7 +87,7 @@ final class ScaffoldServiceTests: TuistUnitTestCase { description: "test", attributes: [ .required("required"), - .optional("optional", default: ""), + .optional("optional", default: .string("")), ], items: [] ) @@ -139,14 +143,14 @@ final class ScaffoldServiceTests: TuistUnitTestCase { func test_optional_attribute_is_taken_from_template() async throws { // Given templateLoader.loadTemplateStub = { _ in - Template.test(attributes: [.optional("optional", default: "optionalValue")]) + Template.test(attributes: [.optional("optional", default: .string("optionalValue"))]) } templatesDirectoryLocator.templateDirectoriesStub = { _ in [try self.temporaryPath().appending(component: "template")] } - var generateAttributes: [String: String] = [:] + var generateAttributes: [String: Template.Attribute.Value] = [:] templateGenerator.generateStub = { _, _, attributes in generateAttributes = attributes } @@ -156,16 +160,76 @@ final class ScaffoldServiceTests: TuistUnitTestCase { // Then XCTAssertEqual( - ["optional": "optionalValue"], + ["optional": .string("optionalValue")], + generateAttributes + ) + } + + func test_optional_dictionary_attribute_is_taken_from_template() async throws { + // Given + let context: Template.Attribute.Value = .dictionary([ + "key1": .string("value1"), + "key2": .string("value2"), + ]) + + templateLoader.loadTemplateStub = { _ in + Template.test(attributes: [.optional("optional", default: context)]) + } + + templatesDirectoryLocator.templateDirectoriesStub = { _ in + [try self.temporaryPath().appending(component: "template")] + } + + var generateAttributes: [String: Template.Attribute.Value] = [:] + templateGenerator.generateStub = { _, _, attributes in + generateAttributes = attributes + } + + // When + try await subject.testRun() + + // Then + XCTAssertEqual( + ["optional": context], + generateAttributes + ) + } + + func test_optional_integer_attribute_is_taken_from_template() async throws { + // Given + let defaultIntegerValue: Template.Attribute.Value = .integer(999) + + templateLoader.loadTemplateStub = { _ in + Template.test(attributes: [.optional("optional", default: defaultIntegerValue)]) + } + + templatesDirectoryLocator.templateDirectoriesStub = { _ in + [try self.temporaryPath().appending(component: "template")] + } + + var generateAttributes: [String: Template.Attribute.Value] = [:] + templateGenerator.generateStub = { _, _, attributes in + generateAttributes = attributes + } + + // When + try await subject.testRun() + + // Then + XCTAssertEqual( + ["optional": defaultIntegerValue], generateAttributes ) } func test_attributes_are_passed_to_generator() async throws { // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) templateLoader.loadTemplateStub = { _ in Template.test(attributes: [ - .optional("optional", default: ""), + .optional("optional", default: .string("")), .required("required"), ]) } @@ -174,7 +238,7 @@ final class ScaffoldServiceTests: TuistUnitTestCase { [try self.temporaryPath().appending(component: "template")] } - var generateAttributes: [String: String] = [:] + var generateAttributes: [String: Template.Attribute.Value] = [:] templateGenerator.generateStub = { _, _, attributes in generateAttributes = attributes } @@ -188,8 +252,8 @@ final class ScaffoldServiceTests: TuistUnitTestCase { // Then XCTAssertEqual( [ - "optional": "optionalValue", - "required": "requiredValue", + "optional": .string("optionalValue"), + "required": .string("requiredValue"), ], generateAttributes ) diff --git a/Tests/TuistKitTests/Services/SessionServiceTests.swift b/Tests/TuistKitTests/Services/SessionServiceTests.swift new file mode 100644 index 00000000000..fe324ff9863 --- /dev/null +++ b/Tests/TuistKitTests/Services/SessionServiceTests.swift @@ -0,0 +1,51 @@ +import Foundation +import MockableTest +import Path +import TuistCore +import TuistCoreTesting +import TuistLoader +import TuistLoaderTesting +import TuistServer +import TuistSupport +import XcodeGraph +import XCTest + +@testable import TuistKit +@testable import TuistSupportTesting + +final class SessionServiceTests: TuistUnitTestCase { + private var serverSessionController: MockServerSessionControlling! + private var subject: SessionService! + private var configLoader: MockConfigLoading! + private var serverURL: URL! + + override func setUp() { + super.setUp() + serverSessionController = MockServerSessionControlling() + configLoader = MockConfigLoading() + serverURL = URL(string: "https://test.cloud.tuist.io")! + given(configLoader).loadConfig(path: .any).willReturn(.test(url: serverURL)) + subject = SessionService( + serverSessionController: serverSessionController, + configLoader: configLoader + ) + } + + override func tearDown() { + serverSessionController = nil + configLoader = nil + serverURL = nil + subject = nil + super.tearDown() + } + + func test_printSession() async throws { + // Given + given(serverSessionController) + .printSession(serverURL: .value(serverURL)) + .willReturn(()) + + // When / Then + try await subject.printSession(directory: nil) + } +} diff --git a/Tests/TuistKitTests/Services/ShareServiceTests.swift b/Tests/TuistKitTests/Services/ShareServiceTests.swift new file mode 100644 index 00000000000..653bbca40f4 --- /dev/null +++ b/Tests/TuistKitTests/Services/ShareServiceTests.swift @@ -0,0 +1,541 @@ +import Foundation +import MockableTest +import TuistAutomation +import TuistCore +import TuistLoader +import TuistServer +import TuistSupport +import TuistSupportTesting +import XcodeGraph + +@testable import TuistKit + +final class ShareServiceTests: TuistUnitTestCase { + private var subject: ShareService! + private var xcodeProjectBuildDirectoryLocator: MockXcodeProjectBuildDirectoryLocating! + private var buildGraphInspector: MockBuildGraphInspecting! + private var previewsUploadService: MockPreviewsUploadServicing! + private var configLoader: MockConfigLoading! + private var serverURLService: MockServerURLServicing! + private var manifestLoader: MockManifestLoading! + private var manifestGraphLoader: MockManifestGraphLoading! + private var userInputReader: MockUserInputReading! + private var defaultConfigurationFetcher: MockDefaultConfigurationFetching! + private var appBundleLoader: MockAppBundleLoading! + + override func setUp() { + super.setUp() + + xcodeProjectBuildDirectoryLocator = .init() + buildGraphInspector = .init() + previewsUploadService = .init() + configLoader = .init() + serverURLService = .init() + manifestLoader = .init() + manifestGraphLoader = .init() + userInputReader = .init() + defaultConfigurationFetcher = .init() + appBundleLoader = .init() + subject = ShareService( + fileHandler: fileHandler, + xcodeProjectBuildDirectoryLocator: xcodeProjectBuildDirectoryLocator, + buildGraphInspector: buildGraphInspector, + previewsUploadService: previewsUploadService, + configLoader: configLoader, + serverURLService: serverURLService, + manifestLoader: manifestLoader, + manifestGraphLoader: manifestGraphLoader, + userInputReader: userInputReader, + defaultConfigurationFetcher: defaultConfigurationFetcher, + appBundleLoader: appBundleLoader + ) + + given(serverURLService) + .url(configServerURL: .any) + .willReturn(Constants.URLs.production) + + Matcher.register([GraphTarget].self) + } + + override func tearDown() { + xcodeProjectBuildDirectoryLocator = nil + buildGraphInspector = nil + previewsUploadService = nil + configLoader = nil + serverURLService = nil + manifestLoader = nil + manifestGraphLoader = nil + userInputReader = nil + defaultConfigurationFetcher = nil + subject = nil + + Matcher.reset() + + super.tearDown() + } + + func test_share_tuist_project_when_multiple_apps_specified() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(true) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + path: nil, + apps: ["AppOne", "AppTwo"], + configuration: nil, + platforms: [], + derivedDataPath: nil + ), + ShareServiceError.multipleAppsSpecified(["AppOne", "AppTwo"]) + ) + } + + func test_share_tuist_project() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(true) + + let projectPath = try temporaryPath() + let appTarget: Target = .test( + name: "AppTarget", + destinations: [.appleVision, .iPhone], + productName: "App" + ) + let appTargetTwo: Target = .test(name: "AppTwo") + let project: Project = .test( + targets: [ + appTarget, + appTargetTwo, + ] + ) + let graphAppTarget = GraphTarget(path: projectPath, target: appTarget, project: project) + let graphAppTargetTwo = GraphTarget(path: projectPath, target: appTargetTwo, project: project) + + given(manifestGraphLoader) + .load(path: .any) + .willReturn( + ( + .test( + projects: [ + projectPath: project, + ] + ), + [], + MapperEnvironment(), + [] + ) + ) + + given(userInputReader) + .readValue( + asking: .any, + values: .value([ + graphAppTarget, + graphAppTargetTwo, + ]), + valueDescription: .any + ) + .willReturn(graphAppTarget) + + given(defaultConfigurationFetcher) + .fetch(configuration: .any, config: .any, graph: .any) + .willReturn("Debug") + + let iosPath = try temporaryPath() + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .value(.iOS), + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(iosPath) + try fileHandler.touch(iosPath.appending(component: "App.app")) + + let visionOSPath = try temporaryPath() + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .value(.visionOS), + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(visionOSPath) + try fileHandler.touch(visionOSPath.appending(component: "App.app")) + + let shareURL: URL = .test() + given(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn(shareURL) + + // When + try await subject.run( + path: nil, + apps: [], + configuration: nil, + platforms: [], + derivedDataPath: nil + ) + + // Then + verify(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .value("tuist/tuist"), + serverURL: .value(Constants.URLs.production) + ) + .called(1) + + XCTAssertStandardOutput( + pattern: "App uploaded – share it with others using the following link: \(shareURL.absoluteString)" + ) + } + + func test_share_tuist_project_with_a_specified_app() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(true) + + let projectPath = try temporaryPath() + let appTarget: Target = .test( + name: "App", + destinations: [.appleVision, .iPhone] + ) + let appTargetTwo: Target = .test(name: "AppTwo") + let project: Project = .test( + targets: [ + appTarget, + appTargetTwo, + ] + ) + let graphAppTargetTwo = GraphTarget(path: projectPath, target: appTargetTwo, project: project) + + given(manifestGraphLoader) + .load(path: .any) + .willReturn( + ( + .test( + projects: [ + projectPath: project, + ] + ), + [], + MapperEnvironment(), + [] + ) + ) + + given(userInputReader) + .readValue( + asking: .any, + values: .value([ + graphAppTargetTwo, + ]), + valueDescription: .any + ) + .willReturn(graphAppTargetTwo) + + given(defaultConfigurationFetcher) + .fetch(configuration: .any, config: .any, graph: .any) + .willReturn("Debug") + + let iosPath = try temporaryPath() + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .value(.iOS), + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(iosPath) + try fileHandler.touch(iosPath.appending(component: "AppTwo.app")) + + let shareURL: URL = .test() + given(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn(shareURL) + + // When + try await subject.run( + path: nil, + apps: ["AppTwo"], + configuration: nil, + platforms: [], + derivedDataPath: nil + ) + + // Then + verify(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .value("tuist/tuist"), + serverURL: .value(Constants.URLs.production) + ) + .called(1) + + XCTAssertStandardOutput( + pattern: "AppTwo uploaded – share it with others using the following link: \(shareURL.absoluteString)" + ) + } + + func test_share_xcode_app_when_no_app_specified() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(false) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + path: nil, + apps: [], + configuration: nil, + platforms: [], + derivedDataPath: nil + ), + ShareServiceError.appNotSpecified + ) + } + + func test_share_xcode_app_when_multiple_apps_specified() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(false) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + path: nil, + apps: ["AppOne", "AppTwo"], + configuration: nil, + platforms: [], + derivedDataPath: nil + ), + ShareServiceError.multipleAppsSpecified(["AppOne", "AppTwo"]) + ) + } + + func test_share_xcode_app_when_no_platforms_specified() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(false) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + path: nil, + apps: ["App"], + configuration: nil, + platforms: [], + derivedDataPath: nil + ), + ShareServiceError.platformsNotSpecified + ) + } + + func test_share_xcode_app_when_no_project_or_workspace_not_found() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(false) + + let path = try temporaryPath() + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + path: path.pathString, + apps: ["App"], + configuration: nil, + platforms: [.iOS], + derivedDataPath: nil + ), + ShareServiceError.projectOrWorkspaceNotFound(path: path.pathString) + ) + } + + func test_share_xcode_app() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + given(manifestLoader) + .hasRootManifest(at: .any) + .willReturn(false) + + let path = try temporaryPath() + let xcodeprojPath = try temporaryPath().appending(component: "App.xcodeproj") + try fileHandler.touch(xcodeprojPath) + + let iosPath = try temporaryPath() + given(xcodeProjectBuildDirectoryLocator) + .locate( + platform: .value(.iOS), + projectPath: .any, + derivedDataPath: .any, + configuration: .any + ) + .willReturn(iosPath) + try fileHandler.touch(iosPath.appending(component: "App.app")) + + let shareURL: URL = .test() + given(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn(shareURL) + + // When + try await subject.run( + path: path.pathString, + apps: ["App"], + configuration: nil, + platforms: [.iOS], + derivedDataPath: nil + ) + + // Then + verify(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .value("tuist/tuist"), + serverURL: .value(Constants.URLs.production) + ) + .called(1) + + XCTAssertStandardOutput( + pattern: "App uploaded – share it with others using the following link: \(shareURL.absoluteString)" + ) + } + + func test_share_different_apps() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + let appOne = try temporaryPath().appending(component: "AppOne.app") + let appTwo = try temporaryPath().appending(component: "AppTwo.app") + + given(appBundleLoader) + .load(.value(appOne)) + .willReturn(.test(infoPlist: .test(name: "AppOne"))) + + given(appBundleLoader) + .load(.value(appTwo)) + .willReturn(.test(infoPlist: .test(name: "AppTwo"))) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.run( + path: nil, + apps: [ + appOne.pathString, + appTwo.pathString, + ], + configuration: nil, + platforms: [], + derivedDataPath: nil + ), + ShareServiceError.multipleAppsSpecified(["AppOne", "AppTwo"]) + ) + } + + func test_share_apps() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test(fullHandle: "tuist/tuist")) + + let iosApp = try temporaryPath().appending(components: "iOS", "App.app") + let visionOSApp = try temporaryPath().appending(components: "visionOs", "App.app") + + given(appBundleLoader) + .load(.value(iosApp)) + .willReturn(.test(infoPlist: .test(name: "App"))) + + given(appBundleLoader) + .load(.value(visionOSApp)) + .willReturn(.test(infoPlist: .test(name: "App"))) + + let shareURL: URL = .test() + given(previewsUploadService) + .uploadPreviews( + .any, + fullHandle: .any, + serverURL: .any + ) + .willReturn(shareURL) + + // When + try await subject.run( + path: nil, + apps: [ + iosApp.pathString, + visionOSApp.pathString, + ], + configuration: nil, + platforms: [], + derivedDataPath: nil + ) + + // Then + verify(previewsUploadService) + .uploadPreviews( + .value([iosApp, visionOSApp]), + fullHandle: .value("tuist/tuist"), + serverURL: .value(Constants.URLs.production) + ) + .called(1) + + XCTAssertStandardOutput( + pattern: "App uploaded – share it with others using the following link: \(shareURL.absoluteString)" + ) + } +} diff --git a/Tests/TuistKitTests/Services/TestServiceTests.swift b/Tests/TuistKitTests/Services/TestServiceTests.swift index 85d1bf62120..2f80cf1823c 100644 --- a/Tests/TuistKitTests/Services/TestServiceTests.swift +++ b/Tests/TuistKitTests/Services/TestServiceTests.swift @@ -1,10 +1,12 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistAutomation import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistLoader +import TuistServer import TuistSupport +import XcodeGraph import XCTest @testable import TuistAutomationTesting @@ -14,14 +16,16 @@ import XCTest final class TestServiceTests: TuistUnitTestCase { private var subject: TestService! - private var generator: MockGenerator! - private var generatorFactory: MockGeneratorFactory! - private var xcodebuildController: MockXcodeBuildController! - private var buildGraphInspector: MockBuildGraphInspector! - private var simulatorController: MockSimulatorController! - private var contentHasher: MockContentHasher! + private var generator: MockGenerating! + private var generatorFactory: MockGeneratorFactorying! + private var xcodebuildController: MockXcodeBuildControlling! + private var buildGraphInspector: MockBuildGraphInspecting! + private var simulatorController: MockSimulatorControlling! + private var contentHasher: MockContentHashing! private var testsCacheTemporaryDirectory: TemporaryDirectory! - private var cacheDirectoriesProvider: MockCacheDirectoriesProvider! + private var cacheDirectoriesProvider: MockCacheDirectoriesProviding! + private var configLoader: MockConfigLoading! + private var cacheStorage: MockCacheStoring! override func setUpWithError() throws { try super.setUpWithError() @@ -32,23 +36,59 @@ final class TestServiceTests: TuistUnitTestCase { contentHasher = .init() testsCacheTemporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true) generatorFactory = .init() - generatorFactory.stubbedTestResult = generator - let mockCacheDirectoriesProvider = try MockCacheDirectoriesProvider() + + cacheStorage = .init() + + let cacheStorageFactory = MockCacheStorageFactorying() + given(cacheStorageFactory) + .cacheStorage(config: .any) + .willReturn(cacheStorage) + + given(cacheStorage) + .store(.any, cacheCategory: .any) + .willReturn() + + let mockCacheDirectoriesProvider = MockCacheDirectoriesProviding() cacheDirectoriesProvider = mockCacheDirectoriesProvider + let cacheDirectoryProviderFactory = MockCacheDirectoriesProviderFactoring() + given(cacheDirectoryProviderFactory) + .cacheDirectories() + .willReturn(mockCacheDirectoriesProvider) - contentHasher.hashStub = { _ in - "hash" - } + let runsCacheDirectory = try temporaryPath() + given(mockCacheDirectoriesProvider) + .cacheDirectory(for: .value(.runs)) + .willReturn(runsCacheDirectory) + + configLoader = .init() + + given(contentHasher) + .hash(Parameter.any) + .willReturn("hash") + + given(buildGraphInspector) + .buildArguments(project: .any, target: .any, configuration: .any, skipSigning: .any) + .willReturn([]) subject = TestService( - testsCacheTemporaryDirectory: testsCacheTemporaryDirectory, generatorFactory: generatorFactory, + cacheStorageFactory: cacheStorageFactory, xcodebuildController: xcodebuildController, buildGraphInspector: buildGraphInspector, simulatorController: simulatorController, contentHasher: contentHasher, - cacheDirectoryProviderFactory: MockCacheDirectoriesProviderFactory(provider: mockCacheDirectoriesProvider) + cacheDirectoryProviderFactory: cacheDirectoryProviderFactory, + configLoader: configLoader ) + + given(simulatorController) + .findAvailableDevice( + platform: .any, + version: .any, + minVersion: .any, + deviceName: .any + ) + .willReturn(.test()) } override func tearDown() { @@ -64,25 +104,25 @@ final class TestServiceTests: TuistUnitTestCase { } func test_validateParameters_noParameters() throws { - try subject.validateParameters(testTargets: [], skipTestTargets: []) + try TestService.validateParameters(testTargets: [], skipTestTargets: []) } func test_validateParameters_nonConflictingParameters_target() throws { - try subject.validateParameters( + try TestService.validateParameters( testTargets: [TestIdentifier(string: "test1")], skipTestTargets: [TestIdentifier(string: "test1/class1")] ) } func test_validateParameters_with_testTargets_and_no_skipTestTargets() throws { - try subject.validateParameters( + try TestService.validateParameters( testTargets: [TestIdentifier(target: "TestTarget", class: "TestClass")], skipTestTargets: [] ) } func test_validateParameters_nonConflictingParameters_targetClass() throws { - try subject.validateParameters( + try TestService.validateParameters( testTargets: [TestIdentifier(string: "test1/class1")], skipTestTargets: [TestIdentifier(string: "test1/class1/method1")] ) @@ -93,7 +133,7 @@ final class TestServiceTests: TuistUnitTestCase { let skipTestTargets = try [TestIdentifier(string: "test2")] let error = TestServiceError.nothingToSkip(skipped: skipTestTargets, included: testTargets) XCTAssertThrowsSpecific( - try subject.validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ), @@ -106,7 +146,7 @@ final class TestServiceTests: TuistUnitTestCase { let skipTestTargets = try [TestIdentifier(string: "test1/class2")] let error = TestServiceError.nothingToSkip(skipped: skipTestTargets, included: testTargets) XCTAssertThrowsSpecific( - try subject.validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ), @@ -119,7 +159,7 @@ final class TestServiceTests: TuistUnitTestCase { let skipTestTargets = try [TestIdentifier(string: "test1/class2/method2")] let error = TestServiceError.nothingToSkip(skipped: skipTestTargets, included: testTargets) XCTAssertThrowsSpecific( - try subject.validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ), @@ -132,7 +172,7 @@ final class TestServiceTests: TuistUnitTestCase { let skipTestTargets = try [TestIdentifier(string: "test1")] let error = TestServiceError.duplicatedTestTargets(Set(testTargets)) XCTAssertThrowsSpecific( - try subject.validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ), @@ -145,7 +185,7 @@ final class TestServiceTests: TuistUnitTestCase { let skipTestTargets = try [TestIdentifier(string: "test1/class1")] let error = TestServiceError.duplicatedTestTargets(Set(testTargets)) XCTAssertThrowsSpecific( - try subject.validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ), @@ -158,7 +198,7 @@ final class TestServiceTests: TuistUnitTestCase { let skipTestTargets = try [TestIdentifier(string: "test1/class1/method1")] let error = TestServiceError.duplicatedTestTargets(Set(testTargets)) XCTAssertThrowsSpecific( - try subject.validateParameters( + try TestService.validateParameters( testTargets: testTargets, skipTestTargets: skipTestTargets ), @@ -168,80 +208,136 @@ final class TestServiceTests: TuistUnitTestCase { func test_run_generates_project() async throws { // Given + givenGenerator() let path = try temporaryPath() - var generatedPath: AbsolutePath? - generator.generateWithGraphStub = { - generatedPath = $0 - return ($0, Graph.test()) - } + given(generator) + .generateWithGraph(path: .value(path)) + .willReturn((path, .test(), MapperEnvironment())) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) // When - try? await subject.testRun( + try await subject.testRun( path: path ) - - // Then - XCTAssertEqual(generatedPath, path) } - func test_run_tests_wtih_specified_arch() async throws { + func test_run_tests_with_specified_arch() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "App-Workspace"), - Scheme.test(name: "TestScheme"), - ] - } - buildGraphInspector.testableTargetStub = { scheme, _, _, _, _ in - GraphTarget.test( - target: Target.test( - name: scheme.name + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "App-Workspace"), + Scheme.test(name: "TestScheme"), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willProduce { scheme, _, _, _, _ in + GraphTarget.test( + target: Target.test( + name: scheme.name + ) ) + } + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "TestScheme")])), MapperEnvironment()) + } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .value(true), + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any ) - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } - var testedRosetta: Bool? - xcodebuildController.testStub = { _, _, _, _, rosetta, _, _, _, _, _, _, _ in - testedRosetta = rosetta - return [.standardOutput(.init(raw: "success"))] - } + .willReturn(()) - // When + // When / Then try await subject.testRun( schemeName: "TestScheme", path: try temporaryPath(), rosetta: true ) - - // Then - XCTAssertEqual(testedRosetta, true) } func test_run_tests_for_only_specified_scheme() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "App-Workspace"), - Scheme.test(name: "TestScheme"), - ] - } - buildGraphInspector.testableTargetStub = { scheme, _, _, _, _ in - GraphTarget.test( - target: Target.test( - name: scheme.name - ) + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "App-Workspace"), + Scheme.test(name: "TestScheme"), + ] ) - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willProduce { scheme, _, _, _, _ in + GraphTarget.test( + target: Target.test( + name: scheme.name + ) + ) + } + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "TestScheme")])), MapperEnvironment()) + } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } // When try await subject.testRun( @@ -255,25 +351,53 @@ final class TestServiceTests: TuistUnitTestCase { func test_run_tests_all_project_schemes() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "TestScheme"), - ] - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectSchemeOne"), - Scheme.test(name: "ProjectSchemeTwo"), - ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "TestScheme"), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectSchemeOne"), + Scheme.test(name: "ProjectSchemeTwo"), + ] + ) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(), MapperEnvironment()) + } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } try fileHandler.touch( testsCacheTemporaryDirectory.path.appending(component: "A") ) @@ -298,25 +422,57 @@ final class TestServiceTests: TuistUnitTestCase { func test_run_tests_individual_scheme() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "TestScheme"), - ] - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectSchemeOne"), - Scheme.test(name: "ProjectSchemeTwo"), - ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "TestScheme"), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectSchemeOne"), + Scheme.test(name: "ProjectSchemeTwo"), + ] + ) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test(workspace: .test(schemes: [.test(name: "ProjectSchemeOne"), .test(name: "ProjectSchemeTwo")])), + MapperEnvironment() + ) + } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } try fileHandler.touch( testsCacheTemporaryDirectory.path.appending(component: "A") ) @@ -334,50 +490,603 @@ final class TestServiceTests: TuistUnitTestCase { XCTAssertEqual(testedSchemes, ["ProjectSchemeOne"]) } - func test_run_tests_with_skipped_targets() async throws { + func test_run_tests_individual_scheme_with_no_test_actions() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "ProjectSchemeOneTests"), + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test(workspace: .test(schemes: [.test(name: "ProjectSchemeOne", testAction: .test(targets: []))])), + MapperEnvironment() + ) + } + var testedSchemes: [String] = [] + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } + try fileHandler.touch( + testsCacheTemporaryDirectory.path.appending(component: "A") + ) + try fileHandler.touch( + testsCacheTemporaryDirectory.path.appending(component: "B") + ) + + // When + try await subject.testRun( + schemeName: "ProjectSchemeOne", + path: try temporaryPath() + ) + + // Then + XCTAssertStandardOutput(pattern: "The scheme ProjectSchemeOne's test action has no tests to run, finishing early.") + XCTAssertEmpty(testedSchemes) + } + + func test_throws_when_scheme_does_not_exist_and_initial_graph_is_nil() async throws { + // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test( + projects: [ + try self.temporaryPath(): .test(schemes: [.test(name: "ProjectSchemeTwo")]), + ] + ), + MapperEnvironment() + ) + } + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.testRun( + schemeName: "ProjectSchemeOne", + path: try temporaryPath() + ), + TestServiceError.schemeNotFound( + scheme: "ProjectSchemeOne", + existing: ["ProjectSchemeTwo"] + ) + ) + } + + func test_throws_scheme_does_not_exist_in_initial_graph() async throws { + // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + var environment = MapperEnvironment() + environment.initialGraph = .test( + workspace: .test( + schemes: [.test(name: "ProjectSchemeTwo", testAction: .test(targets: []))] + ), + projects: [ + try temporaryPath(): .test(schemes: [.test(name: "ProjectSchemeTwo")]), ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + ) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test(), + environment + ) + } + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.testRun( + schemeName: "ProjectSchemeOne", + path: try temporaryPath() + ), + TestServiceError.schemeNotFound( + scheme: "ProjectSchemeOne", + existing: ["ProjectSchemeTwo"] + ) + ) + } + + func test_skips_running_tests_when_scheme_is_in_initial_graph_only() async throws { + // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } + var environment = MapperEnvironment() + environment.initialGraph = .test( + projects: [ + try temporaryPath(): .test(schemes: [.test(name: "ProjectSchemeOne")]), + ] + ) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test(), + environment + ) + } + + // When + try await subject.testRun( + schemeName: "ProjectSchemeOne", + path: try temporaryPath() + ) + + // Then + XCTAssertEmpty(testedSchemes) + XCTAssertStandardOutput(pattern: "The scheme ProjectSchemeOne's test action has no tests to run, finishing early.") + } + + func test_run_tests_when_part_is_cached() async throws { + // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + + let projectPathOne = try temporaryPath().appending(component: "ProjectOne") + let schemeOne = Scheme.test( + name: "ProjectSchemeOne", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetA")), + ] + ) + ) + let schemeTwo = Scheme.test( + name: "ProjectSchemeTwo", + testAction: .test( + targets: [] + ) + ) + + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([schemeOne, schemeTwo]) + given(buildGraphInspector) + .testableTarget( + scheme: .any, + testPlan: .any, + testTargets: .any, + skipTestTargets: .any, + graphTraverser: .any + ) + .willReturn(.test()) + var testedSchemes: [String] = [] + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } + var environment = MapperEnvironment() + environment.initialGraph = .test( + projects: [ + projectPathOne: .test( + path: projectPathOne, + targets: [ + .test(name: "TargetA", bundleId: "io.tuist.TargetA"), + .test(name: "TargetB", bundleId: "io.tuist.TargetB"), + .test(name: "TargetC", bundleId: "io.tuist.TargetC"), + ], + schemes: [ + .test( + name: "ProjectSchemeOne", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetA")), + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetB")), + ] + ) + ), + .test( + name: "ProjectSchemeTwo", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetC")), + ] + ) + ), + ] + ), + ] + ) + environment.testsCacheUntestedHashes = [ + .test(name: "TargetA", bundleId: "io.tuist.TargetA"): "hash-a", + ] + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test( + projects: [ + projectPathOne: .test( + path: projectPathOne, + targets: [ + .test(name: "TargetA"), + .test(name: "TargetB"), + ], + schemes: [schemeOne, schemeTwo] + ), + ] + ), + environment + ) + } + + // When + try await subject.testRun( + path: try temporaryPath() + ) + + // Then + XCTAssertEqual(testedSchemes, ["ProjectSchemeOne"]) + XCTAssertStandardOutput( + pattern: "The following targets have not changed since the last successful run and will be skipped: TargetB, TargetC" + ) + verify(cacheStorage) + .store( + .value( + [ + CacheStorableItem(name: "TargetA", hash: "hash-a"): [], + ] + ), + cacheCategory: .value(.selectiveTests) + ) + .called(1) + } + + func test_run_tests_when_part_is_cached_and_scheme_is_passed() async throws { + // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + + let projectPathOne = try temporaryPath().appending(component: "ProjectOne") + let schemeOne = Scheme.test( + name: "ProjectSchemeOne", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetA")), + ] + ) + ) + let schemeTwo = Scheme.test( + name: "ProjectSchemeTwo", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetD")), + ] + ) + ) + + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([schemeOne, schemeTwo]) + given(buildGraphInspector) + .testableTarget( + scheme: .any, + testPlan: .any, + testTargets: .any, + skipTestTargets: .any, + graphTraverser: .any + ) + .willReturn(.test()) + var testedSchemes: [String] = [] + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } + var environment = MapperEnvironment() + environment.initialGraph = .test( + projects: [ + projectPathOne: .test( + path: projectPathOne, + targets: [ + .test(name: "TargetA", bundleId: "io.tuist.TargetA"), + .test(name: "TargetB", bundleId: "io.tuist.TargetB"), + .test(name: "TargetC", bundleId: "io.tuist.TargetC"), + .test(name: "TargetD", bundleId: "io.tuist.TargetD"), + ], + schemes: [ + .test( + name: "ProjectSchemeOne", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetA")), + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetB")), + ] + ) + ), + .test( + name: "ProjectSchemeTwo", + testAction: .test( + targets: [ + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetC")), + .test(target: TargetReference(projectPath: projectPathOne, name: "TargetD")), + ] + ) + ), + ] + ), + ] + ) + environment.testsCacheUntestedHashes = [ + .test(name: "TargetA", bundleId: "io.tuist.TargetA"): "hash-a", + .test(name: "TargetD", bundleId: "io.tuist.TargetD"): "hash-d", + ] + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test( + projects: [ + projectPathOne: .test( + path: projectPathOne, + targets: [ + .test(name: "TargetA"), + .test(name: "TargetB"), + .test(name: "TargetC"), + .test(name: "TargetD"), + ], + schemes: [schemeOne, schemeTwo] + ), + ] + ), + environment + ) + } + + // When + try await subject.testRun( + schemeName: "ProjectSchemeTwo", + path: try temporaryPath() + ) + + // Then + XCTAssertEqual(testedSchemes, ["ProjectSchemeTwo"]) + XCTAssertStandardOutput( + pattern: "The following targets have not changed since the last successful run and will be skipped: TargetC" + ) + verify(cacheStorage) + .store( + .value( + [ + CacheStorableItem(name: "TargetD", hash: "hash-d"): [], + ] + ), + cacheCategory: .value(.selectiveTests) + ) + .called(1) + } + + func test_run_tests_with_skipped_targets() async throws { + // Given + given(generatorFactory) + .testing( + config: .any, + testPlan: .any, + includedTargets: .any, + excludedTargets: .value([]), + skipUITests: .any, + configuration: .any, + ignoreBinaryCache: .any, + ignoreSelectiveTesting: .any, + cacheStorage: .any + ) + .willReturn(generator) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectSchemeOneTests"), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "ProjectSchemeOneTests")])), MapperEnvironment()) + } + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + var testedSchemes: [String] = [] + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } // When try await subject.testRun( schemeName: "ProjectSchemeOneTests", path: try temporaryPath(), - skipTestTargets: [.init(target: "ProjectSchemeOnTests", class: "TestClass")] + skipTestTargets: [.init(target: "ProjectSchemeOneTests", class: "TestClass")] ) // Then XCTAssertEqual(testedSchemes, ["ProjectSchemeOneTests"]) - XCTAssertEqual(generatorFactory.invokedTestParameters?.excludedTargets, []) } func test_run_tests_all_project_schemes_when_fails() async throws { // Given - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectScheme"), - ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectScheme"), + ] + ) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(), MapperEnvironment()) + } + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) var testedSchemes: [String] = [] - xcodebuildController.testErrorStub = NSError.test() - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + throw NSError.test() + } try fileHandler.touch( testsCacheTemporaryDirectory.path.appending(component: "A") ) @@ -395,24 +1104,51 @@ final class TestServiceTests: TuistUnitTestCase { "ProjectScheme", ] ) - XCTAssertFalse( - fileHandler.exists(cacheDirectoriesProvider.cacheDirectory(for: .tests).appending(component: "A")) - ) +// XCTAssertFalse( +// fileHandler.exists(cacheDirectoriesProvider.cacheDirectory(for: .selectiveTests).appending(component: "A")) +// ) } func test_run_tests_when_no_project_schemes_present() async throws { // Given - buildGraphInspector.workspaceSchemesStub = { _ in - [] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(), MapperEnvironment()) + } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } // When try await subject.testRun( @@ -426,21 +1162,50 @@ final class TestServiceTests: TuistUnitTestCase { func test_run_uses_resource_bundle_path() async throws { // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) let expectedResourceBundlePath = try AbsolutePath(validating: "/test") var resourceBundlePath: AbsolutePath? - xcodebuildController.testStub = { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _ in - resourceBundlePath = gotResourceBundlePath - return [] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectScheme"), - ] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _, _ in + resourceBundlePath = gotResourceBundlePath + } + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(), MapperEnvironment()) + } + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectScheme"), + ] + ) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) // When try await subject.testRun( @@ -455,24 +1220,124 @@ final class TestServiceTests: TuistUnitTestCase { ) } + func test_run_saves_resource_bundle_when_cloud_is_configured() async throws { + // Given + givenGenerator() + var resultBundlePath: AbsolutePath? + let expectedResultBundlePath = try cacheDirectoriesProvider + .cacheDirectory(for: .runs) + .appending(components: "run-id", Constants.resultBundleName) + + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _, _ in + resultBundlePath = gotResourceBundlePath + } + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(), MapperEnvironment()) + } + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectScheme"), + ] + ) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + given(configLoader) + .loadConfig(path: .any) + .willReturn( + .test( + fullHandle: "tuist/tuist" + ) + ) + + let runsCacheDirectory = try temporaryPath() + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.runs)) + .willReturn(runsCacheDirectory) + + try fileHandler.createFolder(runsCacheDirectory) + + // When + try await subject.testRun( + runId: "run-id", + path: try temporaryPath() + ) + + // Then + XCTAssertEqual(resultBundlePath, expectedResultBundlePath) + } + func test_run_uses_resource_bundle_path_with_given_scheme() async throws { // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) let expectedResourceBundlePath = try AbsolutePath(validating: "/test") var resourceBundlePath: AbsolutePath? - xcodebuildController.testStub = { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _ in - resourceBundlePath = gotResourceBundlePath - return [] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectScheme"), - Scheme.test(name: "ProjectScheme2"), - ] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _, _ in + resourceBundlePath = gotResourceBundlePath + } + + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "ProjectScheme2")])), MapperEnvironment()) + } + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectScheme"), + Scheme.test(name: "ProjectScheme2"), + ] + ) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn([]) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) // When try await subject.testRun( @@ -490,25 +1355,53 @@ final class TestServiceTests: TuistUnitTestCase { func test_run_passes_retry_count_as_argument() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "TestScheme"), - ] - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectSchemeOne"), - ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "TestScheme"), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectSchemeOne"), + ] + ) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "ProjectSchemeOne")])), MapperEnvironment()) + } + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) var passedRetryCount = 0 - xcodebuildController.testStub = { _, _, _, _, _, _, _, _, retryCount, _, _, _ in - passedRetryCount = retryCount - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, _, _, _, _, _, _, _, retryCount, _, _, _, _ in + passedRetryCount = retryCount + } // When try await subject.testRun( @@ -523,25 +1416,50 @@ final class TestServiceTests: TuistUnitTestCase { func test_run_defaults_retry_count_to_zero() async throws { // Given - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "TestScheme"), - ] - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectSchemeOne"), - ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "TestScheme"), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectSchemeOne"), + ] + ) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "ProjectSchemeOne")])), MapperEnvironment()) + } + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) - var passedRetryCount = -1 - xcodebuildController.testStub = { _, _, _, _, _, _, _, _, retryCount, _, _, _ in - passedRetryCount = retryCount - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willReturn() // When try await subject.testRun( @@ -550,41 +1468,102 @@ final class TestServiceTests: TuistUnitTestCase { ) // Then - XCTAssertEqual(passedRetryCount, 0) + verify(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .value(0), + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .called(1) } func test_run_test_plan_success() async throws { // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) let testPlan = "TestPlan" let testPlanPath = try AbsolutePath(validating: "/testPlan/\(testPlan)") - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "App-Workspace"), - Scheme.test( - name: "TestScheme", - testAction: .test( - testPlans: [.init(path: testPlanPath, testTargets: [], isDefault: true)] + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "App-Workspace"), + Scheme.test( + name: "TestScheme", + testAction: .test( + testPlans: [.init(path: testPlanPath, testTargets: [], isDefault: true)] + ) + ), + ] + ) + given(buildGraphInspector) + .testableTarget( + scheme: .any, + testPlan: .value(testPlan), + testTargets: .any, + skipTestTargets: .any, + graphTraverser: .any + ) + .willProduce { scheme, _, _, _, _ in + GraphTarget.test( + target: Target.test( + name: scheme.name ) - ), - ] - } - var passedTestPlan: String? - buildGraphInspector.testableTargetStub = { scheme, testPlan, _, _, _ in - passedTestPlan = testPlan - return GraphTarget.test( - target: Target.test( - name: scheme.name ) - ) - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } + } + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn([]) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + ( + path, + .test( + workspace: .test( + schemes: [ + .test( + name: "TestScheme", + testAction: .test(targets: [.test()]) + ), + ] + ) + ), + MapperEnvironment() + ) + } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in - testedSchemes.append(scheme) - return [.standardOutput(.init(raw: "success"))] - } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willProduce { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + } // When try await subject.testRun( @@ -595,35 +1574,61 @@ final class TestServiceTests: TuistUnitTestCase { // Then XCTAssertEqual(testedSchemes, ["TestScheme"]) - XCTAssertEqual(passedTestPlan, testPlan) } func test_run_test_plan_failure() async throws { // Given + givenGenerator() + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) let testPlan = "TestPlan" let testPlanPath = try AbsolutePath(validating: "/testPlan/\(testPlan)") - buildGraphInspector.testableSchemesStub = { _ in - [ - Scheme.test(name: "App-Workspace"), - Scheme.test( - name: "TestScheme", - testAction: .test( - testPlans: [.init(path: testPlanPath, testTargets: [], isDefault: true)] - ) - ), - ] - } - buildGraphInspector.workspaceSchemesStub = { _ in - [ - Scheme.test(name: "ProjectSchemeOne"), - ] - } - generator.generateWithGraphStub = { path in - (path, Graph.test()) - } - xcodebuildController.testStub = { _, _, _, _, _, _, _, _, _, _, _, _ in - [.standardOutput(.init(raw: "success"))] - } + given(buildGraphInspector) + .testableSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "App-Workspace"), + Scheme.test( + name: "TestScheme", + testAction: .test( + testPlans: [.init(path: testPlanPath, testTargets: [], isDefault: true)] + ) + ), + ] + ) + given(buildGraphInspector) + .workspaceSchemes(graphTraverser: .any) + .willReturn( + [ + Scheme.test(name: "ProjectSchemeOne"), + ] + ) + given(buildGraphInspector) + .testableTarget(scheme: .any, testPlan: .any, testTargets: .any, skipTestTargets: .any, graphTraverser: .any) + .willReturn(.test()) + given(generator) + .generateWithGraph(path: .any) + .willProduce { path in + (path, .test(workspace: .test(schemes: [.test(name: "TestScheme")])), MapperEnvironment()) + } + given(xcodebuildController) + .test( + .any, + scheme: .any, + clean: .any, + destination: .any, + rosetta: .any, + derivedDataPath: .any, + resultBundlePath: .any, + arguments: .any, + retryCount: .any, + testTargets: .any, + skipTestTargets: .any, + testPlanConfiguration: .any, + passthroughXcodeBuildArguments: .any + ) + .willReturn(()) let notDefinedTestPlan = "NotDefined" do { @@ -641,12 +1646,27 @@ final class TestServiceTests: TuistUnitTestCase { throw error } } -} -// MARK: - Helpers + private func givenGenerator() { + given(generatorFactory) + .testing( + config: .any, + testPlan: .any, + includedTargets: .any, + excludedTargets: .any, + skipUITests: .any, + configuration: .any, + ignoreBinaryCache: .any, + ignoreSelectiveTesting: .any, + cacheStorage: .any + ) + .willReturn(generator) + } +} extension TestService { fileprivate func testRun( + runId: String = "run-id", schemeName: String? = nil, clean: Bool = false, configuration: String? = nil, @@ -662,9 +1682,11 @@ extension TestService { testTargets: [TestIdentifier] = [], skipTestTargets: [TestIdentifier] = [], testPlanConfiguration: TestPlanConfiguration? = nil, - generateOnly: Bool = false + generateOnly: Bool = false, + passthroughXcodeBuildArguments: [String] = [] ) async throws { try await run( + runId: runId, schemeName: schemeName, clean: clean, configuration: configuration, @@ -680,7 +1702,10 @@ extension TestService { testTargets: testTargets, skipTestTargets: skipTestTargets, testPlanConfiguration: testPlanConfiguration, - generateOnly: generateOnly + ignoreBinaryCache: false, + ignoreSelectiveTesting: false, + generateOnly: generateOnly, + passthroughXcodeBuildArguments: passthroughXcodeBuildArguments ) } } diff --git a/Tests/TuistKitTests/Services/TuistServiceTests.swift b/Tests/TuistKitTests/Services/TuistServiceTests.swift index 481e6fc12e3..c0b29dd1e3e 100644 --- a/Tests/TuistKitTests/Services/TuistServiceTests.swift +++ b/Tests/TuistKitTests/Services/TuistServiceTests.swift @@ -1,5 +1,6 @@ -import TSCBasic -import TuistLoaderTesting +import MockableTest +import Path +import TuistLoader import TuistPlugin import TuistPluginTesting import TuistSupport @@ -11,12 +12,12 @@ import XCTest final class TuistServiceTests: TuistUnitTestCase { private var subject: TuistService! private var pluginService: MockPluginService! - private var configLoader: MockConfigLoader! + private var configLoader: MockConfigLoading! override func setUp() { super.setUp() pluginService = MockPluginService() - configLoader = MockConfigLoader() + configLoader = MockConfigLoading() subject = TuistService( pluginService: pluginService, configLoader: configLoader @@ -30,14 +31,20 @@ final class TuistServiceTests: TuistUnitTestCase { super.tearDown() } - func test_run_when_command_not_found() throws { - XCTAssertThrowsSpecific( - try subject.run(arguments: ["my-command"], tuistBinaryPath: ""), + func test_run_when_command_not_found() async throws { + // Given + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) + + // When / Then + await XCTAssertThrowsSpecific( + { try await self.subject.run(arguments: ["my-command"], tuistBinaryPath: "") }, TuistServiceError.taskUnavailable ) } - func test_run_when_plugin_executable() throws { + func test_run_when_plugin_executable() async throws { // Given let path = try temporaryPath() let projectPath = path.appending(component: "Project") @@ -49,11 +56,9 @@ final class TuistServiceTests: TuistUnitTestCase { "--path", projectPath.pathString, ]) - var loadConfigPath: AbsolutePath? - configLoader.loadConfigStub = { configPath in - loadConfigPath = configPath - return .default - } + given(configLoader) + .loadConfig(path: .value(projectPath)) + .willReturn(.default) pluginService.remotePluginPathsStub = { _ in [ RemotePluginPaths( @@ -66,12 +71,11 @@ final class TuistServiceTests: TuistUnitTestCase { // When/Then XCTAssertNoThrow( - try subject.run(arguments: ["command-b", "--path", projectPath.pathString], tuistBinaryPath: "") + { try await self.subject.run(arguments: ["command-b", "--path", projectPath.pathString], tuistBinaryPath: "") } ) - XCTAssertEqual(loadConfigPath, projectPath) } - func test_run_when_command_is_global() throws { + func test_run_when_command_is_global() async throws { // Given var whichCommand: String? system.whichStub = { invokedWhichCommand in @@ -79,11 +83,18 @@ final class TuistServiceTests: TuistUnitTestCase { return "" } system.succeedCommand(["tuist-my-command", "argument-one"]) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.default) // When/Then - XCTAssertNoThrow( - try subject.run(arguments: ["my-command", "argument-one"], tuistBinaryPath: "") - ) + var _error: Error? + do { + try await subject.run(arguments: ["my-command", "argument-one"], tuistBinaryPath: "") + } catch { + _error = error + } + XCTAssertNil(_error) XCTAssertEqual(whichCommand, "tuist-my-command") } } diff --git a/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift b/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift new file mode 100644 index 00000000000..a56042ba24c --- /dev/null +++ b/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift @@ -0,0 +1,120 @@ +import FileSystem +import Mockable +import MockableTest +import TuistCore +import TuistServer +import TuistSupport +import XcodeGraph +import XCTest +@testable import TuistAnalytics +@testable import TuistCoreTesting +@testable import TuistKit +@testable import TuistSupportTesting + +final class TuistAnalyticsDispatcherTests: TuistUnitTestCase { + private var subject: TuistAnalyticsDispatcher! + private var createCommandEventService: MockCreateCommandEventServicing! + private var ciChecker: MockCIChecking! + private var cacheDirectoriesProviderFactory: MockCacheDirectoriesProviderFactoring! + private var analyticsArtifactUploadService: MockAnalyticsArtifactUploadServicing! + + override func setUp() { + super.setUp() + createCommandEventService = .init() + ciChecker = .init() + cacheDirectoriesProviderFactory = .init() + analyticsArtifactUploadService = .init() + } + + override func tearDown() { + subject = nil + createCommandEventService = nil + ciChecker = nil + cacheDirectoriesProviderFactory = nil + analyticsArtifactUploadService = nil + super.tearDown() + } + + func testDispatch_whenAnalyticsIsEnabled_sendsToServer() throws { + // Given + let fullHandle = "project" + let url = URL.test() + let backend = TuistAnalyticsServerBackend( + fullHandle: fullHandle, + url: url, + createCommandEventService: createCommandEventService, + fileHandler: fileHandler, + ciChecker: ciChecker, + cacheDirectoriesProviderFactory: cacheDirectoriesProviderFactory, + analyticsArtifactUploadService: analyticsArtifactUploadService, + fileSystem: FileSystem() + ) + subject = TuistAnalyticsDispatcher( + backend: backend + ) + + given(ciChecker) + .isCI() + .willReturn(false) + given(createCommandEventService) + .createCommandEvent( + commandEvent: .matching { commandEvent in + commandEvent.name == Self.commandEvent.name + }, + projectId: .value(fullHandle), + serverURL: .value(url) + ) + .willReturn(.test(id: 10)) + + given(analyticsArtifactUploadService) + .uploadResultBundle( + .any, + targetHashes: .any, + graphPath: .any, + commandEventId: .value(10), + serverURL: .value(url) + ) + .willReturn(()) + + let cacheDirectoriesProvider = MockCacheDirectoriesProviding() + + given(cacheDirectoriesProviderFactory) + .cacheDirectories() + .willReturn(cacheDirectoriesProvider) + + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.runs)) + .willReturn(try temporaryPath()) + + // When + let expectation = XCTestExpectation(description: "completion is called") + try subject.dispatch(event: Self.commandEvent, completion: { expectation.fulfill() }) + + // Then + _ = XCTWaiter.wait(for: [expectation], timeout: 1.0) + } + + static var commandEvent: CommandEvent { + CommandEvent( + runId: "run-id", + name: "event", + subcommand: nil, + params: [:], + commandArguments: ["event"], + durationInMs: 100, + clientId: "client", + tuistVersion: "2.0.0", + swiftVersion: "5.5", + macOSVersion: "12.0", + machineHardwareName: "arm64", + isCI: false, + status: .success + ) + } + + static func commandEventData() throws -> Data { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + return try encoder.encode(Self.commandEvent) + } +} diff --git a/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift b/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift new file mode 100644 index 00000000000..471ca981da8 --- /dev/null +++ b/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift @@ -0,0 +1,156 @@ +import FileSystem +import MockableTest +import TuistAnalytics +import TuistCore +import TuistCoreTesting +import TuistServer +import TuistSupport +import XcodeGraph +import XCTest +@testable import TuistKit +@testable import TuistSupportTesting + +final class TuistAnalyticsServerBackendTests: TuistUnitTestCase { + private var fullHandle = "tuist-org/tuist" + private var createCommandEventService: MockCreateCommandEventServicing! + private var ciChecker: MockCIChecking! + private var cacheDirectoriesProviderFactory: MockCacheDirectoriesProviderFactoring! + private var analyticsArtifactUploadService: MockAnalyticsArtifactUploadServicing! + private var cacheDirectoriesProvider: MockCacheDirectoriesProviding! + private var subject: TuistAnalyticsServerBackend! + + override func setUpWithError() throws { + super.setUp() + createCommandEventService = .init() + ciChecker = .init() + cacheDirectoriesProviderFactory = .init() + analyticsArtifactUploadService = .init() + cacheDirectoriesProvider = .init() + given(cacheDirectoriesProviderFactory) + .cacheDirectories() + .willReturn(cacheDirectoriesProvider) + subject = TuistAnalyticsServerBackend( + fullHandle: fullHandle, + url: Constants.URLs.production, + createCommandEventService: createCommandEventService, + fileHandler: fileHandler, + ciChecker: ciChecker, + cacheDirectoriesProviderFactory: cacheDirectoriesProviderFactory, + analyticsArtifactUploadService: analyticsArtifactUploadService, + fileSystem: FileSystem() + ) + } + + override func tearDown() { + createCommandEventService = nil + ciChecker = nil + cacheDirectoriesProviderFactory = nil + analyticsArtifactUploadService = nil + subject = nil + super.tearDown() + } + + func test_send_when_is_not_ci() async throws { + // Given + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.runs)) + .willReturn(try temporaryPath()) + given(ciChecker) + .isCI() + .willReturn(false) + let event = CommandEvent.test() + given(createCommandEventService) + .createCommandEvent( + commandEvent: .value(event), + projectId: .value(fullHandle), + serverURL: .value(Constants.URLs.production) + ) + .willReturn( + .test( + id: 10, + url: URL(string: "https://cloud.tuist.io/tuist-org/tuist/runs/10")! + ) + ) + + // When + try await subject.send(commandEvent: event) + + // Then + XCTAssertPrinterOutputNotContains("You can view a detailed report at: https://cloud.tuist.io/tuist-org/tuist/runs/10") + } + + func test_send_when_is_ci() async throws { + // Given + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.runs)) + .willReturn(try temporaryPath()) + given(ciChecker) + .isCI() + .willReturn(true) + let event = CommandEvent.test() + given(createCommandEventService) + .createCommandEvent( + commandEvent: .value(event), + projectId: .value(fullHandle), + serverURL: .value(Constants.URLs.production) + ) + .willReturn( + .test( + id: 10, + url: URL(string: "https://cloud.tuist.io/tuist-org/tuist/runs/10")! + ) + ) + + // When + try await subject.send(commandEvent: event) + + // Then + XCTAssertStandardOutput(pattern: "You can view a detailed report at: https://cloud.tuist.io/tuist-org/tuist/runs/10") + } + + func test_send_when_is_ci_and_result_bundle_exists() async throws { + // Given + given(ciChecker) + .isCI() + .willReturn(true) + let event = CommandEvent.test() + given(createCommandEventService) + .createCommandEvent( + commandEvent: .value(event), + projectId: .value(fullHandle), + serverURL: .value(Constants.URLs.production) + ) + .willReturn( + .test( + id: 10, + url: URL(string: "https://cloud.tuist.io/tuist-org/tuist/runs/10")! + ) + ) + + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.runs)) + .willReturn(try temporaryPath()) + + let resultBundle = try cacheDirectoriesProvider + .cacheDirectory(for: .runs) + .appending(components: event.runId, "\(Constants.resultBundleName).xcresult") + try fileHandler.createFolder(resultBundle) + + given(analyticsArtifactUploadService) + .uploadResultBundle( + .value(resultBundle), + targetHashes: .any, + graphPath: .any, + commandEventId: .value(10), + serverURL: .value(Constants.URLs.production) + ) + .willReturn(()) + + // When + try await subject.send(commandEvent: event) + + // Then + XCTAssertStandardOutput(pattern: "You can view a detailed report at: https://cloud.tuist.io/tuist-org/tuist/runs/10") + XCTAssertFalse(fileHandler.exists(resultBundle)) + } +} diff --git a/Tests/TuistKitTests/Utils/TuistVersionLoaderTests.swift b/Tests/TuistKitTests/Utils/TuistVersionLoaderTests.swift new file mode 100644 index 00000000000..a6897eb426f --- /dev/null +++ b/Tests/TuistKitTests/Utils/TuistVersionLoaderTests.swift @@ -0,0 +1,48 @@ +import XCTest + +@testable import TuistKit +@testable import TuistSupportTesting + +final class TuistVersionLoaderTests: TuistUnitTestCase { + private var mockSystem: MockSystem! + + private var sut: TuistVersionLoader! + + override func setUp() { + super.setUp() + mockSystem = MockSystem() + sut = TuistVersionLoader(system: mockSystem) + } + + func test_getVersion_passesRightArguments() throws { + // given + let version = "4.0.1" + mockSystem.stubs = ["tuist version": (stderror: nil, stdout: version, exitstatus: 0)] + + // when + let result = try sut.getVersion() + + // then + XCTAssertTrue(mockSystem.called(["tuist", "version"])) + XCTAssertEqual(result, "4.0.1") + } + + func test_getVersion_removesNewLines() throws { + // given + let version = "4.0.1\n" + mockSystem.stubs = ["tuist version": (stderror: nil, stdout: version, exitstatus: 0)] + + // when + let result = try sut.getVersion() + + // then + XCTAssertTrue(mockSystem.called(["tuist", "version"])) + XCTAssertEqual(result, "4.0.1") + } + + override func tearDown() { + mockSystem = nil + sut = nil + super.tearDown() + } +} diff --git a/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift b/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift index 4cf9d026691..14041970c93 100644 --- a/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift +++ b/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistLoader @@ -19,7 +19,7 @@ final class ManifestLoaderTests: TuistTestCase { super.tearDown() } - func test_loadConfig() throws { + func test_loadConfig() async throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -35,10 +35,10 @@ final class ManifestLoaderTests: TuistTestCase { ) // When - _ = try subject.loadConfig(at: temporaryPath) + _ = try await subject.loadConfig(at: temporaryPath) } - func test_loadPlugin() throws { + func test_loadPlugin() async throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -50,10 +50,10 @@ final class ManifestLoaderTests: TuistTestCase { try content.write(to: manifestPath.url, atomically: true, encoding: .utf8) // When - _ = try subject.loadPlugin(at: temporaryPath) + _ = try await subject.loadPlugin(at: temporaryPath) } - func test_loadProject() throws { + func test_loadProject() async throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -69,13 +69,13 @@ final class ManifestLoaderTests: TuistTestCase { ) // When - let got = try subject.loadProject(at: temporaryPath) + let got = try await subject.loadProject(at: temporaryPath) // Then XCTAssertEqual(got.name, "tuist") } - func test_loadPackageSettings() throws { + func test_loadPackage() throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -91,6 +91,79 @@ final class ManifestLoaderTests: TuistTestCase { #endif + let package = Package( + name: "tuist", + products: [ + .executable(name: "tuist", targets: ["tuist"]), + ], + dependencies: [], + targets: [ + .target( + name: "tuist", + dependencies: [] + ), + ] + ) + + """ + + let manifestPath = temporaryPath.appending( + component: 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.loadPackage(at: manifestPath.parentDirectory) + + // Then + XCTAssertBetterEqual( + got, + .test( + name: "tuist", + products: [ + PackageInfo.Product(name: "tuist", type: .executable, targets: ["tuist"]), + ], + targets: [ + PackageInfo.Target( + name: "tuist", + path: nil, + url: nil, + sources: nil, + resources: [], + exclude: [], + dependencies: [], + publicHeadersPath: nil, + type: .regular, + settings: [], + checksum: nil, + packageAccess: true + ), + ] + ) + ) + } + + func test_loadPackageSettings() async throws { + // Given + let temporaryPath = try temporaryPath() + let content = """ + // swift-tools-version: 5.9 + import PackageDescription + + #if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + targetSettings: ["TargetA": ["OTHER_LDFLAGS": "-ObjC"]] + ) + + #endif + let package = Package( name: "PackageName", dependencies: [ @@ -101,10 +174,7 @@ final class ManifestLoaderTests: TuistTestCase { """ let manifestPath = temporaryPath.appending( - components: [ - Constants.tuistDirectoryName, - Manifest.package.fileName(temporaryPath), - ] + component: Manifest.package.fileName(temporaryPath) ) try FileHandler.shared.createFolder(temporaryPath.appending(component: Constants.tuistDirectoryName)) try content.write( @@ -114,16 +184,22 @@ final class ManifestLoaderTests: TuistTestCase { ) // When - let got = try subject.loadPackageSettings(at: temporaryPath) + let got = try await subject.loadPackageSettings(at: temporaryPath) // Then XCTAssertEqual( got, - .init(platforms: [.iOS, .watchOS]) + .init( + targetSettings: [ + "TargetA": [ + "OTHER_LDFLAGS": "-ObjC", + ], + ] + ) ) } - func test_loadPackageSettings_without_package_settings() throws { + func test_loadPackageSettings_without_package_settings() async throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -138,10 +214,7 @@ final class ManifestLoaderTests: TuistTestCase { """ let manifestPath = temporaryPath.appending( - components: [ - Constants.tuistDirectoryName, - Manifest.package.fileName(temporaryPath), - ] + component: Manifest.package.fileName(temporaryPath) ) try FileHandler.shared.createFolder(temporaryPath.appending(component: Constants.tuistDirectoryName)) try content.write( @@ -151,7 +224,7 @@ final class ManifestLoaderTests: TuistTestCase { ) // When - let got = try subject.loadPackageSettings(at: temporaryPath) + let got = try await subject.loadPackageSettings(at: temporaryPath) // Then XCTAssertEqual( @@ -160,7 +233,7 @@ final class ManifestLoaderTests: TuistTestCase { ) } - func test_loadWorkspace() throws { + func test_loadWorkspace() async throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -176,13 +249,13 @@ final class ManifestLoaderTests: TuistTestCase { ) // When - let got = try subject.loadWorkspace(at: temporaryPath) + let got = try await subject.loadWorkspace(at: temporaryPath) // Then XCTAssertEqual(got.name, "tuist") } - func test_loadTemplate() throws { + func test_loadTemplate() async throws { // Given let temporaryPath = try temporaryPath().appending(component: "folder") try fileHandler.createFolder(temporaryPath) @@ -203,13 +276,13 @@ final class ManifestLoaderTests: TuistTestCase { ) // When - let got = try subject.loadTemplate(at: temporaryPath) + let got = try await subject.loadTemplate(at: temporaryPath) // Then XCTAssertEqual(got.description, "Template description") } - func test_load_invalidFormat() throws { + func test_load_invalidFormat() async throws { // Given let temporaryPath = try temporaryPath() let content = """ @@ -225,18 +298,21 @@ final class ManifestLoaderTests: TuistTestCase { ) // When / Then - XCTAssertThrowsError( - try subject.loadProject(at: temporaryPath) - ) + var _error: Error? + do { + _ = try await subject.loadProject(at: temporaryPath) + } catch { + _error = error + } + XCTAssertNotNil(_error) } - func test_load_missingManifest() throws { + func test_load_missingManifest() async throws { let temporaryPath = try temporaryPath() - XCTAssertThrowsError( - try subject.loadProject(at: temporaryPath) - ) { error in - XCTAssertEqual(error as? ManifestLoaderError, ManifestLoaderError.manifestNotFound(.project, temporaryPath)) - } + await XCTAssertThrowsSpecific( + { try await self.subject.loadProject(at: temporaryPath) }, + ManifestLoaderError.manifestNotFound(.project, temporaryPath) + ) } func test_manifestsAt() throws { @@ -256,7 +332,7 @@ final class ManifestLoaderTests: TuistTestCase { XCTAssertTrue(got.contains(.config)) } - func test_manifestLoadError() throws { + func test_manifestLoadError() async throws { // Given let fileHandler = FileHandler() let temporaryPath = try temporaryPath() @@ -265,20 +341,101 @@ final class ManifestLoaderTests: TuistTestCase { let data = try fileHandler.readFile(configPath) // When - XCTAssertThrowsError( - try subject.loadConfig(at: temporaryPath) - ) { error in - XCTAssertEqual( - 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. - """ - ) + await XCTAssertThrowsSpecific( + { try await self.subject.loadConfig(at: temporaryPath) }, + 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. + """ ) - } + ) + } + + func test_validate_projectExists() throws { + // Given + let path = try temporaryPath().appending(component: "App") + try fileHandler.touch( + path.appending(component: "Project.swift") + ) + + // When / Then + try subject.validateHasRootManifest(at: path) + } + + func test_validate_workspaceExists() throws { + // Given + let path = try temporaryPath().appending(component: "App") + try fileHandler.touch( + path.appending(component: "Workspace.swift") + ) + + // When / Then + try subject.validateHasRootManifest(at: path) + } + + func test_validate_packageExists() throws { + // Given + let path = try temporaryPath().appending(component: "App") + try fileHandler.touch( + path.appending(component: "Package.swift") + ) + + // When / Then + try subject.validateHasRootManifest(at: path) + } + + func test_validate_manifestDoesNotExist() throws { + // Given + let path = try temporaryPath().appending(component: "App") + + // When / Then + XCTAssertThrowsSpecific( + try subject.validateHasRootManifest(at: path), + ManifestLoaderError.manifestNotFound(path) + ) + } + + func test_hasRootManifest_projectExists() throws { + // Given + let path = try temporaryPath().appending(component: "App") + try fileHandler.touch( + path.appending(component: "Project.swift") + ) + + // When / Then + XCTAssertTrue(subject.hasRootManifest(at: path)) + } + + func test_hasRootManifest_workspaceExists() throws { + // Given + let path = try temporaryPath().appending(component: "App") + try fileHandler.touch( + path.appending(component: "Workspace.swift") + ) + + // When / Then + XCTAssertTrue(subject.hasRootManifest(at: path)) + } + + func test_hasRootManifest_packageExists() throws { + // Given + let path = try temporaryPath().appending(component: "App") + try fileHandler.touch( + path.appending(component: "Package.swift") + ) + + // When / Then + XCTAssertTrue(subject.hasRootManifest(at: path)) + } + + func test_hasRootManifest_manifestDoesNotExist() throws { + // Given + let path = try temporaryPath().appending(component: "App") + + // When / Then + XCTAssertFalse(subject.hasRootManifest(at: path)) } } diff --git a/Tests/TuistLoaderIntegrationTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderIntegrationTests.swift b/Tests/TuistLoaderIntegrationTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderIntegrationTests.swift index 82c458ebca7..77462922842 100644 --- a/Tests/TuistLoaderIntegrationTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderIntegrationTests.swift +++ b/Tests/TuistLoaderIntegrationTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersBuilderIntegrationTests.swift @@ -1,7 +1,6 @@ import Foundation -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport import XCTest @@ -26,7 +25,7 @@ final class ProjectDescriptionHelpersBuilderIntegrationTests: TuistTestCase { super.tearDown() } - func test_build_when_the_helpers_is_a_dylib() throws { + func test_build_when_the_helpers_is_a_dylib() async throws { // Given let path = try temporaryPath() subject = ProjectDescriptionHelpersBuilder( @@ -46,8 +45,12 @@ final class ProjectDescriptionHelpersBuilderIntegrationTests: TuistTestCase { let searchPaths = ProjectDescriptionSearchPaths.paths(for: projectDescriptionPath) // When - let paths = try (0 ..< 3).map { _ in - try subject.build(at: path, projectDescriptionSearchPaths: searchPaths, projectDescriptionHelperPlugins: []) + let paths = try await Array(0 ..< 3).concurrentMap { _ in + try await self.subject.build( + at: path, + projectDescriptionSearchPaths: searchPaths, + projectDescriptionHelperPlugins: [] + ) } // Then @@ -59,7 +62,7 @@ final class ProjectDescriptionHelpersBuilderIntegrationTests: TuistTestCase { XCTAssertTrue(FileHandler.shared.exists(helpersModule.path)) } - func test_build_when_the_helpers_is_a_plugin() throws { + func test_build_when_the_helpers_is_a_plugin() async throws { // Given let path = try temporaryPath() subject = ProjectDescriptionHelpersBuilder(cacheDirectory: path, helpersDirectoryLocator: helpersDirectoryLocator) @@ -77,8 +80,12 @@ final class ProjectDescriptionHelpersBuilderIntegrationTests: TuistTestCase { let plugins = [ProjectDescriptionHelpersPlugin(name: "Plugin", path: helpersPluginPath, location: .local)] // When - let paths = try (0 ..< 3).map { _ in - try subject.build(at: path, projectDescriptionSearchPaths: searchPaths, projectDescriptionHelperPlugins: plugins) + let paths = try await Array(0 ..< 3).concurrentMap { _ in + try await self.subject.build( + at: path, + projectDescriptionSearchPaths: searchPaths, + projectDescriptionHelperPlugins: plugins + ) } // Then diff --git a/Tests/TuistLoaderTests/Extensions/TuistTestCase+ManifestMappers.swift b/Tests/TuistLoaderTests/Extensions/TuistTestCase+ManifestMappers.swift index 0ffaacb48e5..997e40d1b23 100644 --- a/Tests/TuistLoaderTests/Extensions/TuistTestCase+ManifestMappers.swift +++ b/Tests/TuistLoaderTests/Extensions/TuistTestCase+ManifestMappers.swift @@ -1,16 +1,16 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistLoader extension TuistTestCase { func XCTAssertSettingsMatchesManifest( - settings: TuistGraph.Settings, + settings: XcodeGraph.Settings, matches manifest: ProjectDescription.Settings, at path: AbsolutePath, generatorPaths: GeneratorPaths, @@ -34,7 +34,7 @@ extension TuistTestCase { } func XCTAssertTargetMatchesManifest( - target: TuistGraph.Target, + target: XcodeGraph.Target, matches manifest: ProjectDescription.Target, at path: AbsolutePath, generatorPaths: GeneratorPaths, @@ -85,7 +85,7 @@ extension TuistTestCase { } func XCTAssertBuildConfigurationMatchesManifest( - configuration: (TuistGraph.BuildConfiguration, TuistGraph.Configuration?), + configuration: (XcodeGraph.BuildConfiguration, XcodeGraph.Configuration?), matches manifest: ProjectDescription.Configuration, at _: AbsolutePath, generatorPaths: GeneratorPaths, @@ -108,7 +108,7 @@ extension TuistTestCase { } func assert( - coreDataModels: [TuistGraph.CoreDataModel], + coreDataModels: [XcodeGraph.CoreDataModel], matches manifests: [ProjectDescription.CoreDataModel], at path: AbsolutePath, generatorPaths: GeneratorPaths, @@ -127,7 +127,7 @@ extension TuistTestCase { } func coreDataModel( - _ coreDataModel: TuistGraph.CoreDataModel, + _ coreDataModel: XcodeGraph.CoreDataModel, matches manifest: ProjectDescription.CoreDataModel, at _: AbsolutePath, generatorPaths: GeneratorPaths @@ -137,7 +137,7 @@ extension TuistTestCase { } func assert( - scheme: TuistGraph.Scheme, + scheme: XcodeGraph.Scheme, matches manifest: ProjectDescription.Scheme, path: AbsolutePath, generatorPaths: GeneratorPaths, @@ -160,14 +160,14 @@ extension TuistTestCase { } func assert( - buildAction: TuistGraph.BuildAction, + buildAction: XcodeGraph.BuildAction, matches manifest: ProjectDescription.BuildAction, path _: AbsolutePath, generatorPaths: GeneratorPaths, file: StaticString = #file, line: UInt = #line ) throws { - let convertedTargets: [TuistGraph.TargetReference] = try manifest.targets.map { + let convertedTargets: [XcodeGraph.TargetReference] = try manifest.targets.map { let resolvedPath = try generatorPaths.resolveSchemeActionProjectPath($0.projectPath) return .init(projectPath: resolvedPath, name: $0.targetName) } @@ -175,7 +175,7 @@ extension TuistTestCase { } func assert( - testAction: TuistGraph.TestAction, + testAction: XcodeGraph.TestAction, matches manifest: ProjectDescription.TestAction, path _: AbsolutePath, generatorPaths: GeneratorPaths, @@ -192,7 +192,7 @@ extension TuistTestCase { } func assert( - runAction: TuistGraph.RunAction, + runAction: XcodeGraph.RunAction, matches manifest: ProjectDescription.RunAction, path _: AbsolutePath, generatorPaths: GeneratorPaths, @@ -213,7 +213,7 @@ extension TuistTestCase { } func assert( - arguments: TuistGraph.Arguments, + arguments: XcodeGraph.Arguments, matches manifest: ProjectDescription.Arguments, file: StaticString = #file, line: UInt = #line @@ -250,10 +250,10 @@ extension TuistTestCase { } private func == ( - _ lhs: TuistGraph.Platform, + _ lhs: XcodeGraph.Platform, _ rhs: ProjectDescription.Platform ) -> Bool { - let map: [TuistGraph.Platform: ProjectDescription.Platform] = [ + let map: [XcodeGraph.Platform: ProjectDescription.Platform] = [ .iOS: .iOS, .macOS: .macOS, .tvOS: .tvOS, @@ -262,17 +262,17 @@ private func == ( } private func == ( - _ lhs: TuistGraph.Destinations, + _ lhs: XcodeGraph.Destinations, _ rhs: ProjectDescription.Destinations ) -> Bool { lhs.map(\.rawValue).sorted() == rhs.map(\.rawValue).sorted() } private func == ( - _ lhs: TuistGraph.Product, + _ lhs: XcodeGraph.Product, _ rhs: ProjectDescription.Product ) -> Bool { - let map: [TuistGraph.Product: ProjectDescription.Product] = [ + let map: [XcodeGraph.Product: ProjectDescription.Product] = [ .app: .app, .framework: .framework, .staticFramework: .staticFramework, diff --git a/Tests/TuistLoaderTests/Linters/ManifestLinterTests.swift b/Tests/TuistLoaderTests/Linters/ManifestLinterTests.swift index 3a9eae92586..43b00ffa974 100644 --- a/Tests/TuistLoaderTests/Linters/ManifestLinterTests.swift +++ b/Tests/TuistLoaderTests/Linters/ManifestLinterTests.swift @@ -88,4 +88,221 @@ class ManifestLinterTests: XCTestCase { severity: .error ))) } + + func test_lint_workspace_scheme_missingProjectPathInBuildAction() { + // Given + + let buildAction = BuildAction.buildAction(targets: [.target("TargetA")]) + let scheme = Scheme.scheme(name: "MyScheme", buildAction: buildAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the buildAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInRunAction() { + // Given + let runAction = RunAction.runAction(expandVariableFromTarget: .target("TargetA")) + let scheme = Scheme.scheme(name: "MyScheme", runAction: runAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the runAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInProfileAction() { + // Given + let profileAction = ProfileAction.profileAction(executable: .target("TargetA")) + let scheme = Scheme.scheme(name: "MyScheme", profileAction: profileAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the profileAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInTestAction() { + // Given + let testAction = TestAction.test(targets: [.testableTarget(target: .target("TargetA"))]) + let scheme = Scheme.scheme(name: "MyScheme", testAction: testAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the testAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInBuildActionPreActions() { + // Given + let preActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let buildAction = BuildAction.buildAction(targets: [], preActions: preActions) + let scheme = Scheme.scheme(name: "MyScheme", buildAction: buildAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the buildAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInRunActionPreActions() { + // Given + let preActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let runAction = RunAction.runAction(preActions: preActions) + let scheme = Scheme.scheme(name: "MyScheme", runAction: runAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the runAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInProfileActionPreActions() { + // Given + let preActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let profileAction = ProfileAction.profileAction(preActions: preActions) + let scheme = Scheme.scheme(name: "MyScheme", profileAction: profileAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the profileAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInTestActionPreActions() { + // Given + let preActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let testAction = TestAction.targets([], preActions: preActions) + let scheme = Scheme.scheme(name: "MyScheme", testAction: testAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the testAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInRunActionPostActions() { + // Given + let postActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let runAction = RunAction.runAction(postActions: postActions) + let scheme = Scheme.scheme(name: "MyScheme", runAction: runAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the runAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInProfileActionPostActions() { + // Given + let postActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let profileAction = ProfileAction.profileAction(postActions: postActions) + let scheme = Scheme.scheme(name: "MyScheme", profileAction: profileAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the profileAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } + + func test_lint_workspace_scheme_missingProjectPathInTestActionPostActions() { + // Given + let postActions = [ExecutionAction.executionAction(scriptText: "", target: .target("TargetA"))] + let testAction = TestAction.targets([], preActions: postActions) + let scheme = Scheme.scheme(name: "MyScheme", testAction: testAction) + let workspace = Workspace.test(schemes: [scheme]) + + // When + let results = subject.lint(workspace: workspace) + + // Then + XCTAssertTrue(results.contains(LintingIssue( + reason: """ + Workspace.swift: The target 'TargetA' in the testAction of the scheme 'MyScheme' is missing the project path. + Please specify the project path using .project(path:, target:). + """, + severity: .error + ))) + } } diff --git a/Tests/TuistLoaderTests/Linters/Mocks/MockManifestLinter.swift b/Tests/TuistLoaderTests/Linters/Mocks/MockManifestLinter.swift index fe10ad0bcbc..a6a7163be25 100644 --- a/Tests/TuistLoaderTests/Linters/Mocks/MockManifestLinter.swift +++ b/Tests/TuistLoaderTests/Linters/Mocks/MockManifestLinter.swift @@ -6,7 +6,13 @@ import TuistSupport class MockManifestLinter: ManifestLinting { var stubLintProject: [LintingIssue] = [] + var stubLintWorkspace: [LintingIssue] = [] + func lint(project _: ProjectDescription.Project) -> [LintingIssue] { stubLintProject } + + func lint(workspace _: ProjectDescription.Workspace) -> [TuistCore.LintingIssue] { + stubLintWorkspace + } } diff --git a/Tests/TuistLoaderTests/Loaders/CachedManifestLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/CachedManifestLoaderTests.swift index 8221408407a..2b4a54e9aaf 100644 --- a/Tests/TuistLoaderTests/Loaders/CachedManifestLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/CachedManifestLoaderTests.swift @@ -1,7 +1,9 @@ import Foundation +import MockableTest +import Path import ProjectDescription -import TSCBasic -import struct TuistGraph.Plugins +import TuistCore +import struct TuistCore.Plugins import TuistSupport import XCTest @@ -12,15 +14,15 @@ import XCTest final class CachedManifestLoaderTests: TuistUnitTestCase { private var cacheDirectory: AbsolutePath! - private var manifestLoader = MockManifestLoader() + private var manifestLoader = MockManifestLoading() private var projectDescriptionHelpersHasher = MockProjectDescriptionHelpersHasher() private var helpersDirectoryLocator = MockHelpersDirectoryLocator() - private var cacheDirectoriesProvider: MockCacheDirectoriesProvider! - private var cacheDirectoriesProviderFactory: MockCacheDirectoriesProviderFactory! + private var cacheDirectoriesProvider: MockCacheDirectoriesProviding! + private var cacheDirectoriesProviderFactory: MockCacheDirectoriesProviderFactoring! private var workspaceManifests: [AbsolutePath: Workspace] = [:] private var projectManifests: [AbsolutePath: Project] = [:] - private var configManifests: [AbsolutePath: Config] = [:] - private var pluginManifests: [AbsolutePath: Plugin] = [:] + private var configManifests: [AbsolutePath: ProjectDescription.Config] = [:] + private var pluginManifests: [AbsolutePath: ProjectDescription.Plugin] = [:] private var recordedLoadWorkspaceCalls: Int = 0 private var recordedLoadProjectCalls: Int = 0 private var recordedLoadConfigCalls: Int = 0 @@ -32,47 +34,60 @@ final class CachedManifestLoaderTests: TuistUnitTestCase { super.setUp() do { - cacheDirectoriesProvider = try MockCacheDirectoriesProvider() + cacheDirectoriesProvider = .init() cacheDirectory = try temporaryPath().appending(components: "tuist", "Cache", "Manifests") - cacheDirectoriesProviderFactory = MockCacheDirectoriesProviderFactory(provider: cacheDirectoriesProvider) - cacheDirectoriesProvider.cacheDirectoryStub = cacheDirectory.parentDirectory + cacheDirectoriesProviderFactory = .init() + given(cacheDirectoriesProviderFactory) + .cacheDirectories() + .willReturn(cacheDirectoriesProvider) + given(cacheDirectoriesProvider) + .cacheDirectory(for: .value(.manifests)) + .willReturn(cacheDirectory) } catch { XCTFail("Failed to create temporary directory") } subject = createSubject() - manifestLoader.loadWorkspaceStub = { [unowned self] path in - guard let manifest = workspaceManifests[path] else { - throw ManifestLoaderError.manifestNotFound(.workspace, path) + given(manifestLoader) + .loadWorkspace(at: .any) + .willProduce { [unowned self] path in + guard let manifest = workspaceManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.workspace, path) + } + recordedLoadWorkspaceCalls += 1 + return manifest } - recordedLoadWorkspaceCalls += 1 - return manifest - } - manifestLoader.loadProjectStub = { [unowned self] path in - guard let manifest = projectManifests[path] else { - throw ManifestLoaderError.manifestNotFound(.project, path) + given(manifestLoader) + .loadProject(at: .any) + .willProduce { [unowned self] path in + guard let manifest = projectManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.project, path) + } + recordedLoadProjectCalls += 1 + return manifest } - recordedLoadProjectCalls += 1 - return manifest - } - manifestLoader.loadConfigStub = { [unowned self] path in - guard let manifest = configManifests[path] else { - throw ManifestLoaderError.manifestNotFound(.config, path) + given(manifestLoader) + .loadConfig(at: .any) + .willProduce { [unowned self] path in + guard let manifest = configManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.config, path) + } + recordedLoadConfigCalls += 1 + return manifest } - recordedLoadConfigCalls += 1 - return manifest - } - manifestLoader.loadPluginStub = { [unowned self] path in - guard let manifest = pluginManifests[path] else { - throw ManifestLoaderError.manifestNotFound(.plugin, path) + given(manifestLoader) + .loadPlugin(at: .any) + .willProduce { [unowned self] path in + guard let manifest = pluginManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.plugin, path) + } + recordedLoadPluginCalls += 1 + return manifest } - recordedLoadPluginCalls += 1 - return manifest - } } override func tearDown() { @@ -83,91 +98,94 @@ final class CachedManifestLoaderTests: TuistUnitTestCase { // MARK: - Tests - func test_load_manifestNotCached() throws { + func test_load_manifestNotCached() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) // When - let result = try subject.loadProject(at: path) + let result = try await subject.loadProject(at: path) // Then XCTAssertEqual(result, project) XCTAssertEqual(result.name, "App") } - func test_load_manifestCached() throws { + func test_load_manifestCached() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) // When - _ = try subject.loadProject(at: path) - _ = try subject.loadProject(at: path) - _ = try subject.loadProject(at: path) - let result = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) + let result = try await subject.loadProject(at: path) // Then XCTAssertEqual(result, project) XCTAssertEqual(recordedLoadProjectCalls, 1) } - func test_load_manifestHashChanged() throws { + func test_load_manifestHashChanged() async throws { // Given let path = try temporaryPath().appending(component: "App") let originalProject = Project.test(name: "Original") try stubProject(originalProject, at: path) - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When let modifiedProject = Project.test(name: "Modified") try stubProject(modifiedProject, at: path) - let result = try subject.loadProject(at: path) + let result = try await subject.loadProject(at: path) // Then XCTAssertEqual(result, modifiedProject) XCTAssertEqual(result.name, "Modified") } - func test_load_helpersHashChanged() throws { + func test_load_helpersHashChanged() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) try stubHelpers(withHash: "hash") - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When try stubHelpers(withHash: "updatedHash") subject = createSubject() // we need to re-create the subject as it internally caches hashes - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // Then XCTAssertEqual(recordedLoadProjectCalls, 2) } - func test_load_pluginsHashChanged() throws { + func test_load_pluginsHashChanged() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) + given(manifestLoader) + .register(plugins: .any) + .willReturn() try stubPlugins(withHash: "hash") - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When try stubPlugins(withHash: "updatedHash") subject = createSubject() // we need to re-create the subject as it internally caches hashes - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // Then XCTAssertEqual(recordedLoadProjectCalls, 2) } - func test_load_environmentVariablesRemainTheSame() throws { + func test_load_environmentVariablesRemainTheSame() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") @@ -175,87 +193,87 @@ final class CachedManifestLoaderTests: TuistUnitTestCase { environment.manifestLoadingVariables = ["NAME": "A"] // When - _ = try subject.loadProject(at: path) - _ = try subject.loadProject(at: path) - _ = try subject.loadProject(at: path) - let result = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) + let result = try await subject.loadProject(at: path) // Then XCTAssertEqual(result, project) XCTAssertEqual(recordedLoadProjectCalls, 1) } - func test_load_environmentVariablesChange() throws { + func test_load_environmentVariablesChange() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) environment.manifestLoadingVariables = ["NAME": "A"] - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When environment.manifestLoadingVariables = ["NAME": "B"] - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // Then XCTAssertEqual(recordedLoadProjectCalls, 2) } - func test_load_tuistVersionRemainsTheSame() throws { + func test_load_tuistVersionRemainsTheSame() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) subject = createSubject(tuistVersion: "1.0") - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When subject = createSubject(tuistVersion: "1.0") - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // Then XCTAssertEqual(recordedLoadProjectCalls, 1) } - func test_load_tuistVersionChanged() throws { + func test_load_tuistVersionChanged() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) subject = createSubject(tuistVersion: "1.0") - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When subject = createSubject(tuistVersion: "2.0") - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // Then XCTAssertEqual(recordedLoadProjectCalls, 2) } - func test_load_corruptedCache() throws { + func test_load_corruptedCache() async throws { // Given let path = try temporaryPath().appending(component: "App") let project = Project.test(name: "App") try stubProject(project, at: path) - _ = try subject.loadProject(at: path) + _ = try await subject.loadProject(at: path) // When try corruptFiles(at: cacheDirectory) - let result = try subject.loadProject(at: path) + let result = try await subject.loadProject(at: path) // Then XCTAssertEqual(result, project) XCTAssertEqual(recordedLoadProjectCalls, 2) } - func test_load_missingManifest() throws { + func test_load_missingManifest() async throws { // Given let path = try temporaryPath().appending(component: "App") // When / Then - XCTAssertThrowsSpecific( - try subject.loadProject(at: path), + await XCTAssertThrowsSpecific( + { try await self.subject.loadProject(at: path) }, ManifestLoaderError.manifestNotFound(.project, path) ) } @@ -263,32 +281,41 @@ final class CachedManifestLoaderTests: TuistUnitTestCase { func test_validate_projectExists() throws { // Given let path = try temporaryPath().appending(component: "App") + given(manifestLoader) + .manifests(at: .any) + .willReturn([.project]) + given(manifestLoader) + .validateHasRootManifest(at: .value(path)) + .willReturn() - // When - manifestLoader.manifestsAtStub = { _ in [.project] } - - // Then - try subject.validateHasProjectOrWorkspaceManifest(at: path) + // When / Then + try subject.validateHasRootManifest(at: path) } func test_validate_workspaceExists() throws { // Given let path = try temporaryPath().appending(component: "App") + given(manifestLoader) + .validateHasRootManifest(at: .value(path)) + .willReturn() + given(manifestLoader) + .manifests(at: .any) + .willReturn([.workspace]) - // When - manifestLoader.manifestsAtStub = { _ in [.workspace] } - - // Then - try subject.validateHasProjectOrWorkspaceManifest(at: path) + // When / Then + try subject.validateHasRootManifest(at: path) } func test_validate_manifestDoesNotExist() throws { // Given let path = try temporaryPath().appending(component: "App") + given(manifestLoader) + .validateHasRootManifest(at: .value(path)) + .willThrow(ManifestLoaderError.manifestNotFound(path)) // When / Then XCTAssertThrowsSpecific( - try subject.validateHasProjectOrWorkspaceManifest(at: path), + try subject.validateHasRootManifest(at: path), ManifestLoaderError.manifestNotFound(path) ) } @@ -330,7 +357,7 @@ final class CachedManifestLoaderTests: TuistUnitTestCase { } private func stub( - deprecatedManifest manifest: Config, + deprecatedManifest manifest: ProjectDescription.Config, at path: AbsolutePath ) throws { let manifestPath = path.appending(component: Manifest.config.fileName(path)) @@ -349,7 +376,7 @@ final class CachedManifestLoaderTests: TuistUnitTestCase { } private func stubPlugins(withHash hash: String) throws { - let plugin = Plugin(name: "TestPlugin") + let plugin = ProjectDescription.Plugin(name: "TestPlugin") let path = try temporaryPath().appending(component: "TestPlugin") let manifestPath = path.appending(component: Manifest.plugin.fileName(path)) try fileHandler.touch(manifestPath) diff --git a/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift index 6383a3f5bd1..1809e1b5eff 100644 --- a/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift @@ -1,9 +1,10 @@ import Foundation +import MockableTest +import Path import ProjectDescription -import TSCBasic -import TuistGraph -import TuistGraphTesting +import TuistCore import TuistSupport +import XcodeGraph import XCTest @testable import TuistCoreTesting @testable import TuistLoader @@ -11,14 +12,16 @@ import XCTest @testable import TuistSupportTesting final class ConfigLoaderTests: TuistUnitTestCase { - private var rootDirectoryLocator = MockRootDirectoryLocator() - private var manifestLoader = MockManifestLoader() + private var rootDirectoryLocator: MockRootDirectoryLocating! + private var manifestLoader: MockManifestLoading! private var subject: ConfigLoader! private var registeredPaths: [AbsolutePath: Bool] = [:] private var registeredConfigs: [AbsolutePath: Result] = [:] override func setUp() { super.setUp() + rootDirectoryLocator = .init() + manifestLoader = .init() subject = ConfigLoader( manifestLoader: manifestLoader, rootDirectoryLocator: rootDirectoryLocator, @@ -27,38 +30,41 @@ final class ConfigLoaderTests: TuistUnitTestCase { fileHandler.stubExists = { [weak self] path in self?.registeredPaths[path] == true } - manifestLoader.loadConfigStub = { [weak self] path in - guard let self, - let config = self.registeredConfigs[path] - else { - throw ManifestLoaderError.manifestNotFound(.config, path) + given(manifestLoader) + .loadConfig(at: .any) + .willProduce { [weak self] path in + guard let self, + let config = registeredConfigs[path] + else { + throw ManifestLoaderError.manifestNotFound(.config, path) + } + return try config.get() } - return try config.get() - } } override func tearDown() { subject = nil - manifestLoader.loadConfigStub = nil + manifestLoader = nil fileHandler.stubExists = nil super.tearDown() } // MARK: - Tests - func test_loadConfig_defaultReturnedWhenPathDoesNotExist() throws { + func test_loadConfig_defaultReturnedWhenPathDoesNotExist() async throws { // Given let path: AbsolutePath = "/some/random/path" stub(path: path, exists: false) + stub(rootDirectory: "/project") // When - let result = try subject.loadConfig(path: path) + let result = try await subject.loadConfig(path: path) // Then XCTAssertEqual(result, .default) } - func test_loadConfig_loadConfig() throws { + func test_loadConfig_loadConfig() async throws { // Given let path: AbsolutePath = "/project/Tuist/Config.swift" stub(path: path, exists: true) @@ -66,15 +72,16 @@ final class ConfigLoaderTests: TuistUnitTestCase { config: .test(), at: path.parentDirectory ) + stub(rootDirectory: "/project") // When - let result = try subject.loadConfig(path: path) + let result = try await subject.loadConfig(path: path) // Then - XCTAssertEqual(result, TuistGraph.Config( + XCTAssertEqual(result, TuistCore.Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, + fullHandle: nil, + url: Constants.URLs.production, swiftVersion: nil, plugins: [], generationOptions: .test(), @@ -82,17 +89,18 @@ final class ConfigLoaderTests: TuistUnitTestCase { )) } - func test_loadConfig_loadConfigError() throws { + func test_loadConfig_loadConfigError() async throws { // Given let path: AbsolutePath = "/project/Tuist/Config.swift" stub(path: path, exists: true) stub(configError: TestError.testError, at: "/project/Tuist") + stub(rootDirectory: "/project") // When / Then - XCTAssertThrowsSpecific(try subject.loadConfig(path: path), TestError.testError) + await XCTAssertThrowsSpecific({ try await self.subject.loadConfig(path: path) }, TestError.testError) } - func test_loadConfig_loadConfigInRootDirectory() throws { + func test_loadConfig_loadConfigInRootDirectory() async throws { // Given stub(rootDirectory: "/project") let paths: [AbsolutePath] = [ @@ -109,13 +117,69 @@ final class ConfigLoaderTests: TuistUnitTestCase { ) // When - let result = try subject.loadConfig(path: "/project/Module/A/") + let result = try await subject.loadConfig(path: "/project/Module/A/") + + // Then + XCTAssertEqual(result, TuistCore.Config( + compatibleXcodeVersions: .all, + fullHandle: nil, + url: Constants.URLs.production, + swiftVersion: nil, + plugins: [], + generationOptions: .test(), + path: "/project/Tuist/Config.swift" + )) + } + + func test_loadConfig_with_full_handle_and_url() async throws { + // Given + stub(rootDirectory: "/project") + stub(path: "/project/Tuist/Config.swift", exists: true) + stub( + config: .test( + fullHandle: "tuist/tuist", + url: "https://test.tuist.io" + ), + at: "/project/Tuist" + ) + + // When + let result = try await subject.loadConfig(path: "/project") + + // Then + XCTAssertBetterEqual(result, TuistCore.Config( + compatibleXcodeVersions: .all, + fullHandle: "tuist/tuist", + url: try XCTUnwrap(URL(string: "https://test.tuist.io")), + swiftVersion: nil, + plugins: [], + generationOptions: .test(), + path: "/project/Tuist/Config.swift" + )) + } + + func test_loadConfig_with_deprecated_cloud() async throws { + // Given + stub(rootDirectory: "/project") + stub(path: "/project/Tuist/Config.swift", exists: true) + stub( + config: ProjectDescription.Config( + cloud: .cloud( + projectId: "tuist/tuist", + url: "https://test.tuist.io" + ) + ), + at: "/project/Tuist" + ) + + // When + let result = try await subject.loadConfig(path: "/project") // Then - XCTAssertEqual(result, TuistGraph.Config( + XCTAssertBetterEqual(result, TuistCore.Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, + fullHandle: "tuist/tuist", + url: try XCTUnwrap(URL(string: "https://test.tuist.io")), swiftVersion: nil, plugins: [], generationOptions: .test(), @@ -138,7 +202,9 @@ final class ConfigLoaderTests: TuistUnitTestCase { } private func stub(rootDirectory: AbsolutePath) { - rootDirectoryLocator.locateStub = rootDirectory + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(rootDirectory) } private enum TestError: Error, Equatable { diff --git a/Tests/TuistLoaderTests/Loaders/DependenciesModelLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/DependenciesModelLoaderTests.swift deleted file mode 100644 index 42543fed8b7..00000000000 --- a/Tests/TuistLoaderTests/Loaders/DependenciesModelLoaderTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -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 DependenciesModelLoaderTests: TuistUnitTestCase { - private var manifestLoader: MockManifestLoader! - - private var subject: DependenciesModelLoader! - - override func setUp() { - super.setUp() - - manifestLoader = MockManifestLoader() - subject = DependenciesModelLoader(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.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.loadDependencies(at: temporaryPath, with: plugins) - - // Then - let expected: TuistGraph.Dependencies = .init( - carthage: .init( - [ - .github(path: "Dependency1", requirement: .exact("1.1.1")), - .git(path: "Dependency1", requirement: .exact("2.3.4")), - ] - ), - swiftPackageManager: .init( - .manifest, - 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/Loaders/ManifestLoaderErrorTests.swift b/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift index 60106ebd408..4d3b84e62d0 100644 --- a/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift +++ b/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistLoaderTests/Loaders/ManifestLoaderFactoryTests.swift b/Tests/TuistLoaderTests/Loaders/ManifestLoaderFactoryTests.swift index 08629ed4c86..8c4dc6ab93e 100644 --- a/Tests/TuistLoaderTests/Loaders/ManifestLoaderFactoryTests.swift +++ b/Tests/TuistLoaderTests/Loaders/ManifestLoaderFactoryTests.swift @@ -1,5 +1,6 @@ import Foundation -import TSCBasic +import Mockable +import Path import TuistSupport import XCTest @@ -7,6 +8,12 @@ import XCTest @testable import TuistSupportTesting final class ManifestLoaderFactoryTests: TuistUnitTestCase { + override func setUp() { + super.setUp() + + system.succeedCommand(["/usr/bin/xcrun", "swift", "-version"], output: "Swift Version 5.2.1") + } + func test_create_default_cached_manifest_loader() { // Given let sut = ManifestLoaderFactory() @@ -18,7 +25,7 @@ final class ManifestLoaderFactoryTests: TuistUnitTestCase { func test_create_non_cached_manifest_loader_when_explicitely_configured_via_enviromentvariable() { // Given - environment.tuistConfigVariables[Constants.EnvironmentVariables.cacheManifests] = "0" + environment.tuistVariables[Constants.EnvironmentVariables.cacheManifests] = "0" let sut = ManifestLoaderFactory() // When let result = sut.createManifestLoader() diff --git a/Tests/TuistLoaderTests/Loaders/ManifestModelConverterTests.swift b/Tests/TuistLoaderTests/Loaders/ManifestModelConverterTests.swift index 0b3e12ee5a9..8ef1e45f92a 100644 --- a/Tests/TuistLoaderTests/Loaders/ManifestModelConverterTests.swift +++ b/Tests/TuistLoaderTests/Loaders/ManifestModelConverterTests.swift @@ -1,5 +1,6 @@ import Foundation -import TSCBasic +import MockableTest +import Path import TuistCore import TuistSupport import XCTest @@ -86,13 +87,13 @@ class ManifestModelConverterTests: TuistUnitTestCase { // Then XCTAssertEqual(model.targets.count, 2) try XCTAssertTargetMatchesManifest( - target: model.targets[0], + target: try XCTUnwrap(model.targets["A"]), matches: targetA, at: temporaryPath, generatorPaths: generatorPaths ) try XCTAssertTargetMatchesManifest( - target: model.targets[1], + target: try XCTUnwrap(model.targets["B"]), matches: targetB, at: temporaryPath, generatorPaths: generatorPaths @@ -320,30 +321,36 @@ class ManifestModelConverterTests: TuistUnitTestCase { with projects: [AbsolutePath: ProjectDescription.Project], configs: [AbsolutePath: ProjectDescription.Config] = [:] ) -> ManifestLoading { - let manifestLoader = MockManifestLoader() - manifestLoader.loadProjectStub = { path in - guard let manifest = projects[path] else { - throw ManifestLoaderError.manifestNotFound(path) + let manifestLoader = MockManifestLoading() + given(manifestLoader) + .loadProject(at: .any) + .willProduce { path in + guard let manifest = projects[path] else { + throw ManifestLoaderError.manifestNotFound(path) + } + return manifest } - return manifest - } - manifestLoader.loadConfigStub = { path in - guard let manifest = configs[path] else { - throw ManifestLoaderError.manifestNotFound(path) + given(manifestLoader) + .loadConfig(at: .any) + .willProduce { path in + guard let manifest = configs[path] else { + throw ManifestLoaderError.manifestNotFound(path) + } + return manifest } - return manifest - } - manifestLoader.manifestsAtStub = { path in - var manifests = Set() - if projects[path] != nil { - manifests.insert(.project) + given(manifestLoader) + .manifests(at: .any) + .willProduce { path in + var manifests = Set() + if projects[path] != nil { + manifests.insert(.project) + } + + if configs[path] != nil { + manifests.insert(.config) + } + return manifests } - - if configs[path] != nil { - manifests.insert(.config) - } - return manifests - } return manifestLoader } @@ -351,16 +358,20 @@ class ManifestModelConverterTests: TuistUnitTestCase { with workspaces: [AbsolutePath: ProjectDescription.Workspace], projects: [AbsolutePath] = [] ) -> ManifestLoading { - let manifestLoader = MockManifestLoader() - manifestLoader.loadWorkspaceStub = { path in - guard let manifest = workspaces[path] else { - throw ManifestLoaderError.manifestNotFound(path) + let manifestLoader = MockManifestLoading() + given(manifestLoader) + .loadWorkspace(at: .any) + .willProduce { path in + guard let manifest = workspaces[path] else { + throw ManifestLoaderError.manifestNotFound(path) + } + return manifest + } + given(manifestLoader) + .manifests(at: .any) + .willProduce { path in + projects.contains(path) ? Set([.project]) : Set([]) } - return manifest - } - manifestLoader.manifestsAtStub = { path in - projects.contains(path) ? Set([.project]) : Set([]) - } return manifestLoader } diff --git a/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift index bb210855f1c..e784f12c226 100644 --- a/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift @@ -1,9 +1,11 @@ import Foundation -import TSCBasic +import MockableTest +import Path +import TSCUtility import TuistCore -import TuistGraph -import TuistGraphTesting +import TuistCoreTesting import TuistSupport +import XcodeGraph import XCTest @testable import ProjectDescription @@ -12,58 +14,75 @@ import XCTest @testable import TuistSupportTesting final class PackageSettingsLoaderTests: TuistUnitTestCase { - private var manifestLoader: MockManifestLoader! + private var manifestLoader: MockManifestLoading! + private var swiftPackageManagerController: MockSwiftPackageManagerController! + private var manifestFilesLocator: MockManifestFilesLocating! private var subject: PackageSettingsLoader! override func setUp() { super.setUp() - manifestLoader = MockManifestLoader() - subject = PackageSettingsLoader(manifestLoader: manifestLoader) + manifestLoader = .init() + swiftPackageManagerController = MockSwiftPackageManagerController() + manifestFilesLocator = MockManifestFilesLocating() + subject = PackageSettingsLoader( + manifestLoader: manifestLoader, + swiftPackageManagerController: swiftPackageManagerController, + fileHandler: fileHandler, + manifestFilesLocator: manifestFilesLocator + ) } override func tearDown() { subject = nil manifestLoader = nil + swiftPackageManagerController = nil super.tearDown() } - func test_loadDependencies() throws { + func test_loadPackageSettings() async throws { // Given let temporaryPath = try temporaryPath() let plugins = Plugins.test() + given(manifestFilesLocator) + .locatePackageManifest(at: .any) + .willReturn(temporaryPath) - 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] - ) + given(manifestLoader) + .register(plugins: .any) + .willReturn(()) + + given(manifestLoader) + .loadPackageSettings(at: .any) + .willReturn(.test()) + + swiftPackageManagerController.getToolsVersionStub = { _ in + TSCUtility.Version("5.4.9") } // When - let got = try subject.loadPackageSettings(at: temporaryPath, with: plugins) + let got = try await subject.loadPackageSettings(at: temporaryPath, with: plugins) // Then - let expected: TuistGraph.PackageSettings = .init( + let expected: TuistCore.PackageSettings = .init( productTypes: [:], - baseSettings: .init(configurations: [ - .debug: .init(settings: [:], xcconfig: nil), - .release: .init(settings: [:], xcconfig: nil), - ]), + productDestinations: [:], + baseSettings: XcodeGraph.Settings( + base: [:], + baseDebug: [:], + configurations: [ + .release: XcodeGraph.Configuration(settings: [:], xcconfig: nil), + .debug: XcodeGraph.Configuration(settings: [:], xcconfig: nil), + ], + defaultSettings: .recommended + ), targetSettings: [:], - platforms: [.iOS, .macOS] + swiftToolsVersion: Version(stringLiteral: "5.4.9") ) - XCTAssertEqual(manifestLoader.registerPluginsCount, 1) + verify(manifestLoader) + .register(plugins: .any) + .called(1) XCTAssertEqual(got, expected) } } diff --git a/Tests/TuistLoaderTests/Loaders/RecursiveManifestLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/RecursiveManifestLoaderTests.swift index 66c05fd46ce..fd7549d9db3 100644 --- a/Tests/TuistLoaderTests/Loaders/RecursiveManifestLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/RecursiveManifestLoaderTests.swift @@ -1,18 +1,20 @@ import Foundation +import MockableTest +import Path import ProjectDescription -import TSCBasic import TuistSupport import XCTest @testable import TuistLoader -@testable import TuistLoaderTesting @testable import TuistSupportTesting final class RecursiveManifestLoaderTests: TuistUnitTestCase { private var path: AbsolutePath! - private var manifestLoader: MockManifestLoader! + private var manifestLoader: MockManifestLoading! + private var packageInfoMapper: MockPackageInfoMapping! private var projectManifests: [AbsolutePath: Project] = [:] private var workspaceManifests: [AbsolutePath: Workspace] = [:] + private var packageManifests: [AbsolutePath: PackageInfo] = [:] private var subject: RecursiveManifestLoader! @@ -25,28 +27,31 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { } manifestLoader = createManifestLoader() + packageInfoMapper = MockPackageInfoMapping() subject = RecursiveManifestLoader( manifestLoader: manifestLoader, - fileHandler: fileHandler + fileHandler: fileHandler, + packageInfoMapper: packageInfoMapper ) } override func tearDown() { path = nil manifestLoader = nil + packageInfoMapper = nil subject = nil super.tearDown() } // MARK: - Tests - func test_loadProject_loadingSingleProject() throws { + func test_loadProject_loadingSingleProject() async throws { // Given let projectA = createProject(name: "ProjectA") try stub(manifest: projectA, at: try RelativePath(validating: "Some/Path/A")) // When - let manifests = try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) + let manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) // Then XCTAssertEqual(withRelativePaths(manifests.projects), [ @@ -54,7 +59,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ]) } - func test_loadProject_projectWithDependencies() throws { + func test_loadProject_projectWithDependencies() async throws { // Given let projectA = createProject( name: "ProjectA", @@ -82,7 +87,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { try stub(manifest: projectC, at: try RelativePath(validating: "Some/Path/C")) // When - let manifests = try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) + let manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) // Then XCTAssertEqual(withRelativePaths(manifests.projects), [ @@ -92,7 +97,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ]) } - func test_loadProject_projectWithTransitiveDependencies() throws { + func test_loadProject_projectWithTransitiveDependencies() async throws { // Given let projectA = createProject( name: "ProjectA", @@ -134,7 +139,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { try stub(manifest: projectE, at: try RelativePath(validating: "Some/Path/E")) // When - let manifests = try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) + let manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) // Then XCTAssertEqual(withRelativePaths(manifests.projects), [ @@ -146,7 +151,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ]) } - func test_loadProject_missingManifest() throws { + func test_loadProject_missingManifest() async throws { // Given let projectA = createProject( name: "ProjectA", @@ -159,13 +164,13 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { try stub(manifest: projectA, at: try RelativePath(validating: "Some/Path/A")) // When / Then - XCTAssertThrowsSpecific( - try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))), + await XCTAssertThrowsSpecific( + { try await self.subject.loadWorkspace(at: self.path.appending(try RelativePath(validating: "Some/Path/A"))) }, ManifestLoaderError.manifestNotFound(.project, path.appending(try RelativePath(validating: "Some/Path/B"))) ) } - func test_loadWorkspace() throws { + func test_loadWorkspace() async throws { // Given let workspace = Workspace.test( name: "Workspace", @@ -200,7 +205,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { try stub(manifest: workspace, at: try RelativePath(validating: "Some/Path")) // When - let manifests = try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path"))) + let manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path"))) // Then XCTAssertEqual(manifests.path, path.appending(try RelativePath(validating: "Some/Path"))) @@ -212,7 +217,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ]) } - func test_loadWorkspace_withGlobPattern() throws { + func test_loadWorkspace_withGlobPattern() async throws { // Given let workspace = Workspace.test( name: "Workspace", @@ -246,7 +251,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { try stub(manifest: workspace, at: try RelativePath(validating: "Some/Path")) // When - let manifests = try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path"))) + let manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path"))) // Then XCTAssertEqual(manifests.path, path.appending(try RelativePath(validating: "Some/Path"))) @@ -258,7 +263,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ]) } - func test_loadWorkspace_withSameProjectName() throws { + func test_loadWorkspace_withSameProjectName() async throws { // Given let workspace = Workspace.test( name: "MyWorkspace", @@ -266,7 +271,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ".", ], schemes: [ - Scheme(name: "CustomWorkspaceScheme"), + .scheme(name: "CustomWorkspaceScheme"), ] ) @@ -281,7 +286,7 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { try stub(manifest: workspace, at: try RelativePath(validating: "Some/Path")) // When - let manifests = try subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path"))) + let manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path"))) // Then XCTAssertEqual(manifests.path, path.appending(try RelativePath(validating: "Some/Path"))) @@ -291,6 +296,31 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { ]) } + func test_loadSPM_Package() async throws { + // Given + let packageA = createPackage(name: "PackageA") + try stub(manifest: packageA, at: try RelativePath(validating: "Some/Path/A")) + given(packageInfoMapper).map( + packageInfo: .value(packageA), + path: .any, + packageType: .any, + packageSettings: .any, + packageToProject: .any + ) + .willReturn( + .test(name: "PackageA") + ) + + // When + var manifests = try await subject.loadWorkspace(at: path.appending(try RelativePath(validating: "Some/Path/A"))) + manifests = try await subject.loadAndMergePackageProjects(in: manifests, packageSettings: .test()) + + // Then + XCTAssertEqual(withRelativePaths(manifests.projects), [ + "Some/Path/A": .test(name: "PackageA"), + ]) + } + // MARK: - Helpers private func createProject( @@ -303,6 +333,12 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { return .test(name: name, targets: targets) } + private func createPackage( + name: String + ) -> PackageInfo { + return .test(name: name) + } + private func withRelativePaths(_ projects: [AbsolutePath: Project]) -> [String: Project] { Dictionary(uniqueKeysWithValues: projects.map { ($0.key.relative(to: path).pathString, $0.value) @@ -345,32 +381,61 @@ final class RecursiveManifestLoaderTests: TuistUnitTestCase { workspaceManifests[manifestPath.parentDirectory] = manifest } - private func createManifestLoader() -> MockManifestLoader { - let manifestLoader = MockManifestLoader() - manifestLoader.loadProjectStub = { [unowned self] path in - guard let manifest = projectManifests[path] else { - throw ManifestLoaderError.manifestNotFound(.project, path) + private func stub( + manifest: PackageInfo, + at relativePath: RelativePath + ) throws { + let manifestPath = path + .appending(relativePath) + .appending(component: Manifest.package.fileName(path.appending(relativePath))) + try fileHandler.touch(manifestPath) + packageManifests[manifestPath.parentDirectory] = manifest + } + + private func createManifestLoader() -> MockManifestLoading { + let manifestLoader = MockManifestLoading() + given(manifestLoader) + .loadProject(at: .any) + .willProduce { [unowned self] path in + guard let manifest = projectManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.project, path) + } + return manifest } - return manifest - } - manifestLoader.loadWorkspaceStub = { [unowned self] path in - guard let manifest = workspaceManifests[path] else { - throw ManifestLoaderError.manifestNotFound(.workspace, path) + given(manifestLoader) + .loadWorkspace(at: .any) + .willProduce { [unowned self] path in + guard let manifest = workspaceManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.workspace, path) + } + return manifest } - return manifest - } - manifestLoader.manifestsAtStub = { [unowned self] path in - var manifests = Set() - if let _ = projectManifests[path] { - manifests.insert(.project) + given(manifestLoader) + .loadPackage(at: .any) + .willProduce { [unowned self] path in + guard let manifest = packageManifests[path] else { + throw ManifestLoaderError.manifestNotFound(.workspace, path) + } + return manifest } - if let _ = workspaceManifests[path] { - manifests.insert(.workspace) + + given(manifestLoader) + .manifests(at: .any) + .willProduce { [unowned self] path in + var manifests = Set() + if let _ = projectManifests[path] { + manifests.insert(.project) + } + if let _ = workspaceManifests[path] { + manifests.insert(.workspace) + } + if let _ = packageManifests[path] { + manifests.insert(.package) + } + return manifests } - return manifests - } return manifestLoader } } diff --git a/Tests/TuistLoaderTests/Loaders/SwiftPackageManagerDependenciesTests.swift b/Tests/TuistLoaderTests/Loaders/SwiftPackageManagerDependenciesTests.swift deleted file mode 100644 index bc1c4d371cc..00000000000 --- a/Tests/TuistLoaderTests/Loaders/SwiftPackageManagerDependenciesTests.swift +++ /dev/null @@ -1,146 +0,0 @@ -import Foundation -import XCTest -@testable import TuistGraph -@testable import TuistSupportTesting - -final class SwiftPackageManagerDependenciesTests: TuistUnitTestCase { - func test_manifestValue_singleDependency() throws { - // Given - let subject = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "url/url/url", requirement: .branch("branch")), - ]), - productTypes: [:], - baseSettings: .init(configurations: [:]), - targetSettings: [:], - projectOptions: [:] - ) - - // When - let got = subject.manifest(isLegacy: false, packageManifestFolder: "/") - - // Then - let expected = SwiftPackageManagerDependencies.Manifest.content(""" - import PackageDescription - - let package = Package( - name: "PackageName", - dependencies: [ - .package(url: "url/url/url", branch: "branch"), - ] - ) - """) - XCTAssertEqual(got, expected) - } - - func test_manifestValue_multipleDependencies() throws { - // Given - let subject = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "xyz", requirement: .exact("10.10.10")), - .remote(url: "foo/foo", requirement: .upToNextMinor("1.2.3")), - .remote(url: "bar/bar", requirement: .upToNextMajor("3.2.1")), - .remote(url: "http://xyz.com", requirement: .branch("develop")), - .remote(url: "https://www.google.com/", requirement: .revision("a083aa1435eb35d8a1cb369115a7636cb4b65135")), - .remote(url: "url/url/url", requirement: .range(from: "1.2.3", to: "5.2.1")), - .local(path: "/path/path/path"), - ]), - productTypes: [:], - baseSettings: .init(configurations: [:]), - targetSettings: [:], - projectOptions: [:] - ) - - // When - let got = subject.manifest(isLegacy: false, packageManifestFolder: "/path") - - // Then - let expected = SwiftPackageManagerDependencies.Manifest.content(""" - import PackageDescription - - let package = Package( - name: "PackageName", - dependencies: [ - .package(url: "xyz", exact: "10.10.10"), - .package(url: "foo/foo", .upToNextMinor(from: "1.2.3")), - .package(url: "bar/bar", from: "3.2.1"), - .package(url: "http://xyz.com", branch: "develop"), - .package(url: "https://www.google.com/", revision: "a083aa1435eb35d8a1cb369115a7636cb4b65135"), - .package(url: "url/url/url", "1.2.3" ..< "5.2.1"), - .package(path: "path/path"), - ] - ) - """) - XCTAssertEqual(got, expected) - } - - func test_legacyManifest_singleDependency() throws { - // Given - let subject = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "url/url/url", requirement: .branch("branch")), - ]), - productTypes: [:], - baseSettings: .init(configurations: [:]), - targetSettings: [:], - projectOptions: [:] - ) - - // When - let got = subject.manifest(isLegacy: true, packageManifestFolder: "/path") - - // Then - let expected = SwiftPackageManagerDependencies.Manifest.content(""" - import PackageDescription - - let package = Package( - name: "PackageName", - dependencies: [ - .package(url: "url/url/url", .branch("branch")), - ] - ) - """) - XCTAssertEqual(got, expected) - } - - func test_legacyManifest_multipleDependencies() throws { - // Given - let subject = SwiftPackageManagerDependencies( - .packages([ - .remote(url: "xyz", requirement: .exact("10.10.10")), - .remote(url: "foo/foo", requirement: .upToNextMinor("1.2.3")), - .remote(url: "bar/bar", requirement: .upToNextMajor("3.2.1")), - .remote(url: "http://xyz.com", requirement: .branch("develop")), - .remote(url: "https://www.google.com/", requirement: .revision("a083aa1435eb35d8a1cb369115a7636cb4b65135")), - .remote(url: "url/url/url", requirement: .range(from: "1.2.3", to: "5.2.1")), - .local(path: "/path/path/path"), - ]), - productTypes: [:], - baseSettings: .init(configurations: [:]), - targetSettings: [:], - projectOptions: [:] - ) - - // When - let got = subject.manifest(isLegacy: true, packageManifestFolder: "/path") - - // Then - let expected = SwiftPackageManagerDependencies.Manifest.content(""" - import PackageDescription - - let package = Package( - name: "PackageName", - dependencies: [ - .package(url: "xyz", .exact("10.10.10")), - .package(url: "foo/foo", .upToNextMinor(from: "1.2.3")), - .package(url: "bar/bar", .upToNextMajor(from: "3.2.1")), - .package(url: "http://xyz.com", .branch("develop")), - .package(url: "https://www.google.com/", .revision("a083aa1435eb35d8a1cb369115a7636cb4b65135")), - .package(url: "url/url/url", "1.2.3" ..< "5.2.1"), - .package(path: "path/path"), - ] - ) - """) - XCTAssertEqual(got, expected) - } -} diff --git a/Tests/TuistLoaderTests/Loaders/TemplateGitLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/TemplateGitLoaderTests.swift index 96abd353577..a54e03902c4 100644 --- a/Tests/TuistLoaderTests/Loaders/TemplateGitLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/TemplateGitLoaderTests.swift @@ -1,6 +1,5 @@ -import TSCBasic +import Path import TuistCore -import TuistGraph import TuistSupport import XCTest @@ -33,7 +32,7 @@ final class TemplateGitLoaderTests: TuistUnitTestCase { super.tearDown() } - func test_loadTemplatePath_isSameWithClonedRepository() throws { + func test_loadTemplatePath_isSameWithClonedRepository() async throws { // Given var clonedRepositoryPath: AbsolutePath? gitHandler.cloneToStub = { _, path in @@ -43,13 +42,13 @@ final class TemplateGitLoaderTests: TuistUnitTestCase { var pathToLoadTemplateFrom: AbsolutePath? templateLoader.loadTemplateStub = { path in pathToLoadTemplateFrom = path - return TuistGraph.Template( + return TuistCore.Template( description: "" ) } // When - try subject.loadTemplate(from: "https://url/to/repo.git", templateName: "MyTemplate", closure: { _ in }) + try await subject.loadTemplate(from: "https://url/to/repo.git", templateName: "MyTemplate", closure: { _ in }) // Then XCTAssertNotNil(pathToLoadTemplateFrom) diff --git a/Tests/TuistLoaderTests/Loaders/TemplateLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/TemplateLoaderTests.swift index 81065fb5f8f..e36fb4cf497 100644 --- a/Tests/TuistLoaderTests/Loaders/TemplateLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/TemplateLoaderTests.swift @@ -1,21 +1,20 @@ -import TSCBasic +import MockableTest +import Path import TuistCore -import TuistGraph import TuistSupport import XCTest @testable import ProjectDescription @testable import TuistLoader -@testable import TuistLoaderTesting @testable import TuistSupportTesting final class TemplateLoaderTests: TuistUnitTestCase { var subject: TemplateLoader! - var manifestLoader: MockManifestLoader! + var manifestLoader: MockManifestLoading! override func setUp() { super.setUp() - manifestLoader = MockManifestLoader() + manifestLoader = .init() subject = TemplateLoader(manifestLoader: manifestLoader) } @@ -25,38 +24,49 @@ final class TemplateLoaderTests: TuistUnitTestCase { super.tearDown() } - func test_loadTemplate_when_not_found() throws { + func test_loadTemplate_when_not_found() async throws { // Given let temporaryPath = try temporaryPath() - manifestLoader.loadTemplateStub = { path in - throw ManifestLoaderError.manifestNotFound(path) - } + given(manifestLoader) + .loadTemplate(at: .any) + .willProduce { path in + throw ManifestLoaderError.manifestNotFound(path) + } + given(manifestLoader) + .register(plugins: .any) + .willReturn(()) // Then - XCTAssertThrowsSpecific( - try subject.loadTemplate(at: temporaryPath), + await XCTAssertThrowsSpecific( + { try await self.subject.loadTemplate(at: temporaryPath, plugins: .none) }, ManifestLoaderError.manifestNotFound(temporaryPath) ) } - func test_loadTemplate_files() throws { + func test_loadTemplate_files() async throws { // Given let temporaryPath = try temporaryPath() - manifestLoader.loadTemplateStub = { _ in - ProjectDescription.Template( - description: "desc", - items: [ProjectDescription.Template.Item( - path: "generateOne", - contents: .file("fileOne") - )] + given(manifestLoader) + .loadTemplate(at: .any) + .willReturn( + ProjectDescription.Template( + description: "desc", + items: [ProjectDescription.Template.Item( + path: "generateOne", + contents: .file("fileOne") + )] + ) ) - } + + given(manifestLoader) + .register(plugins: .any) + .willReturn(()) // When - let got = try subject.loadTemplate(at: temporaryPath) + let got = try await subject.loadTemplate(at: temporaryPath, plugins: .none) // Then - XCTAssertEqual(got, TuistGraph.Template( + XCTAssertEqual(got, TuistCore.Template( description: "desc", items: [Template.Item( path: try RelativePath(validating: "generateOne"), diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/CodeCoverageMode+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/CodeCoverageMode+ManifestMapperTests.swift index eb41d5cc6da..a58410ffff9 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/CodeCoverageMode+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/CodeCoverageMode+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -19,7 +19,7 @@ final class CodeCoverageManifestMapperTests: TuistUnitTestCase { let manifest = Manifest.all // When - let got = try TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode + let got = try XcodeGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode .from(manifest: manifest, generatorPaths: generatorPaths) // Then @@ -33,7 +33,7 @@ final class CodeCoverageManifestMapperTests: TuistUnitTestCase { let manifest = Manifest.relevant // When - let got = try TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode + let got = try XcodeGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode .from(manifest: manifest, generatorPaths: generatorPaths) // Then @@ -44,11 +44,11 @@ final class CodeCoverageManifestMapperTests: TuistUnitTestCase { // Given let temporaryPath = try temporaryPath() let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) - let targetRef = ProjectDescription.TargetReference(projectPath: nil, target: "Target") + let targetRef = ProjectDescription.TargetReference.target("Target") let manifest = Manifest.targets([targetRef]) // When - let got = try TuistGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode + let got = try XcodeGraph.Workspace.GenerationOptions.AutogeneratedWorkspaceSchemes.CodeCoverageMode .from(manifest: manifest, generatorPaths: generatorPaths) // Then diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Configuration+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Configuration+ManifestMapperTests.swift index dfcf032294e..23729073b32 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Configuration+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Configuration+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -16,7 +16,7 @@ final class ConfigurationManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) // When - let got = try TuistGraph.Configuration.from(manifest: nil, generatorPaths: generatorPaths) + let got = try XcodeGraph.Configuration.from(manifest: nil, generatorPaths: generatorPaths) // Then XCTAssertNil(got) @@ -31,11 +31,11 @@ final class ConfigurationManifestMapperTests: TuistUnitTestCase { let manifest: ProjectDescription.Configuration = .debug( name: .debug, settings: settings, - xcconfig: Path(xcconfigPath.pathString) + xcconfig: .path(xcconfigPath.pathString) ) // When - let got = try TuistGraph.Configuration.from( + let got = try XcodeGraph.Configuration.from( manifest: manifest, generatorPaths: generatorPaths ) @@ -46,7 +46,7 @@ final class ConfigurationManifestMapperTests: TuistUnitTestCase { return } - guard case let TuistGraph.SettingValue.string(aString) = aSetting else { + guard case let XcodeGraph.SettingValue.string(aString) = aSetting else { XCTFail("Expected A to be a string") return } diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFileElement+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFileElement+ManifestMapperTests.swift new file mode 100644 index 00000000000..7d87151c4a8 --- /dev/null +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFileElement+ManifestMapperTests.swift @@ -0,0 +1,85 @@ +import Foundation +import Path +import ProjectDescription +import TuistCore +import TuistSupport +import XcodeGraph +import XCTest + +@testable import TuistLoader +@testable import TuistSupportTesting + +final class CopyFileElementManifestMapperTests: TuistUnitTestCase { + func test_from_outputs_a_warning_when_the_paths_point_to_directories() throws { + // Given + let temporaryPath = try temporaryPath() + let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) + try createFiles([ + "Documentation/README.md", + "Documentation/USAGE.md", + ]) + + let manifest = ProjectDescription.CopyFileElement.glob(pattern: "Documentation") + + // When + let model = try XcodeGraph.CopyFileElement.from( + manifest: manifest, + generatorPaths: generatorPaths, + includeFiles: { !FileHandler.shared.isFolder($0) } + ) + + // Then + let documentationPath = temporaryPath.appending(component: "Documentation").pathString + XCTAssertPrinterOutputContains( + "'\(documentationPath)' is a directory, try using: '\(documentationPath)/**' to list its files" + ) + XCTAssertEqual(model, []) + } + + func test_from_outputs_a_warning_when_the_folder_reference_is_invalid() throws { + // Given + let temporaryPath = try temporaryPath() + let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) + try createFiles([ + "README.md", + ]) + + let manifest = ProjectDescription.CopyFileElement.folderReference(path: "README.md") + + // When + let model = try XcodeGraph.CopyFileElement.from(manifest: manifest, generatorPaths: generatorPaths) + + // Then + XCTAssertPrinterOutputContains("README.md is not a directory - folder reference paths need to point to directories") + XCTAssertEqual(model, []) + } + + func test_copyFileElement_warning_withMissingFolderReference() throws { + // Given + let temporaryPath = try temporaryPath() + let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) + let manifest = ProjectDescription.CopyFileElement.folderReference(path: "Documentation") + + // When + let model = try XcodeGraph.CopyFileElement.from(manifest: manifest, generatorPaths: generatorPaths) + + // Then + XCTAssertPrinterOutputContains("Documentation does not exist") + XCTAssertEqual(model, []) + } + + func test_throws_when_the_glob_is_invalid() throws { + // Given + let temporaryPath = try temporaryPath() + let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) + let manifest = ProjectDescription.CopyFileElement.glob(pattern: "invalid/path/**/*") + let invalidGlob = InvalidGlob( + pattern: temporaryPath.appending(try RelativePath(validating: "invalid/path/**/*")).pathString, + nonExistentPath: temporaryPath.appending(try RelativePath(validating: "invalid/path/")) + ) + let error = GlobError.nonExistentDirectory(invalidGlob) + + // Then + XCTAssertThrowsSpecific(try XcodeGraph.CopyFileElement.from(manifest: manifest, generatorPaths: generatorPaths), error) + } +} diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFilesAction+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFilesAction+ManifestMapperTests.swift index 3f053248752..a061ac132d7 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFilesAction+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/CopyFilesAction+ManifestMapperTests.swift @@ -1,8 +1,8 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph +import XcodeGraph import XCTest @testable import TuistLoader @@ -24,11 +24,11 @@ final class CopyFilesManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.CopyFilesAction.resources( name: "Copy Fonts", subpath: "Fonts", - files: "Fonts/**" + files: ["Fonts/**"] ) // When - let model = try TuistGraph.CopyFilesAction.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.CopyFilesAction.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual(model.name, "Copy Fonts") @@ -57,11 +57,11 @@ final class CopyFilesManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.CopyFilesAction.sharedSupport( name: "Copy Templates", subpath: "Templates", - files: "SharedSupport/**" + files: ["SharedSupport/**"] ) // When - let model = try TuistGraph.CopyFilesAction.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.CopyFilesAction.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual(model.name, "Copy Templates") diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/CoreDataModel+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/CoreDataModel+ManifestMapperTests.swift index a5e136cc549..76ef8de1da5 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/CoreDataModel+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/CoreDataModel+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -15,13 +15,13 @@ final class CoreDataModelManifestMapperTests: TuistUnitTestCase { let temporaryPath = try temporaryPath() let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) try FileHandler.shared.touch(temporaryPath.appending(component: "model.xcdatamodeld")) - let manifest = ProjectDescription.CoreDataModel( + let manifest = ProjectDescription.CoreDataModel.coreDataModel( "model.xcdatamodeld", currentVersion: "1" ) // When - let model = try TuistGraph.CoreDataModel.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.CoreDataModel.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertTrue(try coreDataModel(model, matches: manifest, at: temporaryPath, generatorPaths: generatorPaths)) @@ -33,17 +33,20 @@ final class CoreDataModelManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) try FileManager.default.createDirectory( - at: temporaryPath.appending(component: "model.xcdatamodeld").asURL, + at: URL(fileURLWithPath: temporaryPath.appending(component: "model.xcdatamodeld").pathString), withIntermediateDirectories: false ) try createVersionFile(xcVersion: xcVersionDataString(), temporaryPath: temporaryPath) - let manifestWithoutCurrentVersion = ProjectDescription.CoreDataModel("model.xcdatamodeld") + let manifestWithoutCurrentVersion = ProjectDescription.CoreDataModel.coreDataModel("model.xcdatamodeld") // When - let model = try TuistGraph.CoreDataModel.from(manifest: manifestWithoutCurrentVersion, generatorPaths: generatorPaths) + let model = try XcodeGraph.CoreDataModel.from(manifest: manifestWithoutCurrentVersion, generatorPaths: generatorPaths) - let manifestWithCurrentVersionExplicitly = ProjectDescription.CoreDataModel("model.xcdatamodeld", currentVersion: "83") + let manifestWithCurrentVersionExplicitly = ProjectDescription.CoreDataModel.coreDataModel( + "model.xcdatamodeld", + currentVersion: "83" + ) // Then XCTAssertTrue(try coreDataModel( @@ -60,7 +63,7 @@ final class CoreDataModelManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) try FileManager.default.createDirectory( - at: temporaryPath.appending(component: "model.xcdatamodeld").asURL, + at: URL(fileURLWithPath: temporaryPath.appending(component: "model.xcdatamodeld").pathString), withIntermediateDirectories: false ) try createVersionFile( @@ -69,11 +72,11 @@ final class CoreDataModelManifestMapperTests: TuistUnitTestCase { ) // When - let manifestWithoutCurrentVersion = ProjectDescription.CoreDataModel("model.xcdatamodeld") + let manifestWithoutCurrentVersion = ProjectDescription.CoreDataModel.coreDataModel("model.xcdatamodeld") // Then XCTAssertThrowsError( - try TuistGraph.CoreDataModel.from(manifest: manifestWithoutCurrentVersion, generatorPaths: generatorPaths) + try XcodeGraph.CoreDataModel.from(manifest: manifestWithoutCurrentVersion, generatorPaths: generatorPaths) ) } @@ -83,14 +86,14 @@ final class CoreDataModelManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) // When - let manifestWithoutCurrentVersion = ProjectDescription.CoreDataModel("model.xcdatamodeld") + let manifestWithoutCurrentVersion = ProjectDescription.CoreDataModel.coreDataModel("model.xcdatamodeld") XCTAssertEqual( - try TuistGraph.CoreDataModel.from( + try XcodeGraph.CoreDataModel.from( manifest: manifestWithoutCurrentVersion, generatorPaths: generatorPaths ), - TuistGraph.CoreDataModel( + XcodeGraph.CoreDataModel( path: temporaryPath.appending(component: "model.xcdatamodeld"), versions: [], currentVersion: "model" @@ -102,7 +105,7 @@ final class CoreDataModelManifestMapperTests: TuistUnitTestCase { let urlToCurrentVersion = temporaryPath.appending(try RelativePath(validating: "model.xcdatamodeld")) .appending(component: ".xccurrentversion") let data = try XCTUnwrap(xcVersion.data(using: .utf8)) - try data.write(to: urlToCurrentVersion.asURL) + try data.write(to: URL(fileURLWithPath: urlToCurrentVersion.pathString)) } private func xcVersionDataString() -> String { diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/DependenciesManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/DependenciesManifestMapperTests.swift deleted file mode 100644 index 5e23406ad7e..00000000000 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/DependenciesManifestMapperTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import ProjectDescription -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport -import XCTest - -@testable import TuistLoader -@testable import TuistSupportTesting - -final class DependenciesManifestMapperTests: TuistUnitTestCase { - func test_from() throws { - // Given - let temporaryPath = try temporaryPath() - - let generatorPaths = GeneratorPaths(manifestDirectory: temporaryPath) - let manifest: ProjectDescription.Dependencies = Dependencies( - carthage: [ - .github(path: "Dependency1", requirement: .exact("1.1.1")), - .git(path: "Dependency.git", requirement: .branch("BranchName")), - .binary(path: "DependencyXYZ", requirement: .atLeast("2.3.1")), - ], - swiftPackageManager: .init(), - platforms: [.iOS, .macOS, .tvOS] - ) - - // When - let got = try TuistGraph.Dependencies.from(manifest: manifest, generatorPaths: generatorPaths) - - // Then - let expected: TuistGraph.Dependencies = .init( - carthage: .init( - [ - .github(path: "Dependency1", requirement: .exact("1.1.1")), - .git(path: "Dependency.git", requirement: .branch("BranchName")), - .binary(path: "DependencyXYZ", requirement: .atLeast("2.3.1")), - ] - ), - swiftPackageManager: .init( - .manifest, - productTypes: [:], - baseSettings: .init(configurations: [ - .debug: .init(settings: [:], xcconfig: nil), - .release: .init(settings: [:], xcconfig: nil), - ]), - targetSettings: [:] - ), - platforms: [.iOS, .macOS, .tvOS] - ) - XCTAssertEqual(got, expected) - } -} diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/DeploymentTargets+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/DeploymentTargets+ManifestMapperTests.swift deleted file mode 100644 index 45582d306ec..00000000000 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/DeploymentTargets+ManifestMapperTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistGraph -import TuistGraphTesting -import TuistSupport -import XCTest - -@testable import ProjectDescription -@testable import TuistLoader -@testable import TuistSupportTesting - -final class DeploymentTargetsManifestMapperTests: TuistUnitTestCase { - func test_deploymentTarget() throws { - // Given - let manifest: ProjectDescription.DeploymentTarget = .iOS(targetVersion: "13.1", devices: .iphone) - - // When - let got = ProjectDescription.DeploymentTargets.from(manifest: manifest) - - // Then - XCTAssertEqual(got[.iOS], "13.1") - XCTAssertNil(got[.macOS]) - XCTAssertNil(got[.watchOS]) - XCTAssertNil(got[.tvOS]) - } -} diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/FileElement+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/FileElement+ManifestMapperTests.swift index 7a8b7c177cc..5fe7a5ff363 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/FileElement+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/FileElement+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -22,7 +22,7 @@ final class FileElementManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.FileElement.glob(pattern: "Documentation") // When - let model = try TuistGraph.FileElement.from( + let model = try XcodeGraph.FileElement.from( manifest: manifest, generatorPaths: generatorPaths, includeFiles: { !FileHandler.shared.isFolder($0) } @@ -47,7 +47,7 @@ final class FileElementManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.FileElement.folderReference(path: "README.md") // When - let model = try TuistGraph.FileElement.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.FileElement.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertPrinterOutputContains("README.md is not a directory - folder reference paths need to point to directories") @@ -61,7 +61,7 @@ final class FileElementManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.FileElement.folderReference(path: "Documentation") // When - let model = try TuistGraph.FileElement.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.FileElement.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertPrinterOutputContains("Documentation does not exist") @@ -80,6 +80,6 @@ final class FileElementManifestMapperTests: TuistUnitTestCase { let error = GlobError.nonExistentDirectory(invalidGlob) // Then - XCTAssertThrowsSpecific(try TuistGraph.FileElement.from(manifest: manifest, generatorPaths: generatorPaths), error) + XCTAssertThrowsSpecific(try XcodeGraph.FileElement.from(manifest: manifest, generatorPaths: generatorPaths), error) } } diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Headers+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Headers+ManifestMapperTests.swift index 2f5fac56a86..2c4287ecd1b 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Headers+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Headers+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -38,7 +38,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -89,7 +89,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -132,7 +132,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -177,7 +177,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -221,7 +221,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -264,7 +264,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -320,7 +320,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -373,7 +373,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "ModuleA" @@ -413,7 +413,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { // In this header, you should import all the public headers of your framework using statements like #import #import - #import + #import // to test spaces prefix #import "A3.h" // to test modules with legacy format #import // to test modules, where some protected files became public #import // to test incorrect module @@ -436,7 +436,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "TuistTestModule" @@ -476,7 +476,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { // In this header, you should import all the public headers of your framework using statements like #import #import - #import + #import // to test spaces prefix #import "A3.h" // to test modules with legacy format """ let umbrellaPath = temporaryPath.appending(try RelativePath(validating: "Sources/Umbrella.h")) @@ -503,7 +503,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "TuistTestModule" @@ -543,7 +543,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { // In this header, you should import all the public headers of your framework using statements like #import #import - #import + #import // to test spaces prefix #import "A3.h" // to test modules with legacy format """ let umbrellaPath = temporaryPath.appending(try RelativePath(validating: "Sources/Umbrella.h")) @@ -570,7 +570,7 @@ final class HeadersManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Headers.from( + let model = try XcodeGraph.Headers.from( manifest: manifest, generatorPaths: generatorPaths, productName: "TuistTestModule" diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Platform+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Platform+ManifestMapperTests.swift index 049cd3ccb95..f2021a2a254 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Platform+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Platform+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -15,7 +15,7 @@ final class PlatformManifestMapperTests: TuistUnitTestCase { let manifest: ProjectDescription.Platform = .iOS // When - let model = try TuistGraph.Platform.from(manifest: manifest) + let model = try XcodeGraph.Platform.from(manifest: manifest) // Then XCTAssertEqual(model, .iOS) @@ -26,7 +26,7 @@ final class PlatformManifestMapperTests: TuistUnitTestCase { let manifest: ProjectDescription.Platform = .tvOS // When - let model = try TuistGraph.Platform.from(manifest: manifest) + let model = try XcodeGraph.Platform.from(manifest: manifest) // Then XCTAssertEqual(model, .tvOS) @@ -37,7 +37,7 @@ final class PlatformManifestMapperTests: TuistUnitTestCase { let manifest: ProjectDescription.Platform = .macOS // When - let model = try TuistGraph.Platform.from(manifest: manifest) + let model = try XcodeGraph.Platform.from(manifest: manifest) // Then XCTAssertEqual(model, .macOS) @@ -48,7 +48,7 @@ final class PlatformManifestMapperTests: TuistUnitTestCase { let manifest: ProjectDescription.Platform = .watchOS // When - let model = try TuistGraph.Platform.from(manifest: manifest) + let model = try XcodeGraph.Platform.from(manifest: manifest) // Then XCTAssertEqual(model, .watchOS) diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Project+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Project+ManifestMapperTests.swift index 5965a849c9e..20c29b22c27 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Project+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Project+ManifestMapperTests.swift @@ -1,14 +1,14 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore import TuistLoaderTesting import TuistSupport import XCTest -@testable import TuistGraph @testable import TuistLoader @testable import TuistSupportTesting +@testable import XcodeGraph final class ProjectManifestMapperTests: TuistUnitTestCase { func test_from() throws { @@ -16,6 +16,7 @@ final class ProjectManifestMapperTests: TuistUnitTestCase { let project = ProjectDescription.Project( name: "Name", organizationName: "Organization", + classPrefix: "ClassPrefix", options: .options( automaticSchemesOptions: .enabled( targetSchemesGrouping: .byNameSuffix(build: ["build"], test: ["test"], run: ["run"]), @@ -43,7 +44,7 @@ final class ProjectManifestMapperTests: TuistUnitTestCase { fileHandler.stubExists = { _ in true } // When - let got = try TuistGraph.Project.from( + let got = try XcodeGraph.Project.from( manifest: project, generatorPaths: .init(manifestDirectory: "/"), plugins: .none, @@ -55,12 +56,13 @@ final class ProjectManifestMapperTests: TuistUnitTestCase { // Then XCTAssertEqual( got, - TuistGraph.Project( + XcodeGraph.Project( path: "/", sourceRootPath: "/", xcodeProjPath: "/XcodeName.xcodeproj", name: "Name", organizationName: "Organization", + classPrefix: "ClassPrefix", defaultKnownRegions: ["en-US", "Base"], developmentRegion: "us", options: .init( diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceFileElementManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceFileElementManifestMapperTests.swift index fd005e3931b..0f7811a8bb4 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceFileElementManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceFileElementManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -22,7 +22,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.ResourceFileElement.glob(pattern: "Documentation") // When - let model = try TuistGraph.ResourceFileElement.from( + let model = try XcodeGraph.ResourceFileElement.from( manifest: manifest, generatorPaths: generatorPaths, includeFiles: { !FileHandler.shared.isFolder($0) } @@ -47,7 +47,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.ResourceFileElement.folderReference(path: "README.md") // When - let model = try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertPrinterOutputContains("README.md is not a directory - folder reference paths need to point to directories") @@ -61,7 +61,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { let manifest = ProjectDescription.ResourceFileElement.folderReference(path: "Documentation") // When - let model = try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertPrinterOutputContains("Documentation does not exist") @@ -81,7 +81,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { // Then XCTAssertThrowsSpecific( - try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), + try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), error ) } @@ -99,7 +99,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { // Then XCTAssertEqual( - try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), + try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), [ .file(path: resourcesFolder, tags: []), .file(path: includedResource, tags: []), @@ -121,7 +121,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { // Then XCTAssertEqual( - try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), + try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), [ .file(path: resourcesFolder, tags: []), .file(path: includedResource, tags: []), @@ -143,7 +143,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { // Then XCTAssertEqual( - try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), + try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), [ .file(path: resourcesFolder, tags: []), .file(path: includedResource, tags: []), @@ -165,7 +165,7 @@ final class ResourceFileElementManifestMapperTests: TuistUnitTestCase { // Then XCTAssertEqual( - try TuistGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), + try XcodeGraph.ResourceFileElement.from(manifest: manifest, generatorPaths: generatorPaths), [] ) } diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceSynthesizer+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceSynthesizer+ManifestMapperTests.swift index 9dcd7a00570..a4235b867ff 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceSynthesizer+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/ResourceSynthesizer+ManifestMapperTests.swift @@ -1,10 +1,10 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistLoaderTesting import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistLoader @@ -126,7 +126,7 @@ final class ResourceSynthesizerManifestMapperTests: TuistUnitTestCase { let manifestDirectory = try temporaryPath() var invokedPluginNames: [String] = [] var invokedResourceNames: [String] = [] - var invokedResourceSynthesizerPlugins: [PluginResourceSynthesizer] = [] + var invokedResourceSynthesizerPlugins: [TuistCore.PluginResourceSynthesizer] = [] resourceSynthesizerPathLocator.templatePathStub = { pluginName, resourceName, resourceSynthesizerPlugins in invokedPluginNames.append(pluginName) invokedResourceNames.append(resourceName) diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Scheme+ManifestMapper.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Scheme+ManifestMapper.swift index 92cdfbff7b1..ee61b4f33e7 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Scheme+ManifestMapper.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Scheme+ManifestMapper.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -20,7 +20,7 @@ final class SchemeManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: projectPath) // When - let model = try TuistGraph.Scheme.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.Scheme.from(manifest: manifest, generatorPaths: generatorPaths) // Then try assert(scheme: model, matches: manifest, path: projectPath, generatorPaths: generatorPaths) @@ -31,8 +31,8 @@ final class SchemeManifestMapperTests: TuistUnitTestCase { let arguments = ProjectDescription.Arguments.test( environment: ["FOO": "BAR", "FIZ": "BUZZ"], launchArguments: [ - LaunchArgument(name: "--help", isEnabled: true), - LaunchArgument(name: "subcommand", isEnabled: false), + .launchArgument(name: "--help", isEnabled: true), + .launchArgument(name: "subcommand", isEnabled: false), ] ) @@ -60,7 +60,7 @@ final class SchemeManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Scheme.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.Scheme.from(manifest: manifest, generatorPaths: generatorPaths) // Then try assert(scheme: model, matches: manifest, path: projectPath, generatorPaths: generatorPaths) diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Settings+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Settings+ManifestMapperTests.swift index ef1beb19482..b4efe1372e7 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Settings+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Settings+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -33,7 +33,7 @@ final class SettingsManifestMapperTests: TuistUnitTestCase { ) // When - let model = try TuistGraph.Settings.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.Settings.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertSettingsMatchesManifest(settings: model, matches: manifest, at: temporaryPath, generatorPaths: generatorPaths) diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Stencil+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Stencil+ManifestMapperTests.swift index 51856244641..0b050886a53 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Stencil+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Stencil+ManifestMapperTests.swift @@ -1,10 +1,10 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistLoaderTesting import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistLoader diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Target+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Target+ManifestMapperTests.swift index 8926ef0efd5..1e27923e274 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/Target+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Target+ManifestMapperTests.swift @@ -1,6 +1,6 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/TargetDependency+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/TargetDependency+ManifestMapperTests.swift index 6a814aad8de..11e44355996 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/TargetDependency+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/TargetDependency+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -16,7 +16,7 @@ final class DependencyManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) // When - let got = try TuistGraph.TargetDependency.from( + let got = try XcodeGraph.TargetDependency.from( manifest: dependency, generatorPaths: generatorPaths, externalDependencies: ["library": [.xcframework(path: "/path.xcframework", status: .required)]] @@ -38,7 +38,7 @@ final class DependencyManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) // When - let got = try TuistGraph.TargetDependency.from( + let got = try XcodeGraph.TargetDependency.from( manifest: dependency, generatorPaths: generatorPaths, externalDependencies: ["library": [.project(target: "Target", path: "/Project")]] @@ -60,7 +60,7 @@ final class DependencyManifestMapperTests: TuistUnitTestCase { let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) // When - let got = try TuistGraph.TargetDependency.from( + let got = try XcodeGraph.TargetDependency.from( manifest: dependency, generatorPaths: generatorPaths, externalDependencies: [ @@ -88,13 +88,79 @@ final class DependencyManifestMapperTests: TuistUnitTestCase { XCTAssertEqual(path, "/Project") } + func test_from_when_package_runtime() throws { + // Given + let dependency = ProjectDescription.TargetDependency.package(product: "RuntimePackageProduct", type: .runtime) + let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) + + // When + let got = try XcodeGraph.TargetDependency.from( + manifest: dependency, + generatorPaths: generatorPaths, + externalDependencies: [:] + ) + + // Then + XCTAssertEqual(got.count, 1) + guard case let .package(product, type, _) = got[0] else { + XCTFail("Dependency should be package") + return + } + XCTAssertEqual(product, "RuntimePackageProduct") + XCTAssertEqual(type, .runtime) + } + + func test_from_when_package_macro() throws { + // Given + let dependency = ProjectDescription.TargetDependency.package(product: "MacroPackageProduct", type: .macro) + let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) + + // When + let got = try XcodeGraph.TargetDependency.from( + manifest: dependency, + generatorPaths: generatorPaths, + externalDependencies: [:] + ) + + // Then + XCTAssertEqual(got.count, 1) + guard case let .package(product, type, _) = got[0] else { + XCTFail("Dependency should be package") + return + } + XCTAssertEqual(product, "MacroPackageProduct") + XCTAssertEqual(type, .macro) + } + + func test_from_when_package_plugin() throws { + // Given + let dependency = ProjectDescription.TargetDependency.package(product: "PluginPackageProduct", type: .plugin) + let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) + + // When + let got = try XcodeGraph.TargetDependency.from( + manifest: dependency, + generatorPaths: generatorPaths, + externalDependencies: [:] + ) + + // Then + XCTAssertEqual(got.count, 1) + guard case let .package(product, type, _) = got[0] else { + XCTFail("Dependency should be package") + return + } + XCTAssertEqual(product, "PluginPackageProduct") + XCTAssertEqual(type, .plugin) + } + func test_from_when_sdkLibrary() throws { // Given let dependency = ProjectDescription.TargetDependency.sdk(name: "c++", type: .library, status: .required) let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) // When - let got = try TuistGraph.TargetDependency.from( + let got = try XcodeGraph.TargetDependency.from( manifest: dependency, generatorPaths: generatorPaths, externalDependencies: [:] @@ -110,13 +176,35 @@ final class DependencyManifestMapperTests: TuistUnitTestCase { XCTAssertEqual(status, .required) } + func test_from_when_sdkSwiftLibrary() throws { + // Given + let dependency = ProjectDescription.TargetDependency.sdk(name: "Observation", type: .swiftLibrary, status: .required) + let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) + + // When + let got = try XcodeGraph.TargetDependency.from( + manifest: dependency, + generatorPaths: generatorPaths, + externalDependencies: [:] + ) + + // Then + XCTAssertEqual(got.count, 1) + guard case let .sdk(name, status, _) = got[0] else { + XCTFail("Dependency should be sdk") + return + } + XCTAssertEqual(name, "libswiftObservation.tbd") + XCTAssertEqual(status, .required) + } + func test_from_when_sdkFramework() throws { // Given let dependency = ProjectDescription.TargetDependency.sdk(name: "ARKit", type: .framework, status: .required) let generatorPaths = GeneratorPaths(manifestDirectory: try AbsolutePath(validating: "/")) // When - let got = try TuistGraph.TargetDependency.from( + let got = try XcodeGraph.TargetDependency.from( manifest: dependency, generatorPaths: generatorPaths, externalDependencies: [:] diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift index ec6612b506f..ce4751ed72a 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift @@ -1,9 +1,9 @@ import Foundation +import Path import ProjectDescription -import TSCBasic import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistLoader @@ -21,7 +21,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { arguments: ["arg1", "arg2"] ) // When - let model = try TuistGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual(model.name, "MyScript") @@ -57,7 +57,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { outputFileListPaths: ["$(SRCROOT)/foo/bar/**/*.swift"] ) // When - let model = try TuistGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) // Then let relativeSources = try model.inputPaths.map { try AbsolutePath(validating: $0).relative(to: temporaryPath).pathString } @@ -123,7 +123,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { outputFileListPaths: ["$(SRCROOT)/foo/bar/**/*.swift"] ) // When - let model = try TuistGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) + let model = try XcodeGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) // Then let relativeSources = try model.inputPaths.map { try AbsolutePath(validating: $0).relative(to: temporaryPath).pathString } diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/Workspace+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/Workspace+ManifestMapperTests.swift new file mode 100644 index 00000000000..c036b53e9ea --- /dev/null +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/Workspace+ManifestMapperTests.swift @@ -0,0 +1,70 @@ +import Foundation +import MockableTest +import TuistCore +import TuistLoader +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistLoader + +final class WorkspaceManifestMapperTests: TuistUnitTestCase { + private var manifestLoader: MockManifestLoading! + private var rootDirectoryLocator: MockRootDirectoryLocating! + + override func setUp() { + super.setUp() + + manifestLoader = .init() + rootDirectoryLocator = .init() + } + + override func tearDown() { + manifestLoader = nil + rootDirectoryLocator = nil + super.tearDown() + } + + func test_from_when_using_glob_for_projects() throws { + // Given + given(manifestLoader) + .manifests(at: .any) + .willReturn([.project]) + + let workspacePath = try temporaryPath() + + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(workspacePath) + + try fileHandler.createFolder(workspacePath.appending(components: ".build", "checkouts")) + + // When + let got = try XcodeGraph.Workspace.from( + manifest: .test( + projects: [ + "**", + ] + ), + path: workspacePath, + generatorPaths: .init( + manifestDirectory: workspacePath, + rootDirectoryLocator: rootDirectoryLocator + ), + manifestLoader: manifestLoader + ) + + // Then + XCTAssertBetterEqual( + got, + XcodeGraph.Workspace( + path: workspacePath, + xcWorkspacePath: workspacePath.appending(component: "Workspace.xcworkspace"), + name: "Workspace", + projects: [ + workspacePath, + ] + ) + ) + } +} diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestWrapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestWrapperTests.swift index a01be850999..bd7f9e1c4be 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestWrapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/WorkspaceGenerationOptions+ManifestWrapperTests.swift @@ -1,9 +1,9 @@ import Foundation import ProjectDescription import TuistCore -import TuistGraph import TuistSupport import TuistSupportTesting +import XcodeGraph import XCTest @testable import TuistLoader @@ -21,7 +21,7 @@ final class WorkspaceGenerationOptionsManifestMapperTests: TuistTestCase { ) // When - let actual = try TuistGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) + let actual = try XcodeGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual( @@ -45,7 +45,7 @@ final class WorkspaceGenerationOptionsManifestMapperTests: TuistTestCase { ) // When - let actual = try TuistGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) + let actual = try XcodeGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual( @@ -70,7 +70,7 @@ final class WorkspaceGenerationOptionsManifestMapperTests: TuistTestCase { ) // When - let actual = try TuistGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) + let actual = try XcodeGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual( @@ -97,7 +97,7 @@ final class WorkspaceGenerationOptionsManifestMapperTests: TuistTestCase { ) // When - let actual = try TuistGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) + let actual = try XcodeGraph.Workspace.GenerationOptions.from(manifest: manifest, generatorPaths: generatorPaths) // Then XCTAssertEqual( diff --git a/Tests/TuistLoaderTests/Models/GeneratorPathsTests.swift b/Tests/TuistLoaderTests/Models/GeneratorPathsTests.swift index d44fbbe8180..9321c17dc8e 100644 --- a/Tests/TuistLoaderTests/Models/GeneratorPathsTests.swift +++ b/Tests/TuistLoaderTests/Models/GeneratorPathsTests.swift @@ -1,4 +1,5 @@ -import TSCBasic +import MockableTest +import Path import TuistCore import TuistSupport import XCTest @@ -9,7 +10,7 @@ import XCTest @testable import TuistLoaderTesting @testable import TuistSupportTesting -class GeneratorPathsErrorTests: TuistUnitTestCase { +final class GeneratorPathsErrorTests: TuistUnitTestCase { func test_type_when_rootDirectoryNotFound() { // Given let path = try! AbsolutePath(validating: "/") @@ -35,13 +36,12 @@ class GeneratorPathsErrorTests: TuistUnitTestCase { class GeneratorPathsTests: TuistUnitTestCase { var subject: GeneratorPaths! var path: AbsolutePath! - var rootDirectoryLocator: MockRootDirectoryLocator! + var rootDirectoryLocator: MockRootDirectoryLocating! override func setUp() { super.setUp() path = try! temporaryPath() - rootDirectoryLocator = MockRootDirectoryLocator() - rootDirectoryLocator.locateStub = path.appending(component: "Root") + rootDirectoryLocator = .init() subject = GeneratorPaths( manifestDirectory: path, rootDirectoryLocator: rootDirectoryLocator @@ -62,6 +62,9 @@ class GeneratorPathsTests: TuistUnitTestCase { type: .relativeToCurrentFile, callerPath: path.pathString ) + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(path.appending(component: "Root")) // When let got = try subject.resolve(path: filePath) @@ -73,6 +76,9 @@ class GeneratorPathsTests: TuistUnitTestCase { func test_resolve_when_relative_to_manifest() throws { // Given let filePath = Path.relativeToManifest("file.swift") + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(path.appending(component: "Root")) // When let got = try subject.resolve(path: filePath) @@ -84,6 +90,9 @@ class GeneratorPathsTests: TuistUnitTestCase { func test_resolve_when_relative_to_root_directory() throws { // Given let filePath = Path.relativeToRoot("file.swift") + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(path.appending(component: "Root")) // When let got = try subject.resolve(path: filePath) @@ -95,7 +104,9 @@ class GeneratorPathsTests: TuistUnitTestCase { func test_resolve_throws_when_the_root_directory_cant_be_found() throws { // Given let filePath = Path.relativeToRoot("file.swift") - rootDirectoryLocator.locateStub = nil + given(rootDirectoryLocator) + .locate(from: .any) + .willReturn(nil) // When XCTAssertThrowsSpecific(try subject.resolve(path: filePath), GeneratorPathsError.rootDirectoryNotFound(path)) diff --git a/Tests/TuistLoaderTests/Models/ManifestTests.swift b/Tests/TuistLoaderTests/Models/ManifestTests.swift index e51c61189bc..60f45462378 100644 --- a/Tests/TuistLoaderTests/Models/ManifestTests.swift +++ b/Tests/TuistLoaderTests/Models/ManifestTests.swift @@ -11,7 +11,8 @@ final class ManifestTests: TuistUnitTestCase { XCTAssertEqual(Manifest.project.fileName(temporaryPath), "Project.swift") XCTAssertEqual(Manifest.workspace.fileName(temporaryPath), "Workspace.swift") XCTAssertEqual(Manifest.config.fileName(temporaryPath), "Config.swift") - XCTAssertEqual(Manifest.dependencies.fileName(temporaryPath), "Dependencies.swift") + XCTAssertEqual(Manifest.package.fileName(temporaryPath), "Package.swift") + XCTAssertEqual(Manifest.packageSettings.fileName(temporaryPath), "Package.swift") XCTAssertEqual(Manifest.plugin.fileName(temporaryPath), "Plugin.swift") XCTAssertEqual(Manifest.template.fileName(temporaryPath), "folder.swift") } diff --git a/Tests/TuistLoaderTests/ProjectDescriptionHelpers/Mocks/MockProjectDescriptionHelpersHasher.swift b/Tests/TuistLoaderTests/ProjectDescriptionHelpers/Mocks/MockProjectDescriptionHelpersHasher.swift index 07e14a8a329..db376f891c2 100644 --- a/Tests/TuistLoaderTests/ProjectDescriptionHelpers/Mocks/MockProjectDescriptionHelpersHasher.swift +++ b/Tests/TuistLoaderTests/ProjectDescriptionHelpers/Mocks/MockProjectDescriptionHelpersHasher.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path @testable import TuistLoader final class MockProjectDescriptionHelpersHasher: ProjectDescriptionHelpersHashing { diff --git a/Tests/TuistLoaderTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasherTests.swift b/Tests/TuistLoaderTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasherTests.swift index 2f65defa41d..e33f2783ffa 100644 --- a/Tests/TuistLoaderTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasherTests.swift +++ b/Tests/TuistLoaderTests/ProjectDescriptionHelpers/ProjectDescriptionHelpersHasherTests.swift @@ -1,5 +1,6 @@ +import MockableTest +import Path import ProjectDescription -import TSCBasic import TuistCore import TuistSupport import XCTest @@ -7,13 +8,24 @@ import XCTest @testable import TuistLoader @testable import TuistSupportTesting -class ProjectDescriptionHelpersHasherTests: TuistUnitTestCase { - var subject: ProjectDescriptionHelpersHasher! +final class ProjectDescriptionHelpersHasherTests: TuistUnitTestCase { + private var subject: ProjectDescriptionHelpersHasher! + private var machineEnvironment: MockMachineEnvironmentRetrieving! override func setUp() { super.setUp() - system.swiftVersionStub = { "5.2" } - subject = ProjectDescriptionHelpersHasher(tuistVersion: "3.2.1") + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.2") + machineEnvironment = .init() + given(machineEnvironment) + .macOSVersion + .willReturn("15.2.4") + subject = ProjectDescriptionHelpersHasher( + tuistVersion: "3.2.1", + machineEnvironment: machineEnvironment, + swiftVersionProvider: swiftVersionProvider + ) } override func tearDown() { @@ -31,7 +43,7 @@ class ProjectDescriptionHelpersHasherTests: TuistUnitTestCase { // Then for _ in 0 ..< 20 { let got = try subject.hash(helpersDirectory: temporaryDir) - XCTAssertEqual(got, "0a46768e766bdd05bdf901098d323b8a") + XCTAssertEqual(got, "5032b92c268cb7283c91ee37ec935c73") } } diff --git a/Tests/TuistLoaderTests/SwiftPackageManager/PackageInfoMapperTests.swift b/Tests/TuistLoaderTests/SwiftPackageManager/PackageInfoMapperTests.swift new file mode 100644 index 00000000000..8e37373cb62 --- /dev/null +++ b/Tests/TuistLoaderTests/SwiftPackageManager/PackageInfoMapperTests.swift @@ -0,0 +1,3944 @@ +import FileSystem +import MockableTest +import Path +import ProjectDescription +import TSCUtility +import TuistCore +import TuistSupport +import XcodeGraph +import XCTest + +@testable import TuistCoreTesting +@testable import TuistLoader +@testable import TuistSupportTesting + +final class PackageInfoMapperTests: TuistUnitTestCase { + private var subject: PackageInfoMapper! + private var fileSystem: FileSystem! + + override func setUp() { + super.setUp() + fileSystem = FileSystem() + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.9") + subject = PackageInfoMapper() + } + + override func tearDown() { + subject = nil + fileSystem = nil + super.tearDown() + } + + func testResolveDependencies_whenProductContainsBinaryTarget_mapsToXcframework() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Sources/Target_1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Sources/Target_2"))) + let resolvedDependencies = try subject.resolveExternalDependencies( + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target_1", "Target_2"]), + ], + targets: [ + .test(name: "Target_1", type: .binary, url: "https://binary.target.com"), + .test(name: "Target_2"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageToFolder: ["Package": basePath], + packageToTargetsToArtifactPaths: ["Package": [ + "Target_1": try! + .init(validating: "/artifacts/Package/Target_1.xcframework"), + ]] + ) + + XCTAssertEqual( + resolvedDependencies, + [ + "Product1": [ + .xcframework(path: "/artifacts/Package/Target_1.xcframework"), + .project(target: "Target_2", path: .relativeToManifest(basePath.pathString)), + ], + ] + ) + } + + func testResolveDependencies_whenProductContainsBinaryTargetMissingFrom_packageToTargetsToArtifactPaths() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Sources/Target_1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Sources/Target_2"))) + let resolvedDependencies = try subject.resolveExternalDependencies( + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target_1", "Target_2"]), + ], + targets: [ + .test(name: "Target_1", type: .binary, url: "https://binary.target.com"), + .test(name: "Target_2"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageToFolder: ["Package": basePath], + packageToTargetsToArtifactPaths: [:] + ) + + XCTAssertEqual( + resolvedDependencies, + [ + "Product1": [ + .xcframework(path: "\(basePath.pathString)/Target_1/Target_1.xcframework"), + .project(target: "Target_2", path: .relativeToManifest(basePath.pathString)), + ], + ] + ) + } + + func testResolveDependencies_whenPackageIDDifferentThanName() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_2"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target_2"))) + let resolvedDependencies = try subject.resolveExternalDependencies( + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), + ], + targets: [ + .test( + name: "Target_1", + dependencies: [ + .product( + name: "Product2", + package: "Package2_different_name", + moduleAliases: nil, + condition: nil + ), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + "Package2": .init( + name: "Package2", + products: [ + .init(name: "Product2", type: .library(.automatic), targets: ["Target_2"]), + ], + targets: [ + .test(name: "Target_2"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageToFolder: [ + "Package": basePath.appending(component: "Package"), + "Package2": basePath.appending(component: "Package2"), + ], + packageToTargetsToArtifactPaths: [:] + ) + + XCTAssertBetterEqual( + resolvedDependencies, + [ + "Product1": [ + .project( + target: "Target_1", + path: .path(basePath.appending(try RelativePath(validating: "Package")).pathString), + condition: nil + ), + ], + "Product2": [ + .project( + target: "Target_2", + path: .path(basePath.appending(try RelativePath(validating: "Package2")).pathString), + condition: nil + ), + ], + ] + ) + } + + func testResolveDependencies_whenDependencyNameContainsDot_mapsToUnderscoreInTargetName() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_1"))) + try fileHandler + .createFolder(basePath.appending(try RelativePath(validating: "com.example.dep-1/Sources/com.example.dep-1"))) + let resolvedDependencies = try subject.resolveExternalDependencies( + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), + ], + targets: [ + .test( + name: "Target_1", + dependencies: [ + .product( + name: "com.example.dep-1", + package: "com.example.dep-1", + moduleAliases: nil, + condition: nil + ), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + "com.example.dep-1": .init( + name: "com.example.dep-1", + products: [ + .init(name: "com.example.dep-1", type: .library(.automatic), targets: ["com.example.dep-1"]), + ], + targets: [ + .test(name: "com.example.dep-1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageToFolder: [ + "Package": basePath.appending(component: "Package"), + "com.example.dep-1": basePath.appending(component: "com.example.dep-1"), + ], + packageToTargetsToArtifactPaths: [:] + ) + + XCTAssertEqual( + resolvedDependencies, + [ + "com.example.dep-1": [ + .project( + target: "com_example_dep-1", + path: .path(basePath.appending(try RelativePath(validating: "com.example.dep-1")).pathString), + condition: nil + ), + ], + "Product1": [ + .project( + target: "Target_1", + path: .path(basePath.appending(try RelativePath(validating: "Package")).pathString), + condition: nil + ), + ], + ] + ) + } + + func testResolveDependencies_whenTargetDependenciesOnTargetHaveConditions() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target_1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency_1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency_2"))) + let resolvedDependencies = try subject.resolveExternalDependencies( + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product", type: .library(.automatic), targets: ["Target_1"]), + ], + targets: [ + .test( + name: "Target_1", + dependencies: [ + .byName(name: "Dependency_1", condition: .init(platformNames: ["ios"], config: nil)), + .target(name: "Dependency_2", condition: .init(platformNames: ["tvos"], config: nil)), + ] + ), + .test(name: "Dependency_1"), + .test(name: "Dependency_2"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageToFolder: [ + "Package": basePath.appending(component: "Package"), + ], + packageToTargetsToArtifactPaths: [:] + ) + + XCTAssertEqual( + resolvedDependencies, + [ + "Product": [ + .project( + target: "Target_1", + path: .path(basePath.appending(try RelativePath(validating: "Package")).pathString), + condition: nil + ), + ], + ] + ) + } + + func testResolveDependencies_whenTargetDependenciesOnProductHaveConditions() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package_1/Sources/Target_1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package_2/Sources/Target_2"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package_2/Sources/Target_3"))) + let resolvedDependencies = try subject.resolveExternalDependencies( + packageInfos: [ + "Package_1": .init( + name: "Package_1", + products: [ + .init(name: "Product_1", type: .library(.automatic), targets: ["Target_1"]), + ], + targets: [ + .test( + name: "Target_1", + dependencies: [ + .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) + ), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + "Package_2": .init( + name: "Package_2", + products: [ + .init(name: "Product_2", type: .library(.automatic), targets: ["Target_2"]), + .init(name: "Product_3", type: .library(.automatic), targets: ["Target_3"]), + ], + targets: [ + .test(name: "Target_2"), + .test(name: "Target_3"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageToFolder: [ + "Package_1": basePath.appending(component: "Package_1"), + "Package_2": basePath.appending(component: "Package_2"), + ], + packageToTargetsToArtifactPaths: [:] + ) + + XCTAssertBetterEqual( + resolvedDependencies, + [ + "Product_1": [ + .project( + target: "Target_1", + path: .path(basePath.appending(try RelativePath(validating: "Package_1")).pathString), + condition: nil + ), + ], + "Product_2": [ + .project( + target: "Target_2", + path: .path(basePath.appending(try RelativePath(validating: "Package_2")).pathString), + condition: nil + ), + ], + "Product_3": [ + .project( + target: "Target_3", + path: .path(basePath.appending(try RelativePath(validating: "Package_2")).pathString), + condition: nil + ), + ], + ] + ) + } + + func testMap() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenDynamicAndAutomaticLibraryType_mapsToStaticFramework() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + .init(name: "Product1Dynamic", type: .library(.dynamic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenLegacySwift_usesLegacyIOSVersion() throws { + // Reset is needed because `Mockable` was queueing the responses, the value in `setUp` would be emitted first and then + // this one. + swiftVersionProvider.reset() + given(swiftVersionProvider) + .swiftVersion() + .willReturn("5.6.0") + + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [PackageInfo.Platform(platformName: "iOS", version: "9.0", options: [])], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + deploymentTargets: .multiplatform( + iOS: "9.0", + macOS: "10.10", + watchOS: "2.0", + tvOS: "9.0", + visionOS: "1.0" + ) + ), + ] + ) + ) + } + + func testMap_whenMacCatalyst() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.init(platformName: "maccatalyst", version: "12.0", options: [])], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + destinations: Set(Destination.allCases), + deploymentTargets: .multiplatform( + iOS: "12.0", + macOS: "10.13", + watchOS: "4.0", + tvOS: "12.0", + visionOS: "1.0" + ) + ), + ] + ) + ) + } + + func testMap_whenAlternativeDefaultSources() async throws { + for alternativeDefaultSource in ["Source", "src", "srcs"] { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/\(alternativeDefaultSource)/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSources: .custom(.sourceFilesList( + globs: [ + basePath + .appending(try RelativePath(validating: "Package/\(alternativeDefaultSource)/Target1/**")) + .pathString, + ] + )) + ), + ] + ) + ) + + try await fileSystem.remove(sourcesPath) + } + } + + func testMap_whenOnlyBinaries_doesNotCreateProject() throws { + let project = try subject.map( + package: "Package", + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), + ], + targets: [ + .test(name: "Target_1", type: .binary, url: "https://binary.target.com"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertNil(project) + } + + func testMap_whenNameContainsUnderscores_mapsToDashInBundleID() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target_1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target_1"]), + ], + targets: [ + .test(name: "Target_1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target_1", basePath: basePath, customBundleID: "Target.1"), + ] + ) + ) + } + + func testMap_whenSettingsDefinesContainsQuotes() throws { + // When having a manifest that includes a GCC definition like `FOO="BAR"`, SPM successfully maintains the quotes + // and it will convert it to a compiler parameter like `-DFOO=\"BAR\"`. + // Xcode configuration, instead, treats the quotes as value assignment, resulting in `-DFOO=BAR`, + // which has a different meaning in GCC macros, building packages incorrectly. + // Tuist needs to escape those definitions for SPM manifests, as SPM is doing, so they can be built the same way. + + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .c, name: .define, condition: nil, value: ["FOO1=\"BAR1\""]), + .init(tool: .cxx, name: .define, condition: nil, value: ["FOO2=\"BAR2\""]), + .init(tool: .cxx, name: .define, condition: nil, value: ["FOO3=3"]), + .init( + tool: .cxx, + name: .define, + condition: PackageInfo.PackageConditionDescription( + platformNames: [], + config: "debug" + ), + value: ["FOO_DEBUG=1"] + ), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + baseSettings: .settings( + configurations: [ + .debug( + name: .debug, + settings: [ + "GCC_PREPROCESSOR_DEFINITIONS": [ + "$(inherited)", + "FOO_DEBUG=1", + ], + ] + ), + .release( + name: .release, + settings: [:] + ), + ] + ), + customSettings: [ + "GCC_PREPROCESSOR_DEFINITIONS": [ + // Escaped + "FOO1='\"BAR1\"'", + // Escaped + "FOO2='\"BAR2\"'", + // Not escaped + "FOO3=3", + ], + ] + ), + ] + ) + ) + } + + func testMap_whenNameContainsDot_mapsToUnderscoreInTargetName() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/com.example.target-1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "com.example.product-1", type: .library(.automatic), targets: ["com.example.target-1"]), + ], + targets: [ + .test(name: "com.example.target-1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "com_example_target-1", + basePath: basePath, + customProductName: "com_example_target_1", + customBundleID: "com.example.target-1", + customSources: .custom(.sourceFilesList(globs: [ + basePath + .appending(try RelativePath(validating: "Package/Sources/com.example.target-1/**")).pathString, + ])) + ), + ] + ) + ) + } + + func testMap_whenTargetNotInProduct_ignoresIt() throws { + let basePath = try temporaryPath() + let sourcesPath1 = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let sourcesPath2 = basePath.appending(try RelativePath(validating: "Package/Sources/Target2")) + try fileHandler.createFolder(sourcesPath1) + try fileHandler.createFolder(sourcesPath2) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + .test(name: "Target2"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenTargetIsNotRegular_ignoresTarget() throws { + let basePath = try temporaryPath() + let sourcesPath1 = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let sourcesPath2 = basePath.appending(try RelativePath(validating: "Package/Sources/Target2")) + let sourcesPath3 = basePath.appending(try RelativePath(validating: "Package/Sources/Target3")) + try fileHandler.createFolder(sourcesPath1) + try fileHandler.createFolder(sourcesPath2) + try fileHandler.createFolder(sourcesPath3) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1", "Target2", "Target3"]), + ], + targets: [ + .test(name: "Target1"), + .test(name: "Target2", type: .test), + .test(name: "Target3", type: .binary), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenProductIsNotLibrary_ignoresProduct() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target2"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target3"))) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + .init(name: "Product2", type: .plugin, targets: ["Target2"]), + .init(name: "Product3", type: .test, targets: ["Target3"]), + ], + targets: [ + .test(name: "Target1"), + .test(name: "Target2"), + .test(name: "Target3"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenCustomSources() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1", sources: ["Subfolder", "Another/Subfolder/file.swift"]), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSources: .custom([ + .init( + stringLiteral: + basePath.appending(try RelativePath(validating: "Package/Sources/Target1/Subfolder/**")) + .pathString + ), + .init( + stringLiteral: + basePath + .appending( + try RelativePath(validating: "Package/Sources/Target1/Another/Subfolder/file.swift") + ) + .pathString + ), + ]) + ), + ] + ) + ) + } + + func testMap_whenHasResources() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + // Create resources files and directories + let resource1 = sourcesPath.appending(try RelativePath(validating: "Resource/Folder")) + let resource2 = sourcesPath.appending(try RelativePath(validating: "Another/Resource/Folder")) + let resource3 = sourcesPath.appending(try RelativePath(validating: "AnotherOne/Resource/Folder")) + + try fileHandler.createFolder(resource1) + try fileHandler.createFolder(resource2) + try fileHandler.createFolder(resource3) + + // Project declaration + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + resources: [ + .init(rule: .copy, path: "Resource/Folder"), + .init(rule: .process, path: "Another/Resource/Folder"), + .init(rule: .process, path: "AnotherOne/Resource/Folder"), + .init(rule: .process, path: "AnotherOne/Resource/Folder/NonExisting"), + ], + exclude: [ + "AnotherOne/Resource", + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSources: .custom(.sourceFilesList(globs: [ + .glob( + .path(basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")).pathString), + excluding: [ + .path( + basePath + .appending( + try RelativePath(validating: "Package/Sources/Target1/AnotherOne/Resource/**") + ) + .pathString + ), + ] + ), + ])), + resources: [ + .folderReference( + path: .path( + basePath.appending(try RelativePath(validating: "Package/Sources/Target1/Resource/Folder")) + .pathString + ), + tags: [] + ), + .glob( + pattern: .path( + basePath + .appending( + try RelativePath(validating: "Package/Sources/Target1/Another/Resource/Folder/**") + ) + .pathString + ), + excluding: [ + .path( + basePath + .appending( + try RelativePath(validating: "Package/Sources/Target1/AnotherOne/Resource/**") + ) + .pathString + ), + ], + tags: [] + ), + ] + ), + ] + ) + ) + } + + func testMap_whenHasAlreadyIncludedDefaultResources() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let resourcesPath = sourcesPath.appending(try RelativePath(validating: "Resources")) + let defaultResourcePath = resourcesPath.appending(try RelativePath(validating: "file.xib")) + try fileHandler.createFolder(sourcesPath) + fileHandler.stubFiles = { _, excludedPaths, _, _ in + if excludedPaths == nil { + [defaultResourcePath] + } else { + [] + } + } + fileHandler.stubExists = { path in + [basePath, sourcesPath, resourcesPath, defaultResourcePath].contains(path) + } + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + resources: [ + .init(rule: .process, path: "Resources"), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + resources: [ + .glob( + pattern: .path(resourcesPath.appending(component: "**").pathString), + excluding: [], + tags: [] + ), + ] + ), + ] + ) + ) + } + + func testMap_whenHasDefaultResources() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let defaultResourcePath = sourcesPath.appending(try RelativePath(validating: "Resources/file.xib")) + try fileHandler.createFolder(sourcesPath) + fileHandler.stubFiles = { _, _, _, _ in + return [defaultResourcePath] + } + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + resources: [ + .glob( + pattern: .path(defaultResourcePath.pathString), + excluding: [], + tags: [] + ), + ] + ), + ] + ) + ) + } + + func testMap_whenHasHeadersWithCustomModuleMap() throws { + let basePath = try temporaryPath() + let headersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) + let moduleMapPath = headersPath.appending(component: "module.modulemap") + let topHeaderPath = headersPath.appending(component: "AnHeader.h") + let nestedHeaderPath = headersPath.appending(component: "Subfolder").appending(component: "AnotherHeader.h") + try fileHandler.createFolder(headersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: moduleMapPath, atomically: true) + try fileHandler.write("", path: topHeaderPath, atomically: true) + try fileHandler.write("", path: nestedHeaderPath, atomically: true) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1" + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: [ + "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/include"], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Target1"]), + ], + moduleMap: "$(SRCROOT)/Sources/Target1/include/module.modulemap" + ), + ] + ) + ) + } + + func testMap_whenHasSystemLibrary() throws { + let basePath = try temporaryPath() + let targetPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let moduleMapPath = targetPath.appending(component: "module.modulemap") + try fileHandler.createFolder(targetPath) + try fileHandler.write("", path: moduleMapPath, atomically: true) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + type: .system + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSources: .custom(nil), + customSettings: [ + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Target1"]), + ], + moduleMap: "$(SRCROOT)/Sources/Target1/module.modulemap" + ), + ] + ) + ) + } + + func testMap_errorWhenSystemLibraryHasMissingModuleMap() throws { + let basePath = try temporaryPath() + let targetPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let moduleMapPath = targetPath.appending(component: "module.modulemap") + try fileHandler.createFolder(targetPath) + + let error = PackageInfoMapperError.modulemapMissing( + moduleMapPath: moduleMapPath.pathString, + package: "Package", + target: "Target1" + ) + + XCTAssertThrowsSpecific(try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + type: .system + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ), error) + } + + func testMap_whenHasHeadersWithUmbrellaHeader() throws { + let basePath = try temporaryPath() + let headersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) + let topHeaderPath = headersPath.appending(component: "Target1.h") + let nestedHeaderPath = headersPath.appending(component: "Subfolder").appending(component: "AnHeader.h") + try fileHandler.createFolder(headersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: topHeaderPath, atomically: true) + try fileHandler.write("", path: nestedHeaderPath, atomically: true) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1" + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: [ + "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/include"], + "MODULEMAP_FILE": .string("$(SRCROOT)/Derived/Target1.modulemap"), + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Target1"]), + ] + ), + ] + ) + ) + } + + func testMap_whenDependenciesHaveHeaders() throws { + let basePath = try temporaryPath() + let target1HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) + let target1ModuleMapPath = target1HeadersPath.appending(component: "module.modulemap") + let dependency1HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1/include")) + let dependency1ModuleMapPath = dependency1HeadersPath.appending(component: "module.modulemap") + let dependency2HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency2/include")) + let dependency2ModuleMapPath = dependency2HeadersPath.appending(component: "module.modulemap") + try fileHandler.createFolder(target1HeadersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: target1ModuleMapPath, atomically: true) + try fileHandler.createFolder(dependency1HeadersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: dependency1ModuleMapPath, atomically: true) + try fileHandler.createFolder(dependency2HeadersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: dependency2ModuleMapPath, atomically: true) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.target(name: "Dependency1", condition: nil)] + ), + .test( + name: "Dependency1", + dependencies: [.target(name: "Dependency2", condition: nil)] + ), + .test(name: "Dependency2"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [.target(name: "Dependency1")], + customSettings: [ + "HEADER_SEARCH_PATHS": [ + "$(SRCROOT)/Sources/Target1/include", + ], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Target1"]), + ], + moduleMap: "$(SRCROOT)/Sources/Target1/include/module.modulemap" + ), + .test( + "Dependency1", + basePath: basePath, + dependencies: [.target(name: "Dependency2")], + customSettings: [ + "HEADER_SEARCH_PATHS": [ + "$(SRCROOT)/Sources/Dependency1/include", + ], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Dependency1"]), + ], + moduleMap: "$(SRCROOT)/Sources/Dependency1/include/module.modulemap" + ), + .test( + "Dependency2", + basePath: basePath, + customSettings: [ + "HEADER_SEARCH_PATHS": [ + "$(SRCROOT)/Sources/Dependency2/include", + ], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Dependency2"]), + ], + moduleMap: "$(SRCROOT)/Sources/Dependency2/include/module.modulemap" + ), + ] + ) + ) + } + + func testMap_whenExternalDependenciesHaveHeaders() throws { + let basePath = try temporaryPath() + let target1HeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1/include")) + let target1ModuleMapPath = target1HeadersPath.appending(component: "module.modulemap") + let dependency1HeadersPath = basePath.appending(try RelativePath(validating: "Package2/Sources/Dependency1/include")) + let dependency1ModuleMapPath = dependency1HeadersPath.appending(component: "module.modulemap") + let dependency2HeadersPath = basePath.appending(try RelativePath(validating: "Package3/Sources/Dependency2/include")) + let dependency2ModuleMapPath = dependency2HeadersPath.appending(component: "module.modulemap") + try fileHandler.createFolder(target1HeadersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: target1ModuleMapPath, atomically: true) + try fileHandler.createFolder(dependency1HeadersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: dependency1ModuleMapPath, atomically: true) + try fileHandler.createFolder(dependency2HeadersPath.appending(component: "Subfolder")) + try fileHandler.write("", path: dependency2ModuleMapPath, atomically: true) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.product(name: "Dependency1", package: "Package2", moduleAliases: nil, condition: nil)] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + "Package2": .init( + name: "Package2", + products: [ + .init(name: "Dependency1", type: .library(.automatic), targets: ["Dependency1"]), + ], + targets: [ + .test( + name: "Dependency1", + dependencies: [.product(name: "Dependency2", package: "Package3", moduleAliases: nil, condition: nil)] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + "Package3": .init( + name: "Package3", + products: [ + .init(name: "Dependency2", type: .library(.automatic), targets: ["Dependency2"]), + ], + targets: [ + .test( + name: "Dependency2" + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [.external(name: "Dependency1", condition: nil)], + customSettings: [ + "HEADER_SEARCH_PATHS": [ + "$(SRCROOT)/Sources/Target1/include", + ], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Target1"]), + ], + moduleMap: "$(SRCROOT)/Sources/Target1/include/module.modulemap" + ), + ] + ) + ) + } + + func testMap_whenCustomPath() throws { + let basePath = try temporaryPath() + + // Create resources files and directories + let headersPath = basePath.appending(try RelativePath(validating: "Package/Custom/Headers")) + let headerPath = headersPath.appending(component: "module.h") + let moduleMapPath = headersPath.appending(component: "module.modulemap") + try fileHandler.createFolder(headersPath) + try fileHandler.write("", path: headerPath, atomically: true) + try fileHandler.write("", path: moduleMapPath, atomically: true) + + let resourceFolderPathCustomTarget = basePath.appending(try RelativePath(validating: "Package/Custom/Resource/Folder")) + try fileHandler.createFolder(resourceFolderPathCustomTarget) + + // Project declaration + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + path: "Custom", + sources: ["Sources/Folder"], + resources: [.init(rule: .copy, path: "Resource/Folder")], + publicHeadersPath: "Headers" + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSources: .custom(.sourceFilesList(globs: [ + basePath + .appending(try RelativePath(validating: "Package/Custom/Sources/Folder/**")).pathString, + ])), + resources: [ + .folderReference( + path: .path( + basePath.appending(try RelativePath(validating: "Package/Custom/Resource/Folder")) + .pathString + ), + tags: [] + ), + ], + customSettings: [ + "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Custom/Headers"], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Target1"]), + ], + moduleMap: "$(SRCROOT)/Custom/Headers/module.modulemap" + ), + ] + ) + ) + } + + func testMap_whenDependencyHasHeaders_addsThemToHeaderSearchPath() throws { + let basePath = try temporaryPath() + let dependencyHeadersPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1/include")) + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(dependencyHeadersPath) + try fileHandler.touch(dependencyHeadersPath.appending(component: "Header.h")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.target(name: "Dependency1", condition: nil)] + ), + .test(name: "Dependency1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [.target(name: "Dependency1")] + ), + .test( + "Dependency1", + basePath: basePath, + headers: .headers( + public: .list( + [.glob(.path("\(dependencyHeadersPath.pathString)/*.h"))] + ) + ), + customSettings: [ + "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Dependency1/include"], + "DEFINES_MODULE": "NO", + "OTHER_CFLAGS": .array(["-fmodule-name=Dependency1"]), + ], + moduleMap: "$(SRCROOT)/Derived/Dependency1.modulemap" + ), + ] + ) + ) + } + + func testMap_whenMultipleAvailable_takesMultiple() throws { + // Given + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + // When + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios, .tvos], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + // Then + let expected: ProjectDescription.Project = .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customProductName: "Target1", + customBundleID: "Target1", + customSources: .custom(.sourceFilesList(globs: [ + basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")) + .pathString, + ])) + ), + ] + ) + + XCTAssertEqual(project?.name, expected.name) + + // Need to sort targets of projects, because internally a set is used to generate targets for different platforms + // That could lead to mixed orders + let projectTargets = project?.targets.sorted(by: \.name) + let expectedTargets = expected.targets.sorted(by: \.name) + XCTAssertBetterEqual(projectTargets, expectedTargets) + } + + func testMap_whenPackageDefinesPlatform_configuresDeploymentTarget() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.init(platformName: "ios", version: "13.0", options: [])], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + let other = ProjectDescription.Project.testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + deploymentTargets: .multiplatform( + iOS: "13.0", + macOS: "10.13", + watchOS: "4.0", + tvOS: "12.0", + visionOS: "1.0" + ) + ), + ] + ) + + dump(project?.targets.first?.destinations) + dump(other.targets.first?.destinations) + + XCTAssertBetterEqual( + project, + other + ) + } + + func testMap_whenSettingsContainsCHeaderSearchPath_mapsToHeaderSearchPathsSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .c, name: .headerSearchPath, condition: nil, value: ["value"]), + .init( + tool: .c, + name: .headerSearchPath, + condition: nil, + value: ["White Space Folder/value"] + ), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/value", + "\"$(SRCROOT)/Sources/Target1/White Space Folder/value\""]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsCXXHeaderSearchPath_mapsToHeaderSearchPathsSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [.init(tool: .cxx, name: .headerSearchPath, condition: nil, value: ["value"])] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/value"]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsCDefine_mapsToGccPreprocessorDefinitions() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .c, name: .define, condition: nil, value: ["key1"]), + .init(tool: .c, name: .define, condition: nil, value: ["key2=value"]), + .init(tool: .c, name: .define, condition: nil, value: ["key3="]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["GCC_PREPROCESSOR_DEFINITIONS": ["key1=1", "key2=value", "key3="]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsCXXDefine_mapsToGccPreprocessorDefinitions() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .cxx, name: .define, condition: nil, value: ["key1"]), + .init(tool: .cxx, name: .define, condition: nil, value: ["key2=value"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["GCC_PREPROCESSOR_DEFINITIONS": ["key1=1", "key2=value"]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsSwiftDefine_mapsToSwiftActiveCompilationConditions() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .swift, name: .define, condition: nil, value: ["key"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath, customSettings: ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "key"]), + ] + ) + ) + } + + func testMap_whenSettingsContainsCUnsafeFlags_mapsToOtherCFlags() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .c, name: .unsafeFlags, condition: nil, value: ["key1"]), + .init(tool: .c, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath, customSettings: ["OTHER_CFLAGS": ["key1", "key2", "key3"]]), + ] + ) + ) + } + + func testMap_whenSettingsContainsCXXUnsafeFlags_mapsToOtherCPlusPlusFlags() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .cxx, name: .unsafeFlags, condition: nil, value: ["key1"]), + .init(tool: .cxx, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath, customSettings: ["OTHER_CPLUSPLUSFLAGS": ["key1", "key2", "key3"]]), + ] + ) + ) + } + + func testMap_whenSettingsContainsSwiftUnsafeFlags_mapsToOtherSwiftFlags() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .swift, name: .unsafeFlags, condition: nil, value: ["key1"]), + .init(tool: .swift, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["OTHER_SWIFT_FLAGS": ["$(inherited)", "key1", "key2", "key3"]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsEnableUpcomingFeature_mapsToOtherSwiftFlags() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .swift, name: .enableUpcomingFeature, condition: nil, value: ["Foo"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["OTHER_SWIFT_FLAGS": ["$(inherited)", "-enable-upcoming-feature \"Foo\""]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsEnableExperimentalFeature_mapsToOtherSwiftFlags() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .swift, name: .enableExperimentalFeature, condition: nil, value: ["Foo"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: ["OTHER_SWIFT_FLAGS": ["$(inherited)", "-enable-experimental-feature \"Foo\""]] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsLinkerUnsafeFlags_mapsToOtherLdFlags() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key1"]), + .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath, customSettings: ["OTHER_LDFLAGS": ["key1", "key2", "key3"]]), + ] + ) + ) + } + + func testMap_whenConfigurationContainsBaseSettingsDictionary_usesBaseSettings() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageType: .local, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key1"]), + .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageSettings: .test( + productDestinations: [ + "Product1": .iOS, + ], + baseSettings: .init( + base: [ + "EXCLUDED_ARCHS[sdk=iphonesimulator*]": .string("x86_64"), + ], + configurations: [ + .init(name: "Debug", variant: .debug): .init( + settings: ["CUSTOM_SETTING_1": .string("CUSTOM_VALUE_1")], + xcconfig: sourcesPath.appending(component: "Config.xcconfig") + ), + .init(name: "Release", variant: .release): .init( + settings: ["CUSTOM_SETTING_2": .string("CUSTOM_VALUE_2")], + xcconfig: sourcesPath.appending(component: "Config.xcconfig") + ), + ] + ) + ) + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + options: .options( + automaticSchemesOptions: .enabled(), + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + targets: [ + .test( + "Target1", + basePath: basePath, + destinations: .iOS, + deploymentTargets: .iOS("12.0"), + baseSettings: .settings( + base: [ + "EXCLUDED_ARCHS[sdk=iphonesimulator*]": .string("x86_64"), + ], + configurations: [ + .debug( + name: "Debug", + settings: ["CUSTOM_SETTING_1": .string("CUSTOM_VALUE_1")], + xcconfig: "Sources/Target1/Config.xcconfig" + ), + .release( + name: "Release", + settings: ["CUSTOM_SETTING_2": .string("CUSTOM_VALUE_2")], + xcconfig: "Sources/Target1/Config.xcconfig" + ), + ], + defaultSettings: .recommended + ), + customSettings: [ + "OTHER_LDFLAGS": ["key1", "key2", "key3"], + ] + ), + ] + ) + ) + } + + func testMap_whenConfigurationContainsTargetSettingsDictionary_mapsToCustomSettings() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let customSettings: XcodeGraph.SettingsDictionary = ["CUSTOM_SETTING": .string("CUSTOM_VALUE")] + + let targetSettings = ["Target1": customSettings] + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key1"]), + .init(tool: .linker, name: .unsafeFlags, condition: nil, value: ["key2", "key3"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageSettings: .test( + baseSettings: .default, + targetSettings: targetSettings + ) + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: [ + "OTHER_LDFLAGS": ["key1", "key2", "key3"], + "CUSTOM_SETTING": "CUSTOM_VALUE", + ] + ), + ] + ) + ) + } + + func testMap_whenConditionalSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init( + tool: .c, + name: .headerSearchPath, + condition: .init(platformNames: ["tvos"], config: nil), + value: ["value"] + ), + .init( + tool: .c, + name: .headerSearchPath, + condition: nil, + value: ["otherValue"] + ), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customSettings: [ + "HEADER_SEARCH_PATHS": ["$(SRCROOT)/Sources/Target1/otherValue"], + "HEADER_SEARCH_PATHS[sdk=appletvos*]": [ + "$(inherited)", + "$(SRCROOT)/Sources/Target1/value", + "$(SRCROOT)/Sources/Target1/otherValue", + ], + "HEADER_SEARCH_PATHS[sdk=appletvsimulator*]": [ + "$(inherited)", + "$(SRCROOT)/Sources/Target1/value", + "$(SRCROOT)/Sources/Target1/otherValue", + ], + ] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsLinkedFramework_mapsToSDKDependency() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .linker, name: .linkedFramework, condition: nil, value: ["Framework"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [.sdk(name: "Framework", type: .framework, status: .required)] + ), + ] + ) + ) + } + + func testMap_whenSettingsContainsLinkedLibrary_mapsToSDKDependency() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + settings: [ + .init(tool: .linker, name: .linkedLibrary, condition: nil, value: ["Library"]), + ] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [.sdk(name: "Library", type: .library, status: .required)] + ), + ] + ) + ) + } + + func testMap_whenTargetDependency_mapsToTargetDependency() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let dependenciesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1")) + try fileHandler.createFolder(sourcesPath) + try fileHandler.createFolder(dependenciesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.target(name: "Dependency1", condition: nil)] + ), + .test(name: "Dependency1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath, dependencies: [.target(name: "Dependency1")]), + .test("Dependency1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenBinaryTargetDependency_mapsToXcframework() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let dependenciesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1")) + try fileHandler.createFolder(sourcesPath) + try fileHandler.createFolder(dependenciesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.target(name: "Dependency1", condition: nil)] + ), + .test(name: "Dependency1", type: .binary), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [ + .xcframework(path: .path( + basePath.appending(try RelativePath(validating: "artifacts/Package/Dependency1.xcframework")) + .pathString + )), + ] + ), + ] + ) + ) + } + + func testMap_whenTargetByNameDependency_mapsToTargetDependency() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + let dependenciesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1")) + try fileHandler.createFolder(sourcesPath) + try fileHandler.createFolder(dependenciesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.byName(name: "Dependency1", condition: nil)] + ), + .test(name: "Dependency1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("Target1", basePath: basePath, dependencies: [.target(name: "Dependency1")]), + .test("Dependency1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenBinaryTargetURLByNameDependency_mapsToXcFramework() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [ + .byName(name: "Dependency1", condition: nil), + ] + ), + .test(name: "Dependency1", type: .binary, url: "someURL"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [ + .xcframework(path: .path( + basePath.appending(try RelativePath(validating: "artifacts/Package/Dependency1.xcframework")) + .pathString + )), + ] + ), + ] + ) + ) + } + + func testMap_whenBinaryTargetPathByNameDependency_mapsToXcFramework() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [ + .byName(name: "Dependency1", condition: nil), + ] + ), + .test(name: "Dependency1", type: .binary, path: "Dependency1/Dependency1.xcframework"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [.xcframework(path: .path( + basePath + .appending(try RelativePath(validating: "artifacts/Package/Dependency1/Dependency1.xcframework")) + .pathString + ))] + ), + ] + ) + ) + } + + func testMap_whenExternalProductDependency_mapsToProjectDependencies() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target2"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target3"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target4"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1"))) + + let package1 = PackageInfo( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.product(name: "Product2", package: "Package2", moduleAliases: nil, condition: nil)] + ), + .test(name: "Dependency1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ) + let package2 = PackageInfo( + name: "Package2", + products: [ + .init(name: "Product2", type: .library(.automatic), targets: ["Target2", "Target3"]), + .init(name: "Product3", type: .library(.automatic), targets: ["Target4"]), + ], + targets: [ + .test(name: "Target2"), + .test(name: "Target3"), + .test(name: "Target4"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: ["Package": package1, "Package2": package2] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [ + .external(name: "Product2", condition: nil), + ] + ), + ] + ) + ) + } + + func testMap_whenExternalByNameProductDependency_mapsToProjectDependencies() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target2"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target3"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target4"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1"))) + + let package1 = PackageInfo( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [.byName(name: "Product2", condition: nil)] + ), + .test(name: "Dependency1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ) + let package2 = PackageInfo( + name: "Package2", + products: [ + .init(name: "Product2", type: .library(.automatic), targets: ["Target2", "Target3"]), + .init(name: "Product3", type: .library(.automatic), targets: ["Target4"]), + ], + targets: [ + .test(name: "Target2"), + .test(name: "Target3"), + .test(name: "Target4"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: ["Package": package1, "Package2": package2] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + dependencies: [ + .external(name: "Product2", condition: nil), + ] + ), + ] + ) + ) + } + + func testMap_whenCustomCVersion_mapsToGccCLanguageStandardSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: "c99", + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .test( + name: "Package", + settings: .settings(base: ["GCC_C_LANGUAGE_STANDARD": "c99"]), + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenCustomCXXVersion_mapsToClangCxxLanguageStandardSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: "gnu++14", + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertEqual( + project, + .test( + name: "Package", + settings: .settings(base: ["CLANG_CXX_LANGUAGE_STANDARD": "gnu++14"]), + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenCustomSwiftVersion_mapsToSwiftVersionSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: ["4.0.0"] + ), + ] + ) + XCTAssertEqual( + project, + .test( + name: "Package", + settings: .settings(base: ["SWIFT_VERSION": "4.0.0"]), + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenMultipleCustomSwiftVersions_mapsLargestToSwiftVersionSetting() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: ["4.0.0", "5.0.0", "4.2.0"] + ), + ] + ) + XCTAssertEqual( + project, + .test( + name: "Package", + settings: .settings(base: ["SWIFT_VERSION": "5.0.0"]), + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenMultipleCustomSwiftVersionsAndConfiguredVersion_mapsLargestToSwiftVersionLowerThanConfigured() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: ["4.0.0", "5.0.0", "4.2.0"] + ), + ], + packageSettings: .test( + baseSettings: .default, + swiftToolsVersion: "4.4.0" + ) + ) + XCTAssertEqual( + project, + .test( + name: "Package", + settings: .settings(base: ["SWIFT_VERSION": "4.2.0"]), + targets: [ + .test("Target1", basePath: basePath), + ] + ) + ) + } + + func testMap_whenDependenciesContainsCustomConfiguration_mapsToProjectWithCustomConfig() throws { + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "Package/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageSettings: .test( + baseSettings: Settings( + configurations: [.release: nil, .debug: nil, .init(name: "Custom", variant: .release): nil], + defaultSettings: .recommended + ), + swiftToolsVersion: "4.4.0" + ) + ) + + XCTAssertNotNil(project?.settings?.configurations.first(where: { $0.name == "Custom" })) + } + + func testMap_whenTargetsWithDefaultHardcodedMapping() throws { + let basePath = try temporaryPath() + let testTargets = [ + "Nimble", + "Quick", + "RxTest", + "RxTest-Dynamic", + "SnapshotTesting", + "TempuraTesting", + "TSCTestSupport", + "ViewInspector", + "XCTVapor", + ] + let allTargets = ["RxSwift"] + testTargets + try allTargets + .map { basePath.appending(try RelativePath(validating: "Package/Sources/\($0)")) } + .forEach { try fileHandler.createFolder($0) } + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: allTargets), + ], + targets: allTargets.map { .test(name: $0) }, + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageSettings: .test( + baseSettings: .default, + targetSettings: [ + "Nimble": ["ENABLE_TESTING_SEARCH_PATHS": "NO", "ANOTHER_SETTING": "YES"], + "Quick": ["ANOTHER_SETTING": "YES"], + ] + ) + ) + + XCTAssertEqual( + project, + .testWithDefaultConfigs( + name: "Package", + targets: [ + .test("RxSwift", basePath: basePath, product: .framework), + ] + testTargets.map { + let customSettings: ProjectDescription.SettingsDictionary + var customProductName: String? + switch $0 { + case "Nimble": + customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "NO", "ANOTHER_SETTING": "YES"] + case "Quick": + customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "YES", "ANOTHER_SETTING": "YES"] + case "RxTest-Dynamic": // because RxTest does have an "-" we need to account for the custom mapping to product + // names + customProductName = "RxTest_Dynamic" + customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "YES"] + default: + customSettings = ["ENABLE_TESTING_SEARCH_PATHS": "YES"] + } + + return .test( + $0, + basePath: basePath, + customProductName: customProductName, + customSettings: customSettings + ) + } + ) + ) + } + + func testMap_whenTargetDependenciesOnTargetHaveConditions() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Dependency2"))) + + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [ + .target(name: "Dependency1", condition: .init(platformNames: ["ios"], config: nil)), + .target(name: "Dependency2", condition: .init(platformNames: ["tvos"], config: nil)), + ] + ), + .test(name: "Dependency1"), + .test(name: "Dependency2"), + ], + platforms: [.ios, .tvos], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + let expected: ProjectDescription.Project = .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customProductName: "Target1", + customBundleID: "Target1", + customSources: .custom(.sourceFilesList(globs: [ + basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")).pathString, + ])), + dependencies: [ + .target(name: "Dependency1", condition: .when([.ios])), + .target(name: "Dependency2", condition: .when([.tvos])), + ] + ), + .test( + "Dependency1", + basePath: basePath, + customProductName: "Dependency1", + customBundleID: "Dependency1", + customSources: .custom(.sourceFilesList(globs: [ + basePath.appending(try RelativePath(validating: "Package/Sources/Dependency1/**")).pathString, + ])) + ), + .test( + "Dependency2", + basePath: basePath, + customProductName: "Dependency2", + customBundleID: "Dependency2", + customSources: .custom(.sourceFilesList(globs: [ + basePath.appending(try RelativePath(validating: "Package/Sources/Dependency2/**")).pathString, + ])) + ), + ] + ) + + XCTAssertEqual(project?.name, expected.name) + + let projectTargets = project!.targets.sorted(by: \.name) + let expectedTargets = expected.targets.sorted(by: \.name) + XCTAssertBetterEqual(projectTargets, expectedTargets) + } + + func testMap_whenTargetDependenciesOnProductHaveConditions() throws { + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package/Sources/Target1"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target2"))) + try fileHandler.createFolder(basePath.appending(try RelativePath(validating: "Package2/Sources/Target3"))) + + let package1 = PackageInfo( + name: "Package", + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test( + name: "Target1", + dependencies: [ + .product( + name: "Product2", + package: "Package2", + moduleAliases: nil, + condition: .init(platformNames: ["ios"], config: nil) + ), + ] + ), + ], + platforms: [.ios, .tvos], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ) + let package2 = PackageInfo( + name: "Package2", + products: [ + .init(name: "Product2", type: .library(.automatic), targets: ["Target2", "Target3"]), + ], + targets: [ + .test(name: "Target2"), + .test(name: "Target3"), + ], + platforms: [.ios, .tvos], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ) + let project = try subject.map( + package: "Package", + basePath: basePath, + packageInfos: ["Package": package1, "Package2": package2] + ) + + let expected: ProjectDescription.Project = .testWithDefaultConfigs( + name: "Package", + targets: [ + .test( + "Target1", + basePath: basePath, + customProductName: "Target1", + customBundleID: "Target1", + customSources: .custom(.sourceFilesList(globs: [ + basePath.appending(try RelativePath(validating: "Package/Sources/Target1/**")).pathString, + ])), + dependencies: [ + .external(name: "Product2", condition: .when([.ios])), + ] + ), + ] + ) + + XCTAssertEqual(project?.name, expected.name) + + let projectTargets = project!.targets.sorted(by: \.name) + let expectedTargets = expected.targets.sorted(by: \.name) + + XCTAssertBetterEqual( + projectTargets, + expectedTargets + ) + } + + func testMap_whenTargetNameContainsSpaces() throws { + let packageName = "Package With Space" + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(try RelativePath(validating: "\(packageName)/Sources/Target1")) + try fileHandler.createFolder(sourcesPath) + + let project = try subject.map( + package: packageName, + basePath: basePath, + packageInfos: [ + packageName: .init( + name: packageName, + products: [ + .init(name: "Product1", type: .library(.automatic), targets: ["Target1"]), + ], + targets: [ + .test(name: "Target1"), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + XCTAssertBetterEqual( + project, + .testWithDefaultConfigs( + name: packageName, + targets: [ + .test( + "Target1", + packageName: packageName, + basePath: basePath + ), + ] + ) + ) + } + + func testMap_whenSwiftPackageHasTestTarget() throws { + // Given + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(components: ["Package", "Sources", "Target"]) + try fileHandler.createFolder(sourcesPath) + let testsPath = basePath.appending(components: ["Package", "Tests", "TargetTests"]) + try fileHandler.createFolder(testsPath) + + // When + let project = try subject.map( + package: "Package", + basePath: basePath, + packageType: .local, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product", type: .library(.automatic), targets: ["Target"]), + ], + targets: [ + .test(name: "Target"), + .test( + name: "TargetTests", + type: .test, + dependencies: [.target(name: "Target", condition: nil)] + ), + ], + platforms: [.ios], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ] + ) + + // Then + XCTAssertBetterEqual( + project, + .test( + name: "Package", + options: .options( + automaticSchemesOptions: .enabled(), + disableSynthesizedResourceAccessors: true + ), + settings: .settings(), + targets: [ + .test("Target", basePath: basePath), + .test( + "TargetTests", + basePath: basePath, + product: .unitTests, + customSources: .custom(.sourceFilesList(globs: [ + "\(testsPath.pathString)/**", + ])), + dependencies: [.target(name: "Target")] + ), + ] + ) + ) + } + + func testMap_whenSwiftPackageHasTestTargetWithExplicitProductDestinations() throws { + // Given + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(components: ["Package", "Sources", "Target"]) + try fileHandler.createFolder(sourcesPath) + let testsPath = basePath.appending(components: ["Package", "Tests", "TargetTests"]) + try fileHandler.createFolder(testsPath) + + // When + let project = try subject.map( + package: "Package", + basePath: basePath, + packageType: .local, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init(name: "Product", type: .library(.automatic), targets: ["Target"]), + ], + targets: [ + .test(name: "Target"), + .test( + name: "TargetTests", + type: .test, + dependencies: [.target(name: "Target", condition: nil)] + ), + ], + platforms: [.ios, .macos], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageSettings: .test( + productDestinations: ["Product": [.iPhone, .iPad]] + ) + ) + + // Then + XCTAssertBetterEqual( + project, + .test( + name: "Package", + options: .options( + automaticSchemesOptions: .enabled(), + disableSynthesizedResourceAccessors: true + ), + settings: .settings(), + targets: [ + .test( + "Target", + basePath: basePath, + destinations: [.iPhone, .iPad], + deploymentTargets: .iOS("12.0") + ), + .test( + "TargetTests", + basePath: basePath, + destinations: [.iPhone, .iPad], + product: .unitTests, + deploymentTargets: .iOS("12.0"), + customSources: .custom(.sourceFilesList(globs: [ + "\(testsPath.pathString)/**", + ])), + dependencies: [.target(name: "Target")] + ), + ] + ) + ) + } + + func testMap_whenSwiftPackageHasMultiDependencyTestTargetsWithExplicitProductDestinations() throws { + // Given + let basePath = try temporaryPath() + try fileHandler.createFolder(basePath.appending(components: ["Package", "Sources", "Target"])) + try fileHandler.createFolder(basePath.appending(components: ["Package", "Sources", "MacTarget"])) + try fileHandler.createFolder(basePath.appending(components: ["Package", "Sources", "CommonTarget"])) + let productTestsPath = basePath.appending(components: ["Package", "Tests", "ProductTests"]) + try fileHandler.createFolder(productTestsPath) + let macProductTestsPath = basePath.appending(components: ["Package", "Tests", "MacProductTests"]) + try fileHandler.createFolder(macProductTestsPath) + + // When + let project = try subject.map( + package: "Package", + basePath: basePath, + packageType: .local, + packageInfos: [ + "Package": .init( + name: "Package", + products: [ + .init( + name: "Product", + type: .library(.automatic), + targets: ["Target", "CommonTarget"] + ), + .init( + name: "MacProduct", + type: .library(.automatic), + targets: ["MacTarget", "CommonTarget"] + ), + ], + targets: [ + .test(name: "Target"), + .test(name: "MacTarget"), + .test(name: "CommonTarget"), + .test( + name: "ProductTests", + type: .test, + dependencies: [ + .target(name: "Target", condition: nil), + .target(name: "CommonTarget", condition: nil), + ] + ), + .test( + name: "MacProductTests", + type: .test, + dependencies: [ + .target(name: "MacTarget", condition: nil), + .target(name: "CommonTarget", condition: nil), + ] + ), + ], + platforms: [.ios, .macos], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: nil + ), + ], + packageSettings: .test( + productDestinations: ["MacProduct": [.mac]] + ) + ) + + // Then + XCTAssertBetterEqual( + project, + .test( + name: "Package", + options: .options( + automaticSchemesOptions: .enabled(), + disableSynthesizedResourceAccessors: true + ), + settings: .settings(), + targets: [ + .test("Target", basePath: basePath), + .test( + "MacTarget", + basePath: basePath, + destinations: [.mac], + deploymentTargets: .macOS("10.13") + ), + .test("CommonTarget", basePath: basePath), + .test( + "ProductTests", + basePath: basePath, + product: .unitTests, + customSources: .custom(.sourceFilesList(globs: [ + "\(productTestsPath.pathString)/**", + ])), + dependencies: [ + .target(name: "Target"), + .target(name: "CommonTarget"), + ] + ), + .test( + "MacProductTests", + basePath: basePath, + destinations: [.mac], + product: .unitTests, + deploymentTargets: .macOS("10.13"), + customSources: .custom(.sourceFilesList(globs: [ + "\(macProductTestsPath.pathString)/**", + ])), + dependencies: [ + .target(name: "MacTarget"), + .target(name: "CommonTarget"), + ] + ), + ] + ) + ) + } + + func testMap_whenSwiftPackageHasTestTargetWithExternalDependency() throws { + // Given + let basePath = try temporaryPath() + let sourcesPath = basePath.appending(components: ["Package", "Sources", "Target"]) + try fileHandler.createFolder(sourcesPath) + let testsPath = basePath.appending(components: ["Package", "Tests", "TargetTests"]) + try fileHandler.createFolder(testsPath) + + // When + let project = try subject.map( + package: "Package", + basePath: basePath, + packageType: .local, + packageInfos: [ + "Package": .test( + name: "Package", + products: [ + .init(name: "Product", type: .library(.automatic), targets: ["Target"]), + ], + targets: [ + .test(name: "Target"), + .test( + name: "TargetTests", + type: .test, + dependencies: [ + .target(name: "Target", condition: nil), + .product(name: "External", package: "External", moduleAliases: nil, condition: nil), + ] + ), + ] + ), + ] + ) + + // Then + XCTAssertBetterEqual( + project, + .test( + name: "Package", + options: .options( + automaticSchemesOptions: .enabled(), + disableSynthesizedResourceAccessors: true + ), + settings: .settings(), + targets: [ + .test("Target", basePath: basePath), + .test( + "TargetTests", + basePath: basePath, + product: .unitTests, + customSources: .custom(.sourceFilesList(globs: [ + "\(testsPath.pathString)/**", + ])), + dependencies: [ + .target(name: "Target"), + .external(name: "External", condition: nil), + ] + ), + ] + ) + ) + } +} + +private func defaultSpmResources(_ target: String, customPath: String? = nil) -> ProjectDescription.ResourceFileElements { + let fullPath: String + if let customPath { + fullPath = customPath + } else { + fullPath = "/Package/Sources/\(target)" + } + return [ + "\(fullPath)/**/*.xib", + "\(fullPath)/**/*.storyboard", + "\(fullPath)/**/*.xcdatamodeld", + "\(fullPath)/**/*.xcmappingmodel", + "\(fullPath)/**/*.xcassets", + "\(fullPath)/**/*.lproj", + ] +} + +extension PackageInfoMapping { + fileprivate func map( + package: String, + basePath: AbsolutePath = "/", + packageType: PackageType? = nil, + packageInfos: [String: PackageInfo] = [:], + packageSettings: TuistCore.PackageSettings = .test( + baseSettings: .default + ) + ) throws -> ProjectDescription.Project? { + let packageToTargetsToArtifactPaths: [String: [String: AbsolutePath]] = try packageInfos + .reduce(into: [:]) { packagesResult, element in + let (packageName, packageInfo) = element + packagesResult[packageName] = try packageInfo.targets + .reduce(into: [String: AbsolutePath]()) { targetsResult, target in + guard target.type == .binary else { + return + } + if let path = target.path { + targetsResult[target.name] = basePath.appending( + try RelativePath(validating: "artifacts/\(packageName)/\(path)") + ) + } else { + targetsResult[target.name] = basePath.appending( + try RelativePath(validating: "artifacts/\(packageName)/\(target.name).xcframework") + ) + } + } + } + + return try map( + packageInfo: packageInfos[package]!, + path: basePath.appending(component: package), + packageType: packageType ?? .external(artifactPaths: packageToTargetsToArtifactPaths[package]!), + packageSettings: packageSettings, + packageToProject: Dictionary(uniqueKeysWithValues: packageInfos.keys.map { + ($0, basePath.appending(component: $0)) + }) + ) + } +} + +extension PackageInfo.Target { + fileprivate static func test( + name: String, + type: PackageInfo.Target.TargetType = .regular, + path: String? = nil, + url: String? = nil, + sources: [String]? = nil, + resources: [PackageInfo.Target.Resource] = [], + exclude: [String] = [], + dependencies: [PackageInfo.Target.Dependency] = [], + publicHeadersPath: String? = nil, + settings: [TargetBuildSettingDescription.Setting] = [] + ) -> Self { + .init( + name: name, + path: path, + url: url, + sources: sources, + resources: resources, + exclude: exclude, + dependencies: dependencies, + publicHeadersPath: publicHeadersPath, + type: type, + settings: settings, + checksum: nil + ) + } +} + +extension ProjectDescription.Project { + fileprivate static func test( + name: String, + options: Options = .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + settings: ProjectDescription.Settings? = nil, + targets: [ProjectDescription.Target] + ) -> Self { + .init( + name: name, + options: options, + settings: settings, + targets: targets, + resourceSynthesizers: .default + ) + } + + fileprivate static func testWithDefaultConfigs( + name: String, + options: Options = .options( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableSynthesizedResourceAccessors: true, + textSettings: .textSettings(usesTabs: nil, indentWidth: nil, tabWidth: nil, wrapsLines: nil) + ), + targets: [ProjectDescription.Target] + ) -> Self { + Project.test( + name: name, + options: options, + settings: .settings(configurations: [ + .debug(name: .debug), + .release(name: .release), + ]), + targets: targets + ) + } +} + +extension ProjectDescription.Target { + fileprivate enum SourceFilesListType { + case `default` + case custom(SourceFilesList?) + } + + fileprivate static func test( + _ name: String, + packageName: String = "Package", + basePath: AbsolutePath = "/", + destinations: ProjectDescription.Destinations = Set(Destination.allCases), + product: ProjectDescription.Product = .staticFramework, + customProductName: String? = nil, + customBundleID: String? = nil, + deploymentTargets: ProjectDescription.DeploymentTargets = .multiplatform( + iOS: "12.0", + macOS: "10.13", + watchOS: "4.0", + tvOS: "12.0", + visionOS: "1.0" + ), + customSources: SourceFilesListType = .default, + resources: [ProjectDescription.ResourceFileElement] = [], + headers: ProjectDescription.Headers? = nil, + dependencies: [ProjectDescription.TargetDependency] = [], + baseSettings: ProjectDescription.Settings = .settings(), + customSettings: ProjectDescription.SettingsDictionary = [:], + moduleMap: String? = nil + ) -> Self { + let sources: SourceFilesList? + + switch customSources { + case let .custom(list): + sources = list + case .default: + // swiftlint:disable:next force_try + sources = + .sourceFilesList(globs: [ + basePath.appending(try! RelativePath(validating: "\(packageName)/Sources/\(name)/**")) + .pathString, + ]) + } + + return ProjectDescription.Target.target( + name: name, + destinations: destinations, + product: product, + productName: customProductName ?? name, + bundleId: customBundleID ?? name, + deploymentTargets: deploymentTargets, + infoPlist: .default, + sources: sources, + resources: resources.isEmpty ? nil : .resources(resources), + headers: headers, + dependencies: dependencies, + settings: DependenciesGraph.spmSettings( + packageName: packageName, + baseSettings: baseSettings, + with: customSettings, + moduleMap: moduleMap + ) + ) + } +} + +extension [ProjectDescription.ResourceFileElement] { + static func defaultResources( + path: AbsolutePath, + excluding: [Path] = [] + ) -> Self { + ["xib", "storyboard", "xcdatamodeld", "xcmappingmodel", "xcassets", "lproj"] + .map { file -> ProjectDescription.ResourceFileElement in + ResourceFileElement.glob( + pattern: .path("\(path.appending(component: "**").pathString)/*.\(file)"), + excluding: excluding + ) + } + } +} + +extension Sequence { + func sorted(by keyPath: KeyPath) -> [Element] { + sorted { a, b in + a[keyPath: keyPath] < b[keyPath: keyPath] + } + } +} diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SettingsMapperTests.swift b/Tests/TuistLoaderTests/SwiftPackageManager/SettingsMapperTests.swift similarity index 75% rename from Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SettingsMapperTests.swift rename to Tests/TuistLoaderTests/SwiftPackageManager/SettingsMapperTests.swift index 89699be34e4..2a8e48beb2b 100644 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SettingsMapperTests.swift +++ b/Tests/TuistLoaderTests/SwiftPackageManager/SettingsMapperTests.swift @@ -1,13 +1,12 @@ +import Path import ProjectDescription -import TSCBasic import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest -@testable import TuistDependencies -@testable import TuistDependenciesTesting +@testable import TuistLoader @testable import TuistSupportTesting final class SettingsMapperTests: XCTestCase { @@ -21,7 +20,7 @@ final class SettingsMapperTests: XCTestCase { settings: [] ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual(resolvedSettings, [ "GCC_PREPROCESSOR_DEFINITIONS": .array(["$(inherited)", @@ -45,7 +44,7 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["GCC_PREPROCESSOR_DEFINITIONS"], @@ -73,7 +72,7 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["HEADER_SEARCH_PATHS"], @@ -94,7 +93,7 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"], @@ -113,7 +112,7 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["OTHER_CFLAGS"], @@ -132,7 +131,7 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["OTHER_CPLUSPLUSFLAGS"], @@ -153,15 +152,15 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["OTHER_SWIFT_FLAGS"], .array([ "$(inherited)", "ArbitraryFlag", - "-enable-upcoming-feature NewFeature", - "-enable-experimental-feature Experimental", + "-enable-upcoming-feature \"NewFeature\"", + "-enable-experimental-feature \"Experimental\"", ]) ) } @@ -177,7 +176,7 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let resolvedSettings = try mapper.settingsDictionaryForPlatform(nil) + let resolvedSettings = try mapper.settingsDictionary() XCTAssertEqual( resolvedSettings["OTHER_LDFLAGS"], @@ -205,21 +204,21 @@ final class SettingsMapperTests: XCTestCase { settings: settings ) - let allPlatformSettings = try mapper.settingsDictionaryForPlatform(nil) + let allPlatformSettings = try mapper.settingsDictionary() XCTAssertEqual( allPlatformSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"], .string("$(inherited) SWIFT_PACKAGE Define1") ) - let iosPlatformSettings = try mapper.settingsDictionaryForPlatform(.ios) + let iosPlatformSettings = try mapper.settingsDictionary(for: .iOS) XCTAssertEqual( iosPlatformSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"], .string("$(inherited) SWIFT_PACKAGE Define1 Define2") ) - let combinedSettings = try mapper.settingsForPlatforms([.ios, .macos, .tvos]) + let combinedSettings = try mapper.mapSettings() XCTAssertEqual( combinedSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphoneos*]"], @@ -246,11 +245,60 @@ final class SettingsMapperTests: XCTestCase { .string("$(inherited) SWIFT_PACKAGE Define1") ) } + + func test_set_maccatalyst() throws { + let settings: [PackageInfo.Target.TargetBuildSettingDescription.Setting] = [ + .init(tool: .swift, name: .define, condition: nil, value: ["Define1"]), + .init( + tool: .swift, + name: .define, + condition: PackageInfo.PackageConditionDescription(platformNames: ["maccatalyst"], config: nil), + value: ["Define2"] + ), + ] + + let mapper = SettingsMapper( + headerSearchPaths: [], + mainRelativePath: try RelativePath(validating: "path"), + settings: settings + ) + + let allPlatformSettings = try mapper.settingsDictionary() + + XCTAssertEqual( + allPlatformSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"], + .string("$(inherited) SWIFT_PACKAGE Define1") + ) + + let iosPlatformSettings = try mapper.settingsDictionary(for: .iOS) + + XCTAssertEqual( + iosPlatformSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"], + .string("$(inherited) SWIFT_PACKAGE Define1 Define2") + ) + + let combinedSettings = try mapper.mapSettings() + + XCTAssertEqual( + combinedSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphoneos*]"], + .string("$(inherited) SWIFT_PACKAGE Define1 Define2") + ) + + XCTAssertEqual( + combinedSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphonesimulator*]"], + .string("$(inherited) SWIFT_PACKAGE Define1 Define2") + ) + + XCTAssertEqual( + combinedSettings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"], + .string("$(inherited) SWIFT_PACKAGE Define1") + ) + } } // OTHER_LDFLAGS -extension TuistGraph.SettingsDictionary { +extension XcodeGraph.SettingsDictionary { func stringValueFor(_ key: String) throws -> String { try XCTUnwrap(self[key]?.stringValue) } @@ -260,7 +308,7 @@ extension TuistGraph.SettingsDictionary { } } -extension TuistGraph.SettingValue { +extension XcodeGraph.SettingValue { var stringValue: String? { if case let .string(string) = self { return string @@ -279,11 +327,9 @@ extension TuistGraph.SettingValue { } extension PackageInfo.Platform { - static var ios = PackageInfo.Platform(platformName: "ios", version: "11.0", options: []) - static var macos = PackageInfo.Platform(platformName: "macos", version: "11.0", options: []) - static var watchos = PackageInfo.Platform(platformName: "watchos", version: "11.0", options: []) - static var tvos = PackageInfo.Platform(platformName: "tvos", version: "11.0", options: []) - static var visionos = PackageInfo.Platform(platformName: "visionos", version: "11.0", options: []) - static var linux = PackageInfo.Platform(platformName: "linux", version: "11.0", options: []) - static var windows = PackageInfo.Platform(platformName: "windows", version: "11.0", options: []) + static var ios = PackageInfo.Platform(platformName: "ios", version: "12.0", options: []) + static var macos = PackageInfo.Platform(platformName: "macos", version: "10.13", options: []) + static var watchos = PackageInfo.Platform(platformName: "watchos", version: "4.0", options: []) + static var tvos = PackageInfo.Platform(platformName: "tvos", version: "12.0", options: []) + static var visionos = PackageInfo.Platform(platformName: "visionos", version: "1.0", options: []) } diff --git a/Tests/TuistLoaderTests/SwiftPackageManager/SwiftPackageManagerModuleMapGeneratorTests.swift b/Tests/TuistLoaderTests/SwiftPackageManager/SwiftPackageManagerModuleMapGeneratorTests.swift new file mode 100644 index 00000000000..ec38e879510 --- /dev/null +++ b/Tests/TuistLoaderTests/SwiftPackageManager/SwiftPackageManagerModuleMapGeneratorTests.swift @@ -0,0 +1,171 @@ +import MockableTest +import Path +import TuistCore +import TuistCoreTesting +import TuistSupportTesting +import XCTest + +@testable import TuistLoader + +final class SwiftPackageManagerModuleMapGeneratorTests: TuistTestCase { + private var subject: SwiftPackageManagerModuleMapGenerator! + private var contentHasher: MockContentHashing! + + override func setUp() { + super.setUp() + contentHasher = MockContentHashing() + subject = SwiftPackageManagerModuleMapGenerator(contentHasher: contentHasher) + + given(contentHasher) + .hash(Parameter.any) + .willProduce { $0 } + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_generate_when_no_headers() throws { + try test_generate(for: .none) + } + + func test_generate_when_custom_module_map() throws { + try test_generate(for: .custom("/Absolute/Public/Headers/Path/module.modulemap", umbrellaHeaderPath: nil)) + } + + func test_generate_when_umbrella_header() throws { + try test_generate(for: .header( + "/Absolute/Public/Headers/Path/Module.h", + moduleMapPath: "/Absolute/PackageDir/Derived/Module.modulemap" + )) + } + + func test_generate_when_nested_umbrella_header() throws { + try test_generate(for: .header( + "/Absolute/Public/Headers/Path/Module/Module.h", + moduleMapPath: "/Absolute/PackageDir/Derived/Module.modulemap" + )) + } + + private func test_generate(for moduleMap: ModuleMap) throws { + var writeCount = 0 + fileHandler.stubContentsOfDirectory = { _ in + switch moduleMap { + case .none: + return [] + case .custom: + return ["/Absolute/Public/Headers/Path/module.modulemap"] + case let .header(umbrellaHeaderPath, moduleMapPath: _): + if umbrellaHeaderPath.parentDirectory.basename == "Module" { + return ["/Absolute/Public/Headers/Path/Module/Module.h"] + } else { + return ["/Absolute/Public/Headers/Path/Module.h"] + } + case .directory: + return ["/Absolute/Public/Headers/Path/AnotherHeader.h"] + } + } + fileHandler.stubExists = { path in + switch path { + case "/Absolute/Public/Headers/Path": + return moduleMap != .none + case "/Absolute/Public/Headers/Path/module.modulemap": + return moduleMap == .custom("/Absolute/Public/Headers/Path/module.modulemap", umbrellaHeaderPath: nil) + case "/Absolute/Public/Headers/Path/Module.h": + return moduleMap == .header( + AbsolutePath("/Absolute/Public/Headers/Path/Module.h"), + moduleMapPath: AbsolutePath("/Absolute/PackageDir/Derived/Module.modulemap") + ) + case "/Absolute/Public/Headers/Path/Module/Module.h": + return moduleMap == .header( + AbsolutePath("/Absolute/Public/Headers/Path/Module/Module.h"), + moduleMapPath: AbsolutePath("/Absolute/PackageDir/Derived/Module.modulemap") + ) + case "/Absolute/PackageDir/Derived": + return true + default: + XCTFail("Unexpected exists call: \(path)") + return false + } + } + fileHandler.stubWrite = { content, path, atomically in + writeCount += 1 + guard let expectedContent = self.expectedContent(for: moduleMap) else { + XCTFail("FileHandler.write should not be called") + return + } + + XCTAssertEqual(content, expectedContent) + XCTAssertEqual(path, "/Absolute/PackageDir/Derived/Module.modulemap") + XCTAssertTrue(atomically) + } + + var hash: String? = nil + + given(contentHasher) + .hash(path: .any) + .willProduce { hash ?? $0.pathString } + + let got = try subject.generate( + packageDirectory: "/Absolute/PackageDir", + moduleName: "Module", + publicHeadersPath: "/Absolute/Public/Headers/Path" + ) + + // Set hasher for path on disk + hash = expectedContent(for: moduleMap) + + // generate a 2nd time to validate that we dont write content that is already on disk + let _ = try subject.generate( + packageDirectory: "/Absolute/PackageDir", + moduleName: "Module", + publicHeadersPath: "/Absolute/Public/Headers/Path" + ) + + XCTAssertEqual(got, moduleMap) + switch moduleMap { + case .none, .custom: + XCTAssertEqual(writeCount, 0) + case .directory, .header: + XCTAssertEqual(writeCount, 1) + } + } + + private func expectedContent(for moduleMap: ModuleMap) -> String? { + let expectedContent: String + switch moduleMap { + case .none, .custom: + return nil + case let .header(umbrellaHeaderPath, moduleMapPath: _): + if umbrellaHeaderPath.parentDirectory.basename == "Module" { + expectedContent = """ + framework module Module { + umbrella header "/Absolute/Public/Headers/Path/Module/Module.h" + + export * + module * { export * } + } + """ + } else { + expectedContent = """ + framework module Module { + umbrella header "/Absolute/Public/Headers/Path/Module.h" + + export * + module * { export * } + } + """ + } + case .directory: + expectedContent = """ + module Module { + umbrella "/Absolute/Public/Headers/Path" + export * + } + + """ + } + return expectedContent + } +} diff --git a/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift b/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift index 5fa30c902b8..f2845019fc3 100644 --- a/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift +++ b/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest @@ -485,7 +485,7 @@ final class ManifestFilesLocatorTests: TuistUnitTestCase { XCTAssertNil(configPath) } - func test_locatePackageManifest() throws { + func test_locatePackageManifest_when_in_root() throws { // Given let paths = try createFiles([ "Module01/File01.swift", @@ -499,7 +499,8 @@ final class ManifestFilesLocatorTests: TuistUnitTestCase { "File01.swift", "File02.swift", - "Tuist/Package.swift", + "Tuist/Config.swift", + "Package.swift", ]) // When @@ -510,81 +511,7 @@ final class ManifestFilesLocatorTests: TuistUnitTestCase { XCTAssertEqual(paths.last, packageManifestPath) } - func test_locateDependencies() 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/Dependencies.swift", - ]) - - // When - let dependenciesPath = subject.locateDependencies(at: try temporaryPath()) - - // Then - XCTAssertNotNil(dependenciesPath) - XCTAssertEqual(paths.last, dependenciesPath) - } - - func test_locateDependencies_traversing() 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/Dependencies.swift", - ]) - let locatingPath = paths[5] // "Module02/Subdir01/File01.swift" - - // When - let dependenciesPath = subject.locateDependencies(at: locatingPath) - - // Then - XCTAssertNotNil(dependenciesPath) - XCTAssertEqual(paths.last, dependenciesPath) - } - - func test_locateDependencies_where_config_not_exist() throws { - // Given - 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", - ]) - - // When - let dependenciesPath = subject.locateDependencies(at: try temporaryPath()) - - // Then - XCTAssertNil(dependenciesPath) - } - - func test_locateDependencies_traversing_where_config_not_exist() throws { + func test_locatePackageManifest() throws { // Given let paths = try createFiles([ "Module01/File01.swift", @@ -598,14 +525,17 @@ final class ManifestFilesLocatorTests: TuistUnitTestCase { "File01.swift", "File02.swift", + "Tuist/Config.swift", + "Package.swift", + "Tuist/Package.swift", ]) - let locatingPath = paths[5] // "Module02/Subdir01/File01.swift" // When - let dependenciesPath = subject.locateDependencies(at: locatingPath) + let packageManifestPath = subject.locatePackageManifest(at: try temporaryPath()) // Then - XCTAssertNil(dependenciesPath) + XCTAssertNotNil(packageManifestPath) + XCTAssertEqual(paths.last, packageManifestPath) } func test_locateProjectManifests_returns_all_manifest_containing_manifest_signature() throws { diff --git a/Tests/TuistLoaderTests/Utils/PluginsHelperTests.swift b/Tests/TuistLoaderTests/Utils/PluginsHelperTests.swift index bd6b37c8813..674fc0763d1 100644 --- a/Tests/TuistLoaderTests/Utils/PluginsHelperTests.swift +++ b/Tests/TuistLoaderTests/Utils/PluginsHelperTests.swift @@ -1,5 +1,6 @@ import Foundation -import TSCBasic +import Path +import TuistCore import TuistSupport import XCTest diff --git a/Tests/TuistLoaderTests/Utils/ProjectDescriptionSearchPathsTests.swift b/Tests/TuistLoaderTests/Utils/ProjectDescriptionSearchPathsTests.swift index 784c2aa1e2f..e8b4d4da3f0 100644 --- a/Tests/TuistLoaderTests/Utils/ProjectDescriptionSearchPathsTests.swift +++ b/Tests/TuistLoaderTests/Utils/ProjectDescriptionSearchPathsTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistLoader diff --git a/Tests/TuistMigrationIntegrationTests/Utilities/EmptyBuildSettingsCheckerIntegrationTests.swift b/Tests/TuistMigrationIntegrationTests/Utilities/EmptyBuildSettingsCheckerIntegrationTests.swift index 3eba19f4418..5132d75a389 100644 --- a/Tests/TuistMigrationIntegrationTests/Utilities/EmptyBuildSettingsCheckerIntegrationTests.swift +++ b/Tests/TuistMigrationIntegrationTests/Utilities/EmptyBuildSettingsCheckerIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistMigrationIntegrationTests/Utilities/SettingsToXCConfigExtractorIntegrationTests.swift b/Tests/TuistMigrationIntegrationTests/Utilities/SettingsToXCConfigExtractorIntegrationTests.swift index c2a771b1fb9..9611bbe789c 100644 --- a/Tests/TuistMigrationIntegrationTests/Utilities/SettingsToXCConfigExtractorIntegrationTests.swift +++ b/Tests/TuistMigrationIntegrationTests/Utilities/SettingsToXCConfigExtractorIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistMigrationIntegrationTests/Utilities/TargetsExtractorIntegrationTests.swift b/Tests/TuistMigrationIntegrationTests/Utilities/TargetsExtractorIntegrationTests.swift index 6cd9142b0cd..5a699d55109 100644 --- a/Tests/TuistMigrationIntegrationTests/Utilities/TargetsExtractorIntegrationTests.swift +++ b/Tests/TuistMigrationIntegrationTests/Utilities/TargetsExtractorIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistMigrationTests/Utilities/EmptyBuildSettingsCheckerTests.swift b/Tests/TuistMigrationTests/Utilities/EmptyBuildSettingsCheckerTests.swift index 7565e1c5470..540241263e0 100644 --- a/Tests/TuistMigrationTests/Utilities/EmptyBuildSettingsCheckerTests.swift +++ b/Tests/TuistMigrationTests/Utilities/EmptyBuildSettingsCheckerTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistMigration @testable import TuistSupportTesting diff --git a/Tests/TuistPluginTests/PluginServiceTests.swift b/Tests/TuistPluginTests/PluginServiceTests.swift index 8fdcfe71b03..e3a7bfdbfb9 100644 --- a/Tests/TuistPluginTests/PluginServiceTests.swift +++ b/Tests/TuistPluginTests/PluginServiceTests.swift @@ -1,42 +1,48 @@ -import ProjectDescription +import MockableTest +import Path +import struct ProjectDescription.Plugin +import struct ProjectDescription.PluginLocation import TSCBasic import TuistCore import TuistCoreTesting -import TuistGraph -import TuistGraphTesting import TuistLoader -import TuistLoaderTesting import TuistScaffold import TuistScaffoldTesting import TuistSupport import TuistSupportTesting +import XcodeGraph import XCTest - @testable import TuistPlugin final class PluginServiceTests: TuistUnitTestCase { - private var manifestLoader: MockManifestLoader! + private var manifestLoader: MockManifestLoading! private var templatesDirectoryLocator: MockTemplatesDirectoryLocator! private var gitHandler: MockGitHandler! private var subject: PluginService! - private var cacheDirectoriesProvider: MockCacheDirectoriesProvider! - private var cacheDirectoryProviderFactory: MockCacheDirectoriesProviderFactory! - private var fileUnarchiver: MockFileUnarchiver! + private var cacheDirectoriesProvider: MockCacheDirectoriesProviding! + private var cacheDirectoryProviderFactory: MockCacheDirectoriesProviderFactoring! + private var fileUnarchiver: MockFileUnarchiving! private var fileClient: MockFileClient! override func setUp() { super.setUp() - manifestLoader = MockManifestLoader() + manifestLoader = .init() templatesDirectoryLocator = MockTemplatesDirectoryLocator() gitHandler = MockGitHandler() - let mockCacheDirectoriesProvider = try! MockCacheDirectoriesProvider() + let mockCacheDirectoriesProvider = MockCacheDirectoriesProviding() cacheDirectoriesProvider = mockCacheDirectoriesProvider - cacheDirectoriesProvider.cacheDirectoryStub = try! temporaryPath() - cacheDirectoryProviderFactory = MockCacheDirectoriesProviderFactory(provider: cacheDirectoriesProvider) - cacheDirectoryProviderFactory.cacheDirectoriesStub = { _ in mockCacheDirectoriesProvider } - fileUnarchiver = MockFileUnarchiver() - let fileArchivingFactory = MockFileArchivingFactory() - fileArchivingFactory.stubbedMakeFileUnarchiverResult = fileUnarchiver + given(cacheDirectoriesProvider) + .cacheDirectory() + .willReturn(try! temporaryPath()) + cacheDirectoryProviderFactory = .init() + given(cacheDirectoryProviderFactory) + .cacheDirectories() + .willReturn(cacheDirectoriesProvider) + fileUnarchiver = MockFileUnarchiving() + let fileArchivingFactory = MockFileArchivingFactorying() + + given(fileArchivingFactory).makeFileUnarchiver(for: .any).willReturn(fileUnarchiver) + fileClient = MockFileClient() subject = PluginService( manifestLoader: manifestLoader, @@ -79,11 +85,14 @@ final class PluginServiceTests: TuistUnitTestCase { .git(url: pluginCGitURL, gitReference: .tag(pluginCGitTag), directory: "Sub/Subfolder", releaseUrl: nil), ] ) - let pluginADirectory = cacheDirectoriesProvider.cacheDirectory(for: .plugins) + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) + let pluginADirectory = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(component: pluginAFingerprint) - let pluginBDirectory = cacheDirectoriesProvider.cacheDirectory(for: .plugins) + let pluginBDirectory = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(component: pluginBFingerprint) - let pluginCDirectory = cacheDirectoriesProvider.cacheDirectory(for: .plugins) + let pluginCDirectory = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(component: pluginCFingerprint) try fileHandler.touch( pluginBDirectory.appending(components: PluginServiceConstants.release) @@ -124,17 +133,20 @@ final class PluginServiceTests: TuistUnitTestCase { ] ) var invokedCloneURL: String? - var invokedClonePath: AbsolutePath? + var invokedClonePath: Path.AbsolutePath? gitHandler.cloneToStub = { url, path in invokedCloneURL = url invokedClonePath = path } var invokedCheckoutID: String? - var invokedCheckoutPath: AbsolutePath? + var invokedCheckoutPath: Path.AbsolutePath? gitHandler.checkoutStub = { id, path in invokedCheckoutID = id invokedCheckoutPath = path } + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) // When _ = try await subject.fetchRemotePlugins(using: config) @@ -143,13 +155,13 @@ final class PluginServiceTests: TuistUnitTestCase { XCTAssertEqual(invokedCloneURL, pluginGitURL) XCTAssertEqual( invokedClonePath, - cacheDirectoriesProvider.cacheDirectory(for: .plugins) + try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) ) XCTAssertEqual(invokedCheckoutID, pluginGitSha) XCTAssertEqual( invokedCheckoutPath, - cacheDirectoriesProvider.cacheDirectory(for: .plugins) + try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) ) } @@ -165,17 +177,20 @@ final class PluginServiceTests: TuistUnitTestCase { ] ) var invokedCloneURL: String? - var invokedClonePath: AbsolutePath? + var invokedClonePath: Path.AbsolutePath? gitHandler.cloneToStub = { url, path in invokedCloneURL = url invokedClonePath = path } var invokedCheckoutID: String? - var invokedCheckoutPath: AbsolutePath? + var invokedCheckoutPath: Path.AbsolutePath? gitHandler.checkoutStub = { id, path in invokedCheckoutID = id invokedCheckoutPath = path } + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) // When _ = try await subject.fetchRemotePlugins(using: config) @@ -184,13 +199,13 @@ final class PluginServiceTests: TuistUnitTestCase { XCTAssertEqual(invokedCloneURL, pluginGitURL) XCTAssertEqual( invokedClonePath, - cacheDirectoriesProvider.cacheDirectory(for: .plugins) + try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) ) XCTAssertEqual(invokedCheckoutID, pluginGitTag) XCTAssertEqual( invokedCheckoutPath, - cacheDirectoriesProvider.cacheDirectory(for: .plugins) + try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) ) } @@ -206,13 +221,16 @@ final class PluginServiceTests: TuistUnitTestCase { ] ) - let pluginDirectory = cacheDirectoriesProvider.cacheDirectory(for: .plugins) - .appending(component: pluginFingerprint) let temporaryDirectory = try temporaryPath() - cacheDirectoriesProvider.cacheDirectoryStub = temporaryDirectory + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(temporaryDirectory) + + let pluginDirectory = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) + .appending(component: pluginFingerprint) try fileHandler.touch( pluginDirectory - .appending(components: PluginServiceConstants.repository, Constants.DependenciesDirectory.packageSwiftName) + .appending(components: PluginServiceConstants.repository, Constants.SwiftPackageManager.packageSwiftName) ) let commandPath = pluginDirectory.appending(components: PluginServiceConstants.release, "tuist-command") try fileHandler.touch(commandPath) @@ -226,15 +244,19 @@ final class PluginServiceTests: TuistUnitTestCase { let pluginPath = try temporaryPath().appending(component: "Plugin") let pluginName = "TestPlugin" - manifestLoader.loadConfigStub = { _ in - .test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))]) - } + given(manifestLoader) + .loadConfig(at: .any) + .willReturn( + .test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))]) + ) - manifestLoader.loadPluginStub = { _ in - ProjectDescription.Plugin(name: pluginName) - } + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn( + ProjectDescription.Plugin(name: pluginName) + ) - let config = mockConfig(plugins: [TuistGraph.PluginLocation.local(path: pluginPath.pathString)]) + let config = mockConfig(plugins: [TuistCore.PluginLocation.local(path: pluginPath.pathString)]) try fileHandler.createFolder( pluginPath.appending(component: Constants.helpersDirectoryName) @@ -255,28 +277,40 @@ final class PluginServiceTests: TuistUnitTestCase { let pluginGitUrl = "https://url/to/repo.git" let pluginGitReference = "1.0.0" let pluginFingerprint = "\(pluginGitUrl)-\(pluginGitReference)".md5 - let cachedPluginPath = cacheDirectoriesProvider.cacheDirectory(for: .plugins) + + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) + + let cachedPluginPath = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) let pluginName = "TestPlugin" - manifestLoader.loadConfigStub = { _ in - .test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitReference)]) - } + given(manifestLoader) + .loadConfig(at: .any) + .willReturn( + .test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitReference)]) + ) - manifestLoader.loadPluginStub = { _ in - ProjectDescription.Plugin(name: pluginName) - } + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn( + ProjectDescription.Plugin(name: pluginName) + ) try fileHandler.createFolder(cachedPluginPath.appending(component: Constants.helpersDirectoryName)) let config = mockConfig(plugins: [ - TuistGraph.PluginLocation.git( + TuistCore.PluginLocation.git( url: pluginGitUrl, gitReference: .tag(pluginGitReference), directory: nil, releaseUrl: nil ), ]) + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) // When let plugins = try await subject.loadPlugins(using: config) @@ -294,17 +328,21 @@ final class PluginServiceTests: TuistUnitTestCase { let pluginName = "TestPlugin" let resourceTemplatesPath = pluginPath.appending(components: "ResourceSynthesizers") - try makeDirectories(resourceTemplatesPath) + try makeDirectories(.init(validating: resourceTemplatesPath.pathString)) - manifestLoader.loadConfigStub = { _ in - .test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))]) - } + given(manifestLoader) + .loadConfig(at: .any) + .willReturn( + .test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))]) + ) - manifestLoader.loadPluginStub = { _ in - ProjectDescription.Plugin(name: pluginName) - } + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn( + ProjectDescription.Plugin(name: pluginName) + ) - let config = mockConfig(plugins: [TuistGraph.PluginLocation.local(path: pluginPath.pathString)]) + let config = mockConfig(plugins: [TuistCore.PluginLocation.local(path: pluginPath.pathString)]) // When let plugins = try await subject.loadPlugins(using: config) @@ -321,24 +359,30 @@ final class PluginServiceTests: TuistUnitTestCase { let pluginGitUrl = "https://url/to/repo.git" let pluginGitReference = "1.0.0" let pluginFingerprint = "\(pluginGitUrl)-\(pluginGitReference)".md5 - let cachedPluginPath = cacheDirectoriesProvider.cacheDirectory(for: .plugins) + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) + let cachedPluginPath = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) let pluginName = "TestPlugin" let resourceTemplatesPath = cachedPluginPath.appending(components: "ResourceSynthesizers") - try makeDirectories(resourceTemplatesPath) - - manifestLoader.loadConfigStub = { _ in - .test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitReference)]) - } + try makeDirectories(.init(validating: resourceTemplatesPath.pathString)) - manifestLoader.loadPluginStub = { _ in - ProjectDescription.Plugin(name: pluginName) - } + given(manifestLoader) + .loadConfig(at: .any) + .willReturn( + .test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitReference)]) + ) + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn( + ProjectDescription.Plugin(name: pluginName) + ) let config = mockConfig(plugins: [ - TuistGraph.PluginLocation.git( + TuistCore.PluginLocation.git( url: pluginGitUrl, gitReference: .tag(pluginGitReference), directory: nil, @@ -367,18 +411,21 @@ final class PluginServiceTests: TuistUnitTestCase { ] } - try makeDirectories(templatePath) + try makeDirectories(.init(validating: templatePath.pathString)) // When - manifestLoader.loadConfigStub = { _ in - .test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))]) - } - - manifestLoader.loadPluginStub = { _ in - ProjectDescription.Plugin(name: pluginName) - } - - let config = mockConfig(plugins: [TuistGraph.PluginLocation.local(path: pluginPath.pathString)]) + given(manifestLoader) + .loadConfig(at: .any) + .willReturn( + .test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))]) + ) + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn( + ProjectDescription.Plugin(name: pluginName) + ) + + let config = mockConfig(plugins: [TuistCore.PluginLocation.local(path: pluginPath.pathString)]) // Then let plugins = try await subject.loadPlugins(using: config) @@ -391,7 +438,10 @@ final class PluginServiceTests: TuistUnitTestCase { let pluginGitUrl = "https://url/to/repo.git" let pluginGitReference = "1.0.0" let pluginFingerprint = "\(pluginGitUrl)-\(pluginGitReference)".md5 - let cachedPluginPath = cacheDirectoriesProvider.cacheDirectory(for: .plugins) + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try temporaryPath()) + let cachedPluginPath = try cacheDirectoriesProvider.cacheDirectory(for: .plugins) .appending(components: pluginFingerprint, PluginServiceConstants.repository) let pluginName = "TestPlugin" let templatePath = cachedPluginPath.appending(components: "Templates", "custom") @@ -401,20 +451,24 @@ final class PluginServiceTests: TuistUnitTestCase { ] } - try makeDirectories(templatePath) + try makeDirectories(.init(validating: templatePath.pathString)) // When - manifestLoader.loadConfigStub = { _ in - .test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitReference)]) - } - - manifestLoader.loadPluginStub = { _ in - ProjectDescription.Plugin(name: pluginName) - } + given(manifestLoader) + .loadConfig(at: .any) + .willReturn( + .test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitReference)]) + ) + + given(manifestLoader) + .loadPlugin(at: .any) + .willReturn( + ProjectDescription.Plugin(name: pluginName) + ) let config = mockConfig(plugins: [ - TuistGraph.PluginLocation + TuistCore.PluginLocation .git(url: pluginGitUrl, gitReference: .tag(pluginGitReference), directory: nil, releaseUrl: nil), ]) @@ -424,11 +478,11 @@ final class PluginServiceTests: TuistUnitTestCase { XCTAssertEqual(plugins, expectedPlugins) } - private func mockConfig(plugins: [TuistGraph.PluginLocation]) -> TuistGraph.Config { - Config( + private func mockConfig(plugins: [TuistCore.PluginLocation]) -> TuistCore.Config { + TuistCore.Config( compatibleXcodeVersions: .all, - cloud: nil, - cache: nil, + fullHandle: nil, + url: Constants.URLs.production, swiftVersion: nil, plugins: plugins, generationOptions: .test(), diff --git a/Tests/TuistScaffoldIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift b/Tests/TuistScaffoldIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift index 4b34a6e04e7..ec64cd51595 100644 --- a/Tests/TuistScaffoldIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift +++ b/Tests/TuistScaffoldIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TuistSupport import XCTest diff --git a/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift b/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift index ab381eb9334..ffe8a621f2f 100644 --- a/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift +++ b/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift @@ -1,6 +1,6 @@ import Foundation -import TSCBasic -import TuistGraph +import Path +import TuistCore import TuistSupport import XCTest @@ -22,7 +22,7 @@ final class TemplateGeneratorTests: TuistTestCase { super.tearDown() } - func test_directories_are_generated() throws { + func test_directories_are_generated() async throws { // Given let directories = [ try RelativePath(validating: "a"), @@ -38,7 +38,7 @@ final class TemplateGeneratorTests: TuistTestCase { let template = Template.test(items: items) // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, attributes: [:] @@ -48,7 +48,7 @@ final class TemplateGeneratorTests: TuistTestCase { XCTAssertTrue(expectedDirectories.allSatisfy(FileHandler.shared.exists)) } - func test_directories_with_attributes() throws { + func test_directories_with_attributes() async throws { // Given let directories = [ try RelativePath(validating: "{{ name|lowercase }}"), @@ -67,13 +67,13 @@ final class TemplateGeneratorTests: TuistTestCase { ].map(destinationPath.appending) // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, attributes: [ - "name": "Test_Name", - "aName": "test", - "bName": "nested_dir", + "name": .string("Test_Name"), + "aName": .string("test"), + "bName": .string("nested_dir"), ] ) @@ -81,7 +81,7 @@ final class TemplateGeneratorTests: TuistTestCase { XCTAssertTrue(expectedDirectories.allSatisfy(FileHandler.shared.exists)) } - func test_files_are_generated() throws { + func test_files_are_generated() async throws { // Given let items: [Template.Item] = [ Template.Item(path: try RelativePath(validating: "a"), contents: .string("aContent")), @@ -104,7 +104,7 @@ final class TemplateGeneratorTests: TuistTestCase { } // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, attributes: [:] @@ -116,7 +116,7 @@ final class TemplateGeneratorTests: TuistTestCase { } } - func test_files_are_generated_with_attributes() throws { + func test_files_are_generated_with_attributes() async throws { // Given let sourcePath = try temporaryPath() let items = [ @@ -146,15 +146,15 @@ final class TemplateGeneratorTests: TuistTestCase { ] // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, attributes: [ - "name": name, - "contentName": contentName, - "directoryName": directoryName, - "fileName": fileName, - "filePath": filePath, + "name": .string(name), + "contentName": .string(contentName), + "directoryName": .string(directoryName), + "fileName": .string(fileName), + "filePath": .string(filePath), ] ) @@ -164,7 +164,7 @@ final class TemplateGeneratorTests: TuistTestCase { } } - func test_rendered_files() throws { + func test_rendered_files() async throws { // Given let sourcePath = try temporaryPath() let destinationPath = try temporaryPath() @@ -190,10 +190,10 @@ final class TemplateGeneratorTests: TuistTestCase { ] // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, - attributes: ["name": name] + attributes: ["name": .string(name)] ) // Then @@ -202,7 +202,7 @@ final class TemplateGeneratorTests: TuistTestCase { } } - func test_file_rendered_with_attributes() throws { + func test_file_rendered_with_attributes() async throws { // Given let sourcePath = try temporaryPath() let destinationPath = try temporaryPath() @@ -218,10 +218,10 @@ final class TemplateGeneratorTests: TuistTestCase { )]) // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, - attributes: ["name": "attribute name"] + attributes: ["name": .string("attribute name")] ) // Then @@ -231,7 +231,7 @@ final class TemplateGeneratorTests: TuistTestCase { ) } - func test_only_stencil_files_rendered() throws { + func test_only_stencil_files_rendered() async throws { // Given let sourcePath = try temporaryPath() let destinationPath = try temporaryPath() @@ -259,10 +259,10 @@ final class TemplateGeneratorTests: TuistTestCase { ]) // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, - attributes: ["name": "attribute name"] + attributes: ["name": .string("attribute name")] ) // Then @@ -276,7 +276,7 @@ final class TemplateGeneratorTests: TuistTestCase { ) } - func test_empty_stencil_files_are_skipped() throws { + func test_empty_stencil_files_are_skipped() async throws { // Given let sourcePath = try temporaryPath() let destinationPath = try temporaryPath() @@ -293,17 +293,17 @@ final class TemplateGeneratorTests: TuistTestCase { ]) // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, - attributes: ["name": "attribute name"] + attributes: ["name": .string("attribute name")] ) // Then XCTAssertFalse(FileHandler.shared.exists(destinationPath.appending(component: "ignore"))) } - func test_copy_directory() throws { + func test_copy_directory() async throws { // Given let sourcePath = try temporaryPath().appending(components: "folder") try FileHandler.shared.createFolder(sourcePath) @@ -325,7 +325,7 @@ final class TemplateGeneratorTests: TuistTestCase { ]) // When - try subject.generate( + try await subject.generate( template: template, to: destinationPath, attributes: [:] diff --git a/Tests/TuistServerTests/Client/ServerClientAuthenticationMiddlewareTests.swift b/Tests/TuistServerTests/Client/ServerClientAuthenticationMiddlewareTests.swift new file mode 100644 index 00000000000..128f92bdeb1 --- /dev/null +++ b/Tests/TuistServerTests/Client/ServerClientAuthenticationMiddlewareTests.swift @@ -0,0 +1,259 @@ +import Foundation +import MockableTest +import OpenAPIRuntime +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistServer + +final class ServerClientAuthenticationMiddlewareTests: TuistUnitTestCase { + private var subject: ServerClientAuthenticationMiddleware! + private var serverAuthenticationController: MockServerAuthenticationControlling! + private var serverCredentialsStore: MockServerCredentialsStoring! + private var refreshAuthTokenService: MockRefreshAuthTokenServicing! + private var dateService: MockDateServicing! + + override func setUp() { + super.setUp() + + serverAuthenticationController = .init() + serverCredentialsStore = .init() + refreshAuthTokenService = .init() + dateService = .init() + subject = .init( + serverAuthenticationController: serverAuthenticationController, + serverCredentialsStore: serverCredentialsStore, + refreshAuthTokenService: refreshAuthTokenService, + dateService: dateService + ) + } + + override func tearDown() { + serverAuthenticationController = nil + serverCredentialsStore = nil + refreshAuthTokenService = nil + dateService = nil + subject = nil + super.tearDown() + } + + func test_when_authentication_token_is_nil() async throws { + let url = URL(string: "https://test.tuist.io")! + let request = Request(path: "/", method: .get) + let response = Response( + statusCode: 200 + ) + + given(serverAuthenticationController) + .authenticationToken(serverURL: .any) + .willReturn(nil) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.intercept( + request, + baseURL: url, + operationID: "123" + ) { _, _ in + response + }, + ServerClientAuthenticationError.notAuthenticated + ) + } + + func test_when_using_project_token() async throws { + let url = URL(string: "https://test.tuist.io")! + let request = Request(path: "/", method: .get) + let response = Response( + statusCode: 200 + ) + + given(serverAuthenticationController) + .authenticationToken(serverURL: .any) + .willReturn(.project("project-token")) + + var gotRequest: Request! + + // When + let gotResponse = try await subject.intercept( + request, + baseURL: url, + operationID: "123" + ) { request, _ in + gotRequest = request + return response + } + + // Then + XCTAssertEqual(gotResponse, response) + XCTAssertEqual( + gotRequest.headerFields, + [ + HeaderField( + name: "Authorization", value: "Bearer project-token" + ), + ] + ) + } + + func test_when_using_legacy_token() async throws { + let url = URL(string: "https://test.tuist.io")! + let request = Request(path: "/", method: .get) + let response = Response( + statusCode: 200 + ) + + given(serverAuthenticationController) + .authenticationToken(serverURL: .any) + .willReturn(.user(legacyToken: "legacy-token", accessToken: nil, refreshToken: nil)) + + var gotRequest: Request! + + // When + let gotResponse = try await subject.intercept( + request, + baseURL: url, + operationID: "123" + ) { request, _ in + gotRequest = request + return response + } + + // Then + XCTAssertEqual(gotResponse, response) + XCTAssertEqual( + gotRequest.headerFields, + [ + HeaderField( + name: "Authorization", value: "Bearer legacy-token" + ), + ] + ) + } + + func test_when_using_valid_access_token() async throws { + let url = URL(string: "https://test.tuist.io")! + let request = Request(path: "/", method: .get) + let response = Response( + statusCode: 200 + ) + + given(serverAuthenticationController) + .authenticationToken(serverURL: .any) + .willReturn( + .user( + legacyToken: nil, + accessToken: .test( + token: "access-token", + expiryDate: Date(timeIntervalSince1970: 100) + ), + refreshToken: .test() + ) + ) + + var gotRequest: Request! + + given(dateService) + .now() + .willReturn(Date(timeIntervalSince1970: 0)) + + // When + let gotResponse = try await subject.intercept( + request, + baseURL: url, + operationID: "123" + ) { request, _ in + gotRequest = request + return response + } + + // Then + XCTAssertEqual(gotResponse, response) + XCTAssertEqual( + gotRequest.headerFields, + [ + HeaderField( + name: "Authorization", value: "Bearer access-token" + ), + ] + ) + } + + func test_when_access_token_is_expired() async throws { + let url = URL(string: "https://test.tuist.io")! + let request = Request(path: "/", method: .get) + let response = Response( + statusCode: 200 + ) + + given(serverAuthenticationController) + .authenticationToken(serverURL: .any) + .willReturn( + .user( + legacyToken: nil, + accessToken: .test( + token: "access-token", + expiryDate: Date(timeIntervalSince1970: 100) + ), + refreshToken: .test( + token: "refresh-token", + expiryDate: Date(timeIntervalSince1970: 1000) + ) + ) + ) + + var gotRequest: Request! + + given(dateService) + .now() + .willReturn(Date(timeIntervalSince1970: 90)) + + given(refreshAuthTokenService) + .refreshTokens(serverURL: .any, refreshToken: .value("refresh-token")) + .willReturn( + ServerAuthenticationTokens( + accessToken: "new-access-token", + refreshToken: "new-refresh-token" + ) + ) + + given(serverCredentialsStore) + .store(credentials: .any, serverURL: .any) + .willReturn() + + // When + let gotResponse = try await subject.intercept( + request, + baseURL: url, + operationID: "123" + ) { request, _ in + gotRequest = request + return response + } + + // Then + verify(serverCredentialsStore) + .store( + credentials: .value( + ServerCredentials( + token: nil, + accessToken: "new-access-token", + refreshToken: "new-refresh-token" + ) + ), + serverURL: .any + ) + .called(1) + + XCTAssertEqual(gotResponse, response) + XCTAssertEqual( + gotRequest.headerFields, + [ + HeaderField( + name: "Authorization", value: "Bearer new-access-token" + ), + ] + ) + } +} diff --git a/Tests/TuistServerTests/Client/ServerClientOutputWarningsMiddlewareTests.swift b/Tests/TuistServerTests/Client/ServerClientOutputWarningsMiddlewareTests.swift new file mode 100644 index 00000000000..fb73e690d15 --- /dev/null +++ b/Tests/TuistServerTests/Client/ServerClientOutputWarningsMiddlewareTests.swift @@ -0,0 +1,73 @@ +import Foundation +import OpenAPIRuntime +import TuistSupport +import XCTest +@testable import TuistServer +@testable import TuistSupportTesting + +private class MockWarningController: WarningControlling { + var warnings: [String] = [] + func append(warning: String) { + warnings.append(warning) + } + + func flush() { + warnings.removeAll() + } +} + +final class ServerClientOutputWarningsMiddlewareTests: TuistUnitTestCase { + fileprivate var warningController: MockWarningController! + var subject: ServerClientOutputWarningsMiddleware! + + override func setUp() { + super.setUp() + warningController = MockWarningController() + subject = ServerClientOutputWarningsMiddleware(warningController: warningController) + } + + override func tearDown() { + warningController = nil + subject = nil + super.tearDown() + } + + func test_outputsWarnings_whenTheHeaderIsPresent() async throws { + // Given + let url = URL(string: "https://test.tuist.io")! + let warnings = ["foo", "bar"] + let base64edJsonWarnings = (try JSONSerialization.data(withJSONObject: warnings)).base64EncodedString() + let request = Request(path: "/", method: .get) + let response = Response( + statusCode: 200, + headerFields: [.init(name: "x-tuist-cloud-warnings", value: base64edJsonWarnings)] + ) + + // When + let gotResponse = try await subject.intercept(request, baseURL: url, operationID: "123") { _, _ in + response + } + + // Then + XCTAssertEqual(gotResponse, response) + for warning in warnings { + XCTAssertStandardOutput(pattern: warning) + } + } + + func test_doesntOutputAnyWarning_whenTheHeaderIsAbsent() async throws { + // Given + let url = URL(string: "https://test.tuist.io")! + let request = Request(path: "/", method: .get) + let response = Response(statusCode: 200) + + // When + let gotResponse = try await subject.intercept(request, baseURL: url, operationID: "123") { _, _ in + response + } + + // Then + XCTAssertEqual(gotResponse, response) + XCTAssertEqual(TestingLogHandler.collected[.warning, <=], "") + } +} diff --git a/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift b/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift new file mode 100644 index 00000000000..8ec50c2bcfd --- /dev/null +++ b/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift @@ -0,0 +1,165 @@ +import Foundation +import MockableTest +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistServer + +final class AnalyticsArtifactUploadServiceTests: TuistTestCase { + private var subject: AnalyticsArtifactUploadService! + private var xcresultToolController: MockXCResultToolControlling! + private var fileArchiverFactory: MockFileArchivingFactorying! + private var multipartUploadStartAnalyticsService: MockMultipartUploadStartAnalyticsServicing! + private var multipartUploadGenerateURLAnalyticsService: MockMultipartUploadGenerateURLAnalyticsServicing! + private var multipartUploadArtifactService: MockMultipartUploadArtifactServicing! + private var multipartUploadCompleteAnalyticsService: MockMultipartUploadCompleteAnalyticsServicing! + private var completeAnalyticsArtifactsUploadsService: MockCompleteAnalyticsArtifactsUploadsServicing! + + override func setUp() { + super.setUp() + + xcresultToolController = .init() + fileArchiverFactory = .init() + multipartUploadStartAnalyticsService = .init() + multipartUploadGenerateURLAnalyticsService = .init() + multipartUploadArtifactService = .init() + multipartUploadCompleteAnalyticsService = .init() + completeAnalyticsArtifactsUploadsService = .init() + + subject = AnalyticsArtifactUploadService( + fileHandler: fileHandler, + xcresultToolController: xcresultToolController, + fileArchiver: fileArchiverFactory, + retryProvider: RetryProvider(), + multipartUploadStartAnalyticsService: multipartUploadStartAnalyticsService, + multipartUploadGenerateURLAnalyticsService: multipartUploadGenerateURLAnalyticsService, + multipartUploadArtifactService: multipartUploadArtifactService, + multipartUploadCompleteAnalyticsService: multipartUploadCompleteAnalyticsService, + completeAnalyticsArtifactsUploadsService: completeAnalyticsArtifactsUploadsService + ) + } + + override func tearDown() { + fileArchiverFactory = nil + multipartUploadStartAnalyticsService = nil + multipartUploadGenerateURLAnalyticsService = nil + multipartUploadArtifactService = nil + multipartUploadCompleteAnalyticsService = nil + completeAnalyticsArtifactsUploadsService = nil + + super.tearDown() + } + + func test_upload_analytics_artifact() async throws { + // Given + let temporaryDirectory = try temporaryPath() + let resultBundle = temporaryDirectory.appending(component: "artifact.bundle") + try FileHandler.shared.touch(resultBundle) + + let serverURL: URL = .test() + + let fileArchiver = MockFileArchiving() + given(fileArchiverFactory) + .makeFileArchiver(for: .value([resultBundle])) + .willReturn(fileArchiver) + + let artifactArchivePath = temporaryDirectory.appending(component: "artifact.zip") + + given(fileArchiver) + .zip(name: .value("artifact")) + .willReturn(artifactArchivePath) + + given(multipartUploadStartAnalyticsService) + .uploadAnalyticsArtifact( + .value(.init(type: .resultBundle)), + commandEventId: .value(1), + serverURL: .value(serverURL) + ) + .willReturn("upload-id") + + given(multipartUploadArtifactService) + .multipartUploadArtifact( + artifactPath: .value(artifactArchivePath), + generateUploadURL: .any + ) + .willReturn([(etag: "etag", partNumber: 1)]) + + given(multipartUploadCompleteAnalyticsService) + .uploadAnalyticsArtifact( + .any, + commandEventId: .value(1), + uploadId: .value("upload-id"), + parts: .matching { parts in + parts.map(\.etag) == ["etag"] && parts.map(\.partNumber) == [1] + }, + serverURL: .value(serverURL) + ) + .willReturn(()) + + given(multipartUploadStartAnalyticsService) + .uploadAnalyticsArtifact( + .value(.init(type: .invocationRecord)), + commandEventId: .value(1), + serverURL: .value(serverURL) + ) + .willReturn("upload-id") + + given(multipartUploadArtifactService) + .multipartUploadArtifact( + artifactPath: .value(resultBundle.parentDirectory.appending(component: "invocation_record.json")), + generateUploadURL: .any + ) + .willReturn([(etag: "etag", partNumber: 1)]) + + given(xcresultToolController) + .resultBundleObject(.value(resultBundle)) + .willReturn(invocationRecordMockString) + + let testResultBundleObjectId = + "0~8YCjlb3k7BnCmnR4_ZFg18UnVp4hOkZw8KiWEX8RunY_pe9wBaIbW90Jo1HePHl7st5Le_nRyAP4_dSvsdxYpw==" + + given(xcresultToolController) + .resultBundleObject(.value(resultBundle), id: .value(testResultBundleObjectId)) + .willReturn("{}") + + given(multipartUploadStartAnalyticsService) + .uploadAnalyticsArtifact( + .value(.init(type: .resultBundleObject, name: testResultBundleObjectId)), + commandEventId: .value(1), + serverURL: .value(serverURL) + ) + .willReturn("upload-id") + + given(completeAnalyticsArtifactsUploadsService) + .completeAnalyticsArtifactsUploads( + modules: .any, + commandEventId: .any, + serverURL: .value(serverURL) + ) + .willReturn() + + given(multipartUploadArtifactService) + .multipartUploadArtifact( + artifactPath: .value(resultBundle.parentDirectory.appending(component: "\(testResultBundleObjectId).json")), + generateUploadURL: .any + ) + .willReturn([(etag: "etag", partNumber: 1)]) + + // When / Then + try await subject.uploadResultBundle( + resultBundle, + targetHashes: [ + GraphTarget( + path: try temporaryPath(), + target: .test(), + project: .test() + ): "target-hash", + ], + graphPath: try temporaryPath(), + commandEventId: 1, + serverURL: serverURL + ) + } +} diff --git a/Tests/TuistServerTests/Services/PreviewsUploadServiceTests.swift b/Tests/TuistServerTests/Services/PreviewsUploadServiceTests.swift new file mode 100644 index 00000000000..891137131a2 --- /dev/null +++ b/Tests/TuistServerTests/Services/PreviewsUploadServiceTests.swift @@ -0,0 +1,106 @@ +import Foundation +import MockableTest +import TuistSupport +import TuistSupportTesting +import XcodeGraph +import XCTest + +@testable import TuistServer + +final class PreviewsUploadServiceTests: TuistTestCase { + private var subject: PreviewsUploadService! + + private var fileArchiverFactory: MockFileArchivingFactorying! + private var multipartUploadStartPreviewsService: MockMultipartUploadStartPreviewsServicing! + private var multipartUploadGenerateURLPreviewsService: MockMultipartUploadGenerateURLPreviewsServicing! + private var multipartUploadArtifactService: MockMultipartUploadArtifactServicing! + private var multipartUploadCompletePreviewsService: MockMultipartUploadCompletePreviewsServicing! + + override func setUp() { + super.setUp() + + fileArchiverFactory = .init() + multipartUploadStartPreviewsService = .init() + multipartUploadGenerateURLPreviewsService = .init() + multipartUploadArtifactService = .init() + multipartUploadCompletePreviewsService = .init() + + subject = PreviewsUploadService( + fileHandler: fileHandler, + fileArchiver: fileArchiverFactory, + retryProvider: RetryProvider(), + multipartUploadStartPreviewsService: multipartUploadStartPreviewsService, + multipartUploadGenerateURLPreviewsService: multipartUploadGenerateURLPreviewsService, + multipartUploadArtifactService: multipartUploadArtifactService, + multipartUploadCompletePreviewsService: multipartUploadCompletePreviewsService + ) + } + + override func tearDown() { + fileArchiverFactory = nil + multipartUploadStartPreviewsService = nil + multipartUploadGenerateURLPreviewsService = nil + multipartUploadArtifactService = nil + multipartUploadCompletePreviewsService = nil + + super.tearDown() + } + + func test_upload_app_builds() async throws { + // Given + let preview = try temporaryPath().appending(component: "App.app") + try FileHandler.shared.touch(preview) + + let serverURL: URL = .test() + + let fileArchiver = MockFileArchiving() + given(fileArchiverFactory) + .makeFileArchiver(for: .value([preview])) + .willReturn(fileArchiver) + + let artifactArchivePath = preview.parentDirectory.appending(component: "previews.zip") + + given(fileArchiver) + .zip(name: .value("previews.zip")) + .willReturn(artifactArchivePath) + + given(multipartUploadStartPreviewsService) + .startPreviewsMultipartUpload( + fullHandle: .value("tuist/tuist"), + serverURL: .value(serverURL) + ) + .willReturn( + PreviewUpload(previewId: "preview-id", uploadId: "upload-id") + ) + + given(multipartUploadArtifactService) + .multipartUploadArtifact( + artifactPath: .value(artifactArchivePath), + generateUploadURL: .any + ) + .willReturn([(etag: "etag", partNumber: 1)]) + + let shareURL = URL.test() + given(multipartUploadCompletePreviewsService) + .completePreviewUpload( + .value("preview-id"), + uploadId: .value("upload-id"), + parts: .matching { parts in + parts.map(\.etag) == ["etag"] && parts.map(\.partNumber) == [1] + }, + fullHandle: .value("tuist/tuist"), + serverURL: .value(serverURL) + ) + .willReturn(shareURL) + + // When + let got = try await subject.uploadPreviews( + [preview], + fullHandle: "tuist/tuist", + serverURL: serverURL + ) + + // Then + XCTAssertEqual(got, shareURL) + } +} diff --git a/Tests/TuistServerTests/Session/ServerSessionControllerTests.swift b/Tests/TuistServerTests/Session/ServerSessionControllerTests.swift new file mode 100644 index 00000000000..e1532221986 --- /dev/null +++ b/Tests/TuistServerTests/Session/ServerSessionControllerTests.swift @@ -0,0 +1,200 @@ +import Foundation +import Mockable +import MockableTest +import Path +import TuistSupport +import XCTest + +@testable import TuistServer +@testable import TuistSupportTesting + +final class ServerSessionControllerTests: TuistUnitTestCase { + private var credentialsStore: MockServerCredentialsStoring! + private var ciChecker: MockCIChecking! + private var opener: MockOpening! + private var serverURL: URL! + private var getAuthTokenService: MockGetAuthTokenServicing! + private var uniqueIDGenerator: MockUniqueIDGenerating! + private var serverAuthenticationController: MockServerAuthenticationControlling! + private var subject: ServerSessionController! + + override func setUp() { + super.setUp() + credentialsStore = .init() + ciChecker = .init() + opener = MockOpening() + serverURL = URL.test() + getAuthTokenService = MockGetAuthTokenServicing() + uniqueIDGenerator = MockUniqueIDGenerating() + serverAuthenticationController = MockServerAuthenticationControlling() + subject = ServerSessionController( + credentialsStore: credentialsStore, + ciChecker: ciChecker, + opener: opener, + getAuthTokenService: getAuthTokenService, + uniqueIDGenerator: uniqueIDGenerator, + serverAuthenticationController: serverAuthenticationController + ) + + given(opener) + .open(url: .any) + .willReturn() + } + + override func tearDown() { + credentialsStore = nil + ciChecker = nil + opener = nil + serverURL = nil + uniqueIDGenerator = nil + serverAuthenticationController = nil + subject = nil + super.tearDown() + } + + func test_authenticate_when_tokenAndAccountParametersAreIncluded() async throws { + // Given + given(getAuthTokenService) + .getAuthToken(serverURL: .any, deviceCode: .any) + .willReturn(ServerAuthenticationTokens(accessToken: "access-token", refreshToken: "refresh-token")) + given(uniqueIDGenerator).uniqueID().willReturn("id") + given(credentialsStore) + .read(serverURL: .value(serverURL)) + .willReturn( + ServerCredentials(token: nil, accessToken: "access-token", refreshToken: "refresh-token") + ) + given(credentialsStore) + .store(credentials: .any, serverURL: .value(serverURL)) + .willReturn() + + // When + try await subject.authenticate(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + Opening \(authURL().absoluteString) to start the authentication flow + Press CTRL + C once to cancel the process. + Credentials stored successfully + """) + } + + func test_printSession_when_legacyUserToken() throws { + // When + given(serverAuthenticationController) + .authenticationToken(serverURL: .value(serverURL)) + .willReturn( + .user(legacyToken: "legacy-token", accessToken: nil, refreshToken: nil) + ) + try subject.printSession(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + Requests against \(serverURL.absoluteString) will be authenticated as a user using the following token: + legacy-token + """) + } + + func test_printSession_when_userToken() throws { + // When + given(serverAuthenticationController) + .authenticationToken(serverURL: .value(serverURL)) + .willReturn( + .user( + legacyToken: nil, + accessToken: .test(token: "access-token"), + refreshToken: .test(token: "refresh-token") + ) + ) + try subject.printSession(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + Requests against \(serverURL.absoluteString) will be authenticated as a user using the following token: + access-token + """) + } + + func test_printSession_when_projectToken() throws { + // When + given(serverAuthenticationController).authenticationToken(serverURL: .value(serverURL)).willReturn(.project("token")) + try subject.printSession(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + Requests against \(serverURL.absoluteString) will be authenticated as a project using the following token: + token + """) + } + + func test_printSession_when_credentialsDontExist() throws { + // Given + given(serverAuthenticationController).authenticationToken(serverURL: .value(serverURL)).willReturn(nil) + + // When + try subject.printSession(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + There are no sessions for the server with URL \(serverURL.absoluteString) + """) + } + + func test_logout_deletesLegacyCredentials() async throws { + // Given + let credentials = ServerCredentials( + token: "token", + accessToken: nil, + refreshToken: nil + ) + given(credentialsStore) + .store(credentials: .value(credentials), serverURL: .value(serverURL)) + .willReturn() + try credentialsStore.store(credentials: credentials, serverURL: serverURL) + + given(credentialsStore) + .delete(serverURL: .value(serverURL)) + .willReturn() + + // When + try await subject.logout(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + Removing session for server with URL \(serverURL.absoluteString) + Session deleted successfully + """) + } + + func test_logout_deletesCredentials() async throws { + // Given + let credentials = ServerCredentials( + token: nil, + accessToken: "access-token", + refreshToken: "refresh-token" + ) + given(credentialsStore) + .store(credentials: .value(credentials), serverURL: .value(serverURL)) + .willReturn() + try credentialsStore.store(credentials: credentials, serverURL: serverURL) + + given(credentialsStore) + .delete(serverURL: .value(serverURL)) + .willReturn() + + // When + try await subject.logout(serverURL: serverURL) + + // Then + XCTAssertPrinterOutputContains(""" + Removing session for server with URL \(serverURL.absoluteString) + Session deleted successfully + """) + } + + fileprivate func authURL() -> URL { + var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)! + components.path = "/auth/cli/\(uniqueIDGenerator.uniqueID())" + components.queryItems = nil + return components.url! + } +} diff --git a/Tests/TuistServerTests/TestData/InvocationRecord+TestData.swift b/Tests/TuistServerTests/TestData/InvocationRecord+TestData.swift new file mode 100644 index 00000000000..07b24e1d411 --- /dev/null +++ b/Tests/TuistServerTests/TestData/InvocationRecord+TestData.swift @@ -0,0 +1,635 @@ +import Foundation + +extension AnalyticsArtifactUploadServiceTests { + var invocationRecordMockString: String { + #""" + { + "_type" : { + "_name" : "ActionsInvocationRecord" + }, + "actions" : { + "_type" : { + "_name" : "Array" + }, + "_values" : [ + { + "_type" : { + "_name" : "ActionRecord" + }, + "actionResult" : { + "_type" : { + "_name" : "ActionResult" + }, + "coverage" : { + "_type" : { + "_name" : "CodeCoverageInfo" + } + }, + "diagnosticsRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~OswFMxdHsqFpfWmB3LFdBaMZLu-tXkrmvBsNwQ0BKaT9taUvjiIfOuMGRBR-MawOmNo0LnXLkdHQm5N67mVz1w==" + } + }, + "issues" : { + "_type" : { + "_name" : "ResultIssueSummaries" + }, + "testFailureSummaries" : { + "_type" : { + "_name" : "Array" + }, + "_values" : [ + { + "_type" : { + "_name" : "TestFailureIssueSummary", + "_supertype" : { + "_name" : "IssueSummary" + } + }, + "documentLocationInCreatingWorkspace" : { + "_type" : { + "_name" : "DocumentLocation" + }, + "concreteTypeName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "DVTTextDocumentLocation" + }, + "url" : { + "_type" : { + "_name" : "String" + }, + "_value" : "file:\/\/\/Users\/builder\/clone\/Tests\/TuistDependenciesAcceptanceTests\/DependenciesAcceptanceTests.swift#EndingLineNumber=40&StartingLineNumber=40" + } + }, + "issueType" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Uncategorized" + }, + "message" : { + "_type" : { + "_name" : "String" + }, + "_value" : "failed: caught error: \"The 'xcodebuild' command exited with error code 65 and message:\n2024-05-20 12:44:17.788570+0000 xcodebuild[14114:50965] [devicemanager] DeviceManager sending check-in request: F988FF5A-CDAC-42C9-9C3F-BE87134AD171\n2024-05-20 12:44:17.790440+0000 xcodebuild[14114:50965] [devicemanager] DeviceManager check-in (F988FF5A-CDAC-42C9-9C3F-BE87134AD171) completed successfully\n2024-05-20 12:44:17.793515+0000 xcodebuild[14114:50961] [All] MobileDevice.framework version: 1643.100.58\n2024-05-20 12:44:17.798111+0000 xcodebuild[14114:50961] [All] RemotePairing.framework version: 117.100.41\n2024-05-20 12:44:17.798690+0000 xcodebuild[14114:50961] [library] USBMuxListenerCreateFiltered:898 Created 0x600003581f40\n2024-05-20 12:44:17.798774+0000 xcodebuild[14114:50961] [All] Subscribed for device notifications from usbmuxd.\n2024-05-20 12:44:18.054503+0000 xcodebuild[14114:50949] [general] initializing workspace at path \/var\/folders\/w2\/rrf5p87d1bbfyphxc7jdnyvh0000gn\/T\/TemporaryDirectory.AMHRiB\/ios_app_with_spm_dependencies\/App.xcworkspace\n2024-05-20 12:44:18.055591+0000 xcodebuild[14114:50949] [general] setting up workspace \/var\/folders\/w2\/rrf5p87d1bbfyphxc7jdnyvh0000gn\/T\/TemporaryDirectory.AMHRiB\/ios_app_with_spm_dependencies\/App.xcworkspace\n2024-05-20 12:44:18.071588+0000 xcodebuild[14114:50949] [general] using plugin library at \/Applications\/Xcode-15.3.app\/Contents\/SharedFrameworks\/SwiftPM.framework\/SharedSupport\/PluginAPI\n2024-05-20 12:44:18.071680+0000 xcodebuild[14114:50949] [general] synchronizing contents of workspace at path \/var\/folders\/w2\/rrf5p87d1bbfyphxc7jdnyvh0000gn\/T\/TemporaryDirectory.AMHRiB\/ios_app_with_spm_dependencies\/App.xcworkspace\n2024-05-20 12:44:18.071698+0000 xcodebuild[14114:50949] [general] marking workspace as having finished initial package resolution\n--- xcodebuild: WARNING: Using the first of multiple matching destinations:\n{ platform:iOS Simulator, id:2B6F50E6-2782-405A-94DF-E659C9398717, OS:17.4, name:iPad Pro (12.9-inch) (6th generation) }\n{ platform:iOS Simulator, id:2B6F50E6-2782-405A-94DF-E659C9398717, OS:17.4, name:iPad Pro (12.9-inch) (6th generation) }\n2024-05-20 12:44:18.106264+0000 xcodebuild[14114:50949] [building] creating scheme operation preamble operations for build command Build\nTesting failed:\n\tCompiling for iOS 14.0, but module 'App' has a minimum deployment target of iOS 16.0: \/Users\/builder\/Library\/Developer\/Xcode\/DerivedData\/App-fzcodvpjdyxljhcxfidulaohhxre\/Build\/Products\/Debug-iphonesimulator\/App.swiftmodule\/arm64-apple-ios-simulator.swiftmodule\n\tTesting cancelled because the build failed.\n\n** TEST FAILED **\n\n\nThe following build commands failed:\n\tSwiftEmitModule normal arm64 Emitting\\ module\\ for\\ AppTests (in target 'AppTests' from project 'App')\n(1 failure)\n\"" + }, + "testCaseName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "DependenciesAcceptanceTestIosAppWithSPMDependencies.test_ios_app_spm_dependencies()" + } + } + ] + } + }, + "logRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~P3EqtGf1QBDVW81JX_KQylSrGzn-HJAEOm1bhpBdwiiWhpaRIfCbhxtixsVOOLHp6jGB-YAVUqA1rmIc7dBy4g==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActivityLogSection" + } + } + }, + "metrics" : { + "_type" : { + "_name" : "ResultMetrics" + }, + "testsCount" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "3" + }, + "testsFailedCount" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "1" + } + }, + "resultName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "action" + }, + "status" : { + "_type" : { + "_name" : "String" + }, + "_value" : "failed" + }, + "testsRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~8YCjlb3k7BnCmnR4_ZFg18UnVp4hOkZw8KiWEX8RunY_pe9wBaIbW90Jo1HePHl7st5Le_nRyAP4_dSvsdxYpw==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActionTestPlanRunSummaries" + } + } + } + }, + "buildResult" : { + "_type" : { + "_name" : "ActionResult" + }, + "coverage" : { + "_type" : { + "_name" : "CodeCoverageInfo" + } + }, + "issues" : { + "_type" : { + "_name" : "ResultIssueSummaries" + } + }, + "logRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~JlTezqV63xbUrp0gsF4KV89HvbbNAfQ_g5JOvwR1ecy70PjnlqcfDdyHgggMuhSwJYY9MSoVtKYHzmxj3ZaRcw==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActivityLogSection" + } + } + }, + "metrics" : { + "_type" : { + "_name" : "ResultMetrics" + } + }, + "resultName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "build" + }, + "status" : { + "_type" : { + "_name" : "String" + }, + "_value" : "succeeded" + } + }, + "endedTime" : { + "_type" : { + "_name" : "Date" + }, + "_value" : "2024-05-20T12:44:51.162+0000" + }, + "runDestination" : { + "_type" : { + "_name" : "ActionRunDestinationRecord" + }, + "displayName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "My Mac" + }, + "localComputerRecord" : { + "_type" : { + "_name" : "ActionDeviceRecord" + }, + "busSpeedInMHz" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "0" + }, + "cpuCount" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "1" + }, + "cpuKind" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Apple M1 (Virtual)" + }, + "cpuSpeedInMHz" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "0" + }, + "identifier" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0000FE00-343A79319AA17942" + }, + "isConcreteDevice" : { + "_type" : { + "_name" : "Bool" + }, + "_value" : "true" + }, + "logicalCPUCoresPerPackage" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "4" + }, + "modelCode" : { + "_type" : { + "_name" : "String" + }, + "_value" : "VirtualMac2,1" + }, + "modelName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Apple Virtual Machine 1" + }, + "modelUTI" : { + "_type" : { + "_name" : "String" + }, + "_value" : "com.apple.virtual-machine" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "My Mac" + }, + "nativeArchitecture" : { + "_type" : { + "_name" : "String" + }, + "_value" : "arm64e" + }, + "operatingSystemVersion" : { + "_type" : { + "_name" : "String" + }, + "_value" : "14.3.1" + }, + "operatingSystemVersionWithBuildNumber" : { + "_type" : { + "_name" : "String" + }, + "_value" : "14.3.1 (23D60)" + }, + "physicalCPUCoresPerPackage" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "4" + }, + "platformRecord" : { + "_type" : { + "_name" : "ActionPlatformRecord" + }, + "identifier" : { + "_type" : { + "_name" : "String" + }, + "_value" : "com.apple.platform.macosx" + }, + "userDescription" : { + "_type" : { + "_name" : "String" + }, + "_value" : "macOS" + } + }, + "ramSizeInMegabytes" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "8192" + } + }, + "targetArchitecture" : { + "_type" : { + "_name" : "String" + }, + "_value" : "arm64" + }, + "targetDeviceRecord" : { + "_type" : { + "_name" : "ActionDeviceRecord" + }, + "busSpeedInMHz" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "0" + }, + "cpuCount" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "1" + }, + "cpuKind" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Apple M1 (Virtual)" + }, + "cpuSpeedInMHz" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "0" + }, + "identifier" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0000FE00-343A79319AA17942" + }, + "isConcreteDevice" : { + "_type" : { + "_name" : "Bool" + }, + "_value" : "true" + }, + "logicalCPUCoresPerPackage" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "4" + }, + "modelCode" : { + "_type" : { + "_name" : "String" + }, + "_value" : "VirtualMac2,1" + }, + "modelName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Apple Virtual Machine 1" + }, + "modelUTI" : { + "_type" : { + "_name" : "String" + }, + "_value" : "com.apple.virtual-machine" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "My Mac" + }, + "nativeArchitecture" : { + "_type" : { + "_name" : "String" + }, + "_value" : "arm64e" + }, + "operatingSystemVersion" : { + "_type" : { + "_name" : "String" + }, + "_value" : "14.3.1" + }, + "operatingSystemVersionWithBuildNumber" : { + "_type" : { + "_name" : "String" + }, + "_value" : "14.3.1 (23D60)" + }, + "physicalCPUCoresPerPackage" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "4" + }, + "platformRecord" : { + "_type" : { + "_name" : "ActionPlatformRecord" + }, + "identifier" : { + "_type" : { + "_name" : "String" + }, + "_value" : "com.apple.platform.macosx" + }, + "userDescription" : { + "_type" : { + "_name" : "String" + }, + "_value" : "macOS" + } + }, + "ramSizeInMegabytes" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "8192" + } + }, + "targetSDKRecord" : { + "_type" : { + "_name" : "ActionSDKRecord" + }, + "identifier" : { + "_type" : { + "_name" : "String" + }, + "_value" : "macosx14.4" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "macOS 14.4" + }, + "operatingSystemVersion" : { + "_type" : { + "_name" : "String" + }, + "_value" : "14.4" + } + } + }, + "schemeCommandName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Test" + }, + "schemeTaskName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "BuildAndAction" + }, + "startedTime" : { + "_type" : { + "_name" : "Date" + }, + "_value" : "2024-05-20T12:34:11.228+0000" + }, + "testPlanName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "TuistDependenciesAcceptanceTests" + }, + "title" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Testing workspace Tuist with scheme TuistDependenciesAcceptanceTests" + } + } + ] + }, + "issues" : { + "_type" : { + "_name" : "ResultIssueSummaries" + }, + "testFailureSummaries" : { + "_type" : { + "_name" : "Array" + }, + "_values" : [ + { + "_type" : { + "_name" : "TestFailureIssueSummary", + "_supertype" : { + "_name" : "IssueSummary" + } + }, + "documentLocationInCreatingWorkspace" : { + "_type" : { + "_name" : "DocumentLocation" + }, + "concreteTypeName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "DVTTextDocumentLocation" + }, + "url" : { + "_type" : { + "_name" : "String" + }, + "_value" : "file:\/\/\/Users\/builder\/clone\/Tests\/TuistDependenciesAcceptanceTests\/DependenciesAcceptanceTests.swift#EndingLineNumber=40&StartingLineNumber=40" + } + }, + "issueType" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Uncategorized" + }, + "message" : { + "_type" : { + "_name" : "String" + }, + "_value" : "failed: caught error: \"The 'xcodebuild' command exited with error code 65 and message:\n2024-05-20 12:44:17.788570+0000 xcodebuild[14114:50965] [devicemanager] DeviceManager sending check-in request: F988FF5A-CDAC-42C9-9C3F-BE87134AD171\n2024-05-20 12:44:17.790440+0000 xcodebuild[14114:50965] [devicemanager] DeviceManager check-in (F988FF5A-CDAC-42C9-9C3F-BE87134AD171) completed successfully\n2024-05-20 12:44:17.793515+0000 xcodebuild[14114:50961] [All] MobileDevice.framework version: 1643.100.58\n2024-05-20 12:44:17.798111+0000 xcodebuild[14114:50961] [All] RemotePairing.framework version: 117.100.41\n2024-05-20 12:44:17.798690+0000 xcodebuild[14114:50961] [library] USBMuxListenerCreateFiltered:898 Created 0x600003581f40\n2024-05-20 12:44:17.798774+0000 xcodebuild[14114:50961] [All] Subscribed for device notifications from usbmuxd.\n2024-05-20 12:44:18.054503+0000 xcodebuild[14114:50949] [general] initializing workspace at path \/var\/folders\/w2\/rrf5p87d1bbfyphxc7jdnyvh0000gn\/T\/TemporaryDirectory.AMHRiB\/ios_app_with_spm_dependencies\/App.xcworkspace\n2024-05-20 12:44:18.055591+0000 xcodebuild[14114:50949] [general] setting up workspace \/var\/folders\/w2\/rrf5p87d1bbfyphxc7jdnyvh0000gn\/T\/TemporaryDirectory.AMHRiB\/ios_app_with_spm_dependencies\/App.xcworkspace\n2024-05-20 12:44:18.071588+0000 xcodebuild[14114:50949] [general] using plugin library at \/Applications\/Xcode-15.3.app\/Contents\/SharedFrameworks\/SwiftPM.framework\/SharedSupport\/PluginAPI\n2024-05-20 12:44:18.071680+0000 xcodebuild[14114:50949] [general] synchronizing contents of workspace at path \/var\/folders\/w2\/rrf5p87d1bbfyphxc7jdnyvh0000gn\/T\/TemporaryDirectory.AMHRiB\/ios_app_with_spm_dependencies\/App.xcworkspace\n2024-05-20 12:44:18.071698+0000 xcodebuild[14114:50949] [general] marking workspace as having finished initial package resolution\n--- xcodebuild: WARNING: Using the first of multiple matching destinations:\n{ platform:iOS Simulator, id:2B6F50E6-2782-405A-94DF-E659C9398717, OS:17.4, name:iPad Pro (12.9-inch) (6th generation) }\n{ platform:iOS Simulator, id:2B6F50E6-2782-405A-94DF-E659C9398717, OS:17.4, name:iPad Pro (12.9-inch) (6th generation) }\n2024-05-20 12:44:18.106264+0000 xcodebuild[14114:50949] [building] creating scheme operation preamble operations for build command Build\nTesting failed:\n\tCompiling for iOS 14.0, but module 'App' has a minimum deployment target of iOS 16.0: \/Users\/builder\/Library\/Developer\/Xcode\/DerivedData\/App-fzcodvpjdyxljhcxfidulaohhxre\/Build\/Products\/Debug-iphonesimulator\/App.swiftmodule\/arm64-apple-ios-simulator.swiftmodule\n\tTesting cancelled because the build failed.\n\n** TEST FAILED **\n\n\nThe following build commands failed:\n\tSwiftEmitModule normal arm64 Emitting\\ module\\ for\\ AppTests (in target 'AppTests' from project 'App')\n(1 failure)\n\"" + }, + "testCaseName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "DependenciesAcceptanceTestIosAppWithSPMDependencies.test_ios_app_spm_dependencies()" + } + } + ] + } + }, + "metadataRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~VbN52oFuHDTG0EcAOQetUdZLbKa9zYQrCBFDkKY7h3D2Xi29FsjOinUPBURI9WNvHK3-dvcDm8cZNYvXbsYCHw==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActionsInvocationMetadata" + } + } + }, + "metrics" : { + "_type" : { + "_name" : "ResultMetrics" + }, + "testsCount" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "3" + }, + "testsFailedCount" : { + "_type" : { + "_name" : "Int" + }, + "_value" : "1" + } + } + } + + """# + } +} diff --git a/Tests/TuistServerTests/Utilities/DelayProviderTests.swift b/Tests/TuistServerTests/Utilities/DelayProviderTests.swift new file mode 100644 index 00000000000..289cd76cdbd --- /dev/null +++ b/Tests/TuistServerTests/Utilities/DelayProviderTests.swift @@ -0,0 +1,44 @@ +import Foundation +import TuistSupportTesting +import XCTest + +@testable import TuistServer + +final class DelayProviderTests: TuistUnitTestCase { + private var subject: DelayProviding! + + override func setUp() { + super.setUp() + + subject = DelayProvider() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_delay_for_first_retry() { + for _ in 0 ... 20 { + XCTAssertTrue( + (UInt64(0) ... UInt64(2_000_000)).contains(subject.delay(for: 0)) + ) + } + } + + func test_delay_for_second_retry() { + for _ in 0 ... 20 { + XCTAssertTrue( + (UInt64(1_000_000) ... UInt64(3_000_000)).contains(subject.delay(for: 1)) + ) + } + } + + func test_delay_for_third_retry() { + for _ in 0 ... 20 { + XCTAssertTrue( + (UInt64(3_000_000) ... UInt64(5_000_000)).contains(subject.delay(for: 2)) + ) + } + } +} diff --git a/Tests/TuistServerTests/Utilities/RetryProviderTests.swift b/Tests/TuistServerTests/Utilities/RetryProviderTests.swift new file mode 100644 index 00000000000..3b0ee2689a1 --- /dev/null +++ b/Tests/TuistServerTests/Utilities/RetryProviderTests.swift @@ -0,0 +1,70 @@ +import Foundation +import MockableTest +import TuistSupportTesting +import XCTest + +@testable import TuistServer + +final class RetryProviderTests: TuistUnitTestCase { + private var operationCalls = 0 + private var subject: RetryProviding! + + override func setUp() { + super.setUp() + + let delayProvider = MockDelayProviding() + given(delayProvider) + .delay(for: .any) + .willReturn(1) + + subject = RetryProvider( + delayProvider: delayProvider + ) + } + + override func tearDown() { + subject = nil + operationCalls = 0 + super.tearDown() + } + + func test_exists_whenSucceeds_doesNotRetry() async throws { + // Given / When + try await subject.runWithRetries { [self] in + operationCalls += 1 + } + + // Then + XCTAssertEqual(operationCalls, 1) + } + + func test_exists_whenFails_retries() async throws { + // Given / When + try await subject.runWithRetries { [self] in + operationCalls += 1 + if operationCalls < 3 { + throw TestError("exists failed") + } + } + + // Then + XCTAssertEqual(operationCalls, 3) + } + + func test_exists_whenFailsFourTimes_throws() async throws { + // Given + let error = TestError("exists failed") + + // When + await XCTAssertThrowsSpecific( + try await subject.runWithRetries { [self] in + operationCalls += 1 + throw error + }, + error + ) + + // Then + XCTAssertEqual(operationCalls, 4) + } +} diff --git a/Tests/TuistServerTests/Utilities/ServerAuthenticationControllerTests.swift b/Tests/TuistServerTests/Utilities/ServerAuthenticationControllerTests.swift new file mode 100644 index 00000000000..cf86e30ebb3 --- /dev/null +++ b/Tests/TuistServerTests/Utilities/ServerAuthenticationControllerTests.swift @@ -0,0 +1,213 @@ +import Foundation +import MockableTest +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import TuistServer + +final class ServerAuthenticationControllerTests: TuistUnitTestCase { + private var subject: ServerAuthenticationController! + private var credentialsStore: MockServerCredentialsStoring! + private var ciChecker: MockCIChecking! + + private let accessToken = + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0dWlzdF9jbG91ZCIsImV4cCI6MTcyMDQyOTgxMiwiaWF0IjoxNzIwNDI5NzUyLCJpc3MiOiJ0dWlzdF9jbG91ZCIsImp0aSI6IjlmZGEwYmRmLTE0MjMtNDhmNi1iNWRmLWM2MDVjMGMwMzBiMiIsIm5iZiI6MTcyMDQyOTc1MSwicmVzb3VyY2UiOiJ1c2VyIiwic3ViIjoiMSIsInR5cCI6ImFjY2VzcyJ9.qsxjD51lHHaQo6NWs-gUxVUhQfyWEe3v3-okM0NIV72vDY-fGgzq9JU2F8DQbdOD8POqWkseCbtO66m_4J9uFw" + private let refreshToken = + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0dWlzdF9jbG91ZCIsImV4cCI6MTcyMDQyOTgxMCwiaWF0IjoxNzIwNDI5NzUyLCJpc3MiOiJ0dWlzdF9jbG91ZCIsImp0aSI6IjlmZGEwYmRmLTE0MjMtNDhmNi1iNWRmLWM2MDVjMGMwMzBiMiIsIm5iZiI6MTcyMDQyOTc1MSwicmVzb3VyY2UiOiJ1c2VyIiwic3ViIjoiMSIsInR5cCI6ImFjY2VzcyJ9.UGMOA4nysabRCO0px9ixCW3JTCA6OgYSeVA6X--Xkc8b-YA8ui2SeCL8gV9WvOYeLJA5pvzKUSulVfV1qM4LKg" + + override func setUp() { + super.setUp() + + credentialsStore = .init() + ciChecker = .init() + subject = .init( + credentialsStore: credentialsStore, + ciChecker: ciChecker, + environment: environment + ) + } + + override func tearDown() { + credentialsStore = nil + ciChecker = nil + subject = nil + super.tearDown() + } + + func test_when_config_token_is_present_and_is_ci() throws { + // Given + environment.tuistVariables[ + Constants.EnvironmentVariables.token + ] = "project-token" + given(ciChecker) + .isCI() + .willReturn(true) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + XCTAssertEqual( + got, + .project("project-token") + ) + } + + func test_when_config_token_is_present_and_is_not_ci() throws { + // Given + environment.tuistVariables[ + Constants.EnvironmentVariables.token + ] = "project-token" + given(ciChecker) + .isCI() + .willReturn(false) + given(credentialsStore) + .read(serverURL: .any) + .willReturn(nil) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + XCTAssertNil(got) + } + + func test_when_deprecated_config_token_is_present_and_is_ci() throws { + // Given + environment.tuistVariables[ + Constants.EnvironmentVariables.deprecatedToken + ] = "project-token" + given(ciChecker) + .isCI() + .willReturn(true) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + XCTAssertEqual( + got, + .project("project-token") + ) + XCTAssertStandardOutput( + pattern: "Use `TUIST_CONFIG_TOKEN` environment variable instead of `TUIST_CONFIG_CLOUD_TOKEN` to authenticate on the CI" + ) + } + + func test_when_deprecated_and_current_config_tokens_are_present_and_is_ci() throws { + // Given + environment.tuistVariables[ + Constants.EnvironmentVariables.deprecatedToken + ] = "deprecated-project-token" + environment.tuistVariables[ + Constants.EnvironmentVariables.token + ] = "project-token" + given(ciChecker) + .isCI() + .willReturn(true) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + XCTAssertEqual( + got, + .project("project-token") + ) + XCTAssertPrinterOutputNotContains( + "Use `TUIST_CONFIG_TOKEN` environment variable instead of `TUIST_CONFIG_CLOUD_TOKEN` to authenticate on the CI" + ) + } + + func test_when_credentials_store_returns_legacy_token() throws { + // Given + given(ciChecker) + .isCI() + .willReturn(false) + + given(credentialsStore) + .read(serverURL: .any) + .willReturn(ServerCredentials(token: "legacy-token", accessToken: nil, refreshToken: nil)) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + XCTAssertEqual( + got, + .user(legacyToken: "legacy-token", accessToken: nil, refreshToken: nil) + ) + XCTAssertStandardOutput(pattern: "You are using a deprecated user token. Please, reauthenticate by running `tuist auth`.") + } + + func test_when_credentials_store_returns_legacy_token_and_jwt_tokens() throws { + // Given + given(ciChecker) + .isCI() + .willReturn(false) + + given(credentialsStore) + .read(serverURL: .any) + .willReturn(ServerCredentials(token: "legacy-token", accessToken: accessToken, refreshToken: refreshToken)) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + // Then + XCTAssertEqual( + got, + .user( + legacyToken: nil, + accessToken: JWT( + token: accessToken, + expiryDate: Date(timeIntervalSince1970: 1_720_429_812) + ), + refreshToken: JWT( + token: refreshToken, + expiryDate: Date(timeIntervalSince1970: 1_720_429_810) + ) + ) + ) + XCTAssertPrinterOutputNotContains( + "You are using a deprecated user token. Please, reauthenticate by running `tuist auth`." + ) + } + + func test_when_credentials_store_returns_jwt_tokens() throws { + // Given + given(ciChecker) + .isCI() + .willReturn(false) + + given(credentialsStore) + .read(serverURL: .any) + .willReturn( + ServerCredentials( + token: nil, + accessToken: accessToken, + refreshToken: refreshToken + ) + ) + + // When + let got = try subject.authenticationToken(serverURL: .test()) + + // Then + XCTAssertEqual( + got, + .user( + legacyToken: nil, + accessToken: JWT( + token: accessToken, + expiryDate: Date(timeIntervalSince1970: 1_720_429_812) + ), + refreshToken: JWT( + token: refreshToken, + expiryDate: Date(timeIntervalSince1970: 1_720_429_810) + ) + ) + ) + } +} diff --git a/Tests/TuistServerTests/Utilities/ServerCredentialsStoreTests.swift b/Tests/TuistServerTests/Utilities/ServerCredentialsStoreTests.swift new file mode 100644 index 00000000000..27192e0c23a --- /dev/null +++ b/Tests/TuistServerTests/Utilities/ServerCredentialsStoreTests.swift @@ -0,0 +1,53 @@ +import Foundation +import TuistSupport +import XCTest + +@testable import TuistServer +@testable import TuistSupportTesting + +final class ServerCredentialsStoreTests: TuistUnitTestCase { + var subject: ServerCredentialsStore! + + override func setUp() { + super.setUp() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_crud_with_legacy_token() async throws { + // Given + let temporaryDirectory = try temporaryPath() + let subject = ServerCredentialsStore( + fileHandler: FileHandler.shared, + configDirectory: temporaryDirectory + ) + let credentials = ServerCredentials(token: "token", accessToken: nil, refreshToken: nil) + let serverURL = URL(string: "https://tuist.io")! + + // When/Then + try subject.store(credentials: credentials, serverURL: serverURL) + XCTAssertEqual(try subject.read(serverURL: serverURL), credentials) + try await subject.delete(serverURL: serverURL) + XCTAssertEqual(try subject.read(serverURL: serverURL), nil) + } + + func test_crud() async throws { + // Given + let temporaryDirectory = try temporaryPath() + let subject = ServerCredentialsStore( + fileHandler: FileHandler.shared, + configDirectory: temporaryDirectory + ) + let credentials = ServerCredentials(token: nil, accessToken: "access-token", refreshToken: "refresh-token") + let serverURL = URL(string: "https://tuist.io")! + + // When/Then + try subject.store(credentials: credentials, serverURL: serverURL) + XCTAssertEqual(try subject.read(serverURL: serverURL), credentials) + try await subject.delete(serverURL: serverURL) + XCTAssertEqual(try subject.read(serverURL: serverURL), nil) + } +} diff --git a/Tests/TuistSigningTests/CertificateParserTests.swift b/Tests/TuistSigningTests/CertificateParserTests.swift deleted file mode 100644 index 9882c26efff..00000000000 --- a/Tests/TuistSigningTests/CertificateParserTests.swift +++ /dev/null @@ -1,138 +0,0 @@ -import TSCBasic -import XCTest -@testable import TuistSigning -@testable import TuistSupportTesting - -final class CertificateParserTests: TuistUnitTestCase { - var subject: CertificateParser! - - override func setUp() { - super.setUp() - - subject = CertificateParser() - } - - override func tearDown() { - subject = nil - super.tearDown() - } - - func test_name_parsing_fails_when_not_present() throws { - // Given - let publicKey = try temporaryPath().appending(component: "Target.Debug.p12") - let privateKey = try temporaryPath() - let subjectOutput = "subject= /UID=VD55TKL3V6/OU=QH95ER52SG/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject"], - output: subjectOutput - ) - let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint"], - output: fingerprintOutput - ) - - // When - XCTAssertThrowsSpecific( - try subject.parse(publicKey: publicKey, privateKey: privateKey), - CertificateParserError.nameParsingFailed(publicKey, subjectOutput) - ) - } - - func test_development_team_fails_when_not_present() throws { - // Given - let publicKey = try temporaryPath().appending(component: "Target.Debug.p12") - let privateKey = try temporaryPath() - let subjectOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject"], - output: subjectOutput - ) - let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint"], - output: fingerprintOutput - ) - - // When - XCTAssertThrowsSpecific( - try subject.parse(publicKey: publicKey, privateKey: privateKey), - CertificateParserError.developmentTeamParsingFailed(publicKey, subjectOutput) - ) - } - - func test_parsing_succeeds() throws { - // Given - let publicKey = try temporaryPath().appending(component: "Target.Debug.p12") - let privateKey = try temporaryPath() - let subjectOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject"], - output: subjectOutput - ) - let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint"], - output: fingerprintOutput - ) - let expectedCertificate = Certificate( - publicKey: publicKey, - privateKey: privateKey, - fingerprint: "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US", - developmentTeam: "QH95ER52SG", - name: "Apple Development: Name (54GSF6G47V)", - isRevoked: false - ) - - // When - let certificate = try subject.parse(publicKey: publicKey, privateKey: privateKey) - - // Then - XCTAssertEqual(certificate, expectedCertificate) - } - - func test_parsing_succeeds_with_different_format() throws { - // Given - let publicKey = try temporaryPath().appending(component: "Target.Debug.p12") - let privateKey = try temporaryPath() - let subjectOutput = - "subject=UID = VD55TKL3V6, CN = \"Apple Development: Name (54GSF6G47V)\", OU = QH95ER52SG, O = \"Name\", C = US" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject"], - output: subjectOutput - ) - let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n" - system.succeedCommand( - ["/usr/bin/openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint"], - output: fingerprintOutput - ) - let expectedCertificate = Certificate( - publicKey: publicKey, - privateKey: privateKey, - fingerprint: "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US", - developmentTeam: "QH95ER52SG", - name: "Apple Development: Name (54GSF6G47V)", - isRevoked: false - ) - - // When - let certificate = try subject.parse(publicKey: publicKey, privateKey: privateKey) - - // Then - XCTAssertEqual(certificate, expectedCertificate) - } - - func test_sanitizeEncoding() { - // Given - let oneWrongEncoding = "test \\xC3\\xA4 something" - let twoWrongEncodings = "test \\xC3\\xA4 something \\xC2\\xB6 something else" - let twoWrongEncodingsInARow = "test \\xC3\\xA4\\xC2\\xB2 something" - let oneWrongEncodingWithMixedCapitalization = "test \\xc3\\xA4 something" - - // Then - XCTAssertEqual(oneWrongEncoding.sanitizeEncoding(), "test ä something") - XCTAssertEqual(twoWrongEncodings.sanitizeEncoding(), "test ä something ¶ something else") - XCTAssertEqual(twoWrongEncodingsInARow.sanitizeEncoding(), "test ä² something") - XCTAssertEqual(oneWrongEncodingWithMixedCapitalization.sanitizeEncoding(), "test ä something") - } -} diff --git a/Tests/TuistSigningTests/Mocks/MockCertificateParser.swift b/Tests/TuistSigningTests/Mocks/MockCertificateParser.swift deleted file mode 100644 index cab393eb9ee..00000000000 --- a/Tests/TuistSigningTests/Mocks/MockCertificateParser.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import TSCBasic -@testable import TuistSigning -@testable import TuistSigningTesting - -final class MockCertificateParser: CertificateParsing { - var parseStub: ((AbsolutePath, AbsolutePath) throws -> Certificate)? - func parse(publicKey: AbsolutePath, privateKey: AbsolutePath) throws -> Certificate { - try parseStub?(publicKey, privateKey) ?? Certificate.test() - } - - var parseFingerPrintStub: ((Data) throws -> String)? - func parseFingerPrint(developerCertificate: Data) throws -> String { - try parseFingerPrintStub?(developerCertificate) ?? "" - } -} diff --git a/Tests/TuistSigningTests/Mocks/MockProvisioningProfileParser.swift b/Tests/TuistSigningTests/Mocks/MockProvisioningProfileParser.swift deleted file mode 100644 index a43460986e0..00000000000 --- a/Tests/TuistSigningTests/Mocks/MockProvisioningProfileParser.swift +++ /dev/null @@ -1,9 +0,0 @@ -import TSCBasic -@testable import TuistSigning - -final class MockProvisioningProfileParser: ProvisioningProfileParsing { - var parseStub: ((AbsolutePath) throws -> ProvisioningProfile)? - func parse(at path: AbsolutePath) throws -> ProvisioningProfile { - try parseStub?(path) ?? ProvisioningProfile.test() - } -} diff --git a/Tests/TuistSigningTests/Mocks/MockSecurityController.swift b/Tests/TuistSigningTests/Mocks/MockSecurityController.swift deleted file mode 100644 index bb66626d694..00000000000 --- a/Tests/TuistSigningTests/Mocks/MockSecurityController.swift +++ /dev/null @@ -1,34 +0,0 @@ -import TSCBasic -@testable import TuistSigning - -final class MockSecurityController: SecurityControlling { - var importCertificateStub: ((Certificate, AbsolutePath) throws -> Void)? - func importCertificate(_ certificate: Certificate, keychainPath: AbsolutePath) throws { - try importCertificateStub?(certificate, keychainPath) - } - - var createKeychainStub: ((AbsolutePath, String) throws -> Void)? - func createKeychain(at path: AbsolutePath, password: String) throws { - try createKeychainStub?(path, password) - } - - var unlockKeychainStub: ((AbsolutePath, String) throws -> Void)? - func unlockKeychain(at path: AbsolutePath, password: String) throws { - try unlockKeychainStub?(path, password) - } - - var lockKeychainStub: ((AbsolutePath, String) throws -> Void)? - func lockKeychain(at path: AbsolutePath, password: String) throws { - try lockKeychainStub?(path, password) - } - - var decodeFileStub: ((AbsolutePath) throws -> String)? - func decodeFile(at path: AbsolutePath) throws -> String { - try decodeFileStub?(path) ?? "" - } - - var certificateExistsStub: ((AbsolutePath) throws -> Bool)? - func certificateExists(path: AbsolutePath) throws -> Bool { - try certificateExistsStub?(path) ?? false - } -} diff --git a/Tests/TuistSigningTests/Mocks/MockSigningFilesLocator.swift b/Tests/TuistSigningTests/Mocks/MockSigningFilesLocator.swift deleted file mode 100644 index 89cc3d153ff..00000000000 --- a/Tests/TuistSigningTests/Mocks/MockSigningFilesLocator.swift +++ /dev/null @@ -1,34 +0,0 @@ -import TSCBasic -@testable import TuistSigning - -final class MockSigningFilesLocator: SigningFilesLocating { - var locateSigningDirectoryStub: ((AbsolutePath) throws -> AbsolutePath)? - func locateSigningDirectory(from path: AbsolutePath) throws -> AbsolutePath? { - try locateSigningDirectoryStub?(path) - } - - var locateProvisioningProfilesStub: ((AbsolutePath) throws -> [AbsolutePath])? - func locateProvisioningProfiles(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateProvisioningProfilesStub?(path) ?? [] - } - - var locateUnencryptedCertificatesStub: ((AbsolutePath) throws -> [AbsolutePath])? - func locateUnencryptedCertificates(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateUnencryptedCertificatesStub?(path) ?? [] - } - - var locateEncryptedCertificatesStub: ((AbsolutePath) throws -> [AbsolutePath])? - func locateEncryptedCertificates(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateEncryptedCertificatesStub?(path) ?? [] - } - - var locateUnencryptedPrivateKeysStub: ((AbsolutePath) throws -> [AbsolutePath])? - func locateUnencryptedPrivateKeys(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateUnencryptedPrivateKeysStub?(path) ?? [] - } - - var locateEncryptedPrivateKeysStub: ((AbsolutePath) throws -> [AbsolutePath])? - func locateEncryptedPrivateKeys(from path: AbsolutePath) throws -> [AbsolutePath] { - try locateEncryptedPrivateKeysStub?(path) ?? [] - } -} diff --git a/Tests/TuistSigningTests/Mocks/MockSigningLinter.swift b/Tests/TuistSigningTests/Mocks/MockSigningLinter.swift deleted file mode 100644 index 7be285f4acd..00000000000 --- a/Tests/TuistSigningTests/Mocks/MockSigningLinter.swift +++ /dev/null @@ -1,21 +0,0 @@ -import TSCBasic -import TuistCore -import TuistGraph -@testable import TuistSigning - -final class MockSigningLinter: SigningLinting { - var lintStub: ((Certificate, ProvisioningProfile) -> [LintingIssue])? - func lint(certificate: Certificate, provisioningProfile: ProvisioningProfile) -> [LintingIssue] { - lintStub?(certificate, provisioningProfile) ?? [] - } - - var lintCertificateStub: ((Certificate) -> [LintingIssue])? - func lint(certificate: Certificate) -> [LintingIssue] { - lintCertificateStub?(certificate) ?? [] - } - - var lintProvisioningProfileTargetStub: ((ProvisioningProfile, Target) -> [LintingIssue])? - func lint(provisioningProfile: ProvisioningProfile, target: Target) -> [LintingIssue] { - lintProvisioningProfileTargetStub?(provisioningProfile, target) ?? [] - } -} diff --git a/Tests/TuistSigningTests/Mocks/MockSigningMatcher.swift b/Tests/TuistSigningTests/Mocks/MockSigningMatcher.swift deleted file mode 100644 index f5be61659d5..00000000000 --- a/Tests/TuistSigningTests/Mocks/MockSigningMatcher.swift +++ /dev/null @@ -1,15 +0,0 @@ -import TSCBasic -import TuistCore -@testable import TuistSigning - -final class MockSigningMatcher: SigningMatching { - var matchStub: ( - (AbsolutePath) throws - -> (certificates: [String: Certificate], provisioningProfiles: [String: [String: ProvisioningProfile]]) - )? - func match(from path: AbsolutePath) throws - -> (certificates: [String: Certificate], provisioningProfiles: [String: [String: ProvisioningProfile]]) - { - try matchStub?(path) ?? (certificates: [:], provisioningProfiles: [:]) - } -} diff --git a/Tests/TuistSigningTests/SecurityControllerTests.swift b/Tests/TuistSigningTests/SecurityControllerTests.swift deleted file mode 100644 index 7ff8524e7e3..00000000000 --- a/Tests/TuistSigningTests/SecurityControllerTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistCoreTesting -@testable import TuistSigning -@testable import TuistSigningTesting -@testable import TuistSupportTesting - -final class SecurityControllerTests: TuistUnitTestCase { - var subject: SecurityController! - - override func setUp() { - super.setUp() - subject = SecurityController() - } - - override func tearDown() { - subject = nil - super.tearDown() - } - - func test_decode_file() throws { - // Given - let decodeFilePath = try temporaryPath() - - let expectedOutput = "output" - system.succeedCommand(["/usr/bin/security", "cms", "-D", "-i", decodeFilePath.pathString], output: expectedOutput) - - // When - let output = try subject.decodeFile(at: decodeFilePath) - - // Then - XCTAssertEqual(expectedOutput, output) - } - - func test_import_certificate_and_private_key_succeeds() throws { - // Given - let certificatePath = try temporaryPath() - let privateKeyPath = try temporaryPath() - let certificate = Certificate.test(publicKey: certificatePath, privateKey: privateKeyPath) - let keychainPath = try temporaryPath() - - system.errorCommand([ - "/usr/bin/security", - "find-certificate", - certificatePath.pathString, - "-P", - "", - "-k", - keychainPath.pathString, - ]) - system.errorCommand(["/usr/bin/security", "find-key", privateKeyPath.pathString, "-P", "", "-k", keychainPath.pathString]) - system.succeedCommand([ - "/usr/bin/security", - "import", - certificatePath.pathString, - "-P", - "", - "-T", - "/usr/bin/codesign", - "-T", - "/usr/bin/security", - "-k", - keychainPath.pathString, - ]) - system.succeedCommand([ - "/usr/bin/security", - "import", - privateKeyPath.pathString, - "-P", - "", - "-T", - "/usr/bin/codesign", - "-T", - "/usr/bin/security", - "-k", - keychainPath.pathString, - ]) - - // When - try subject.importCertificate(certificate, keychainPath: keychainPath) - - // Then - XCTAssertPrinterContains("Imported certificate at \(certificate.publicKey.pathString)", at: .debug, ==) - XCTAssertPrinterContains("Imported certificate private key at \(certificate.privateKey.pathString)", at: .debug, ==) - } - - func test_skips_certificate_when_already_imported() throws { - // Given - let certificatePath = try temporaryPath() - let privateKeyPath = try temporaryPath() - let certificate = Certificate.test(publicKey: certificatePath, privateKey: privateKeyPath) - let keychainPath = try temporaryPath() - - system.succeedCommand( - [ - "/usr/bin/security", - "find-certificate", - "-c", - certificate.name, - "-a", - keychainPath.pathString, - ], - output: "Some output" - ) - - // When - try subject.importCertificate(certificate, keychainPath: keychainPath) - - // Then - XCTAssertPrinterContains( - "Skipping importing certificate at \(certificate.publicKey.pathString) because it is already present", - at: .debug, == - ) - } - - func test_keychain_is_created() throws { - // Given - let keychainPath = try temporaryPath() - let password = "" - system.succeedCommand(["/usr/bin/security", "create-keychain", "-p", password, keychainPath.pathString]) - - // When - try subject.createKeychain(at: keychainPath, password: password) - - // Then - XCTAssertPrinterContains("Created keychain at \(keychainPath.pathString)", at: .debug, ==) - } - - func test_keychain_is_unlocked() throws { - // Given - let keychainPath = try temporaryPath() - let password = "" - system.succeedCommand(["/usr/bin/security", "unlock-keychain", "-p", password, keychainPath.pathString]) - - // When - try subject.unlockKeychain(at: keychainPath, password: password) - - // Then - XCTAssertPrinterContains("Unlocked keychain at \(keychainPath.pathString)", at: .debug, ==) - } - - func test_keychain_is_locked() throws { - // Given - let keychainPath = try temporaryPath() - let password = "" - system.succeedCommand(["/usr/bin/security", "lock-keychain", "-p", password, keychainPath.pathString]) - - // When - try subject.lockKeychain(at: keychainPath, password: password) - - // Then - XCTAssertPrinterContains("Locked keychain at \(keychainPath.pathString)", at: .debug, ==) - } -} diff --git a/Tests/TuistSigningTests/SigningCipherTests.swift b/Tests/TuistSigningTests/SigningCipherTests.swift deleted file mode 100644 index b25d50c6fcb..00000000000 --- a/Tests/TuistSigningTests/SigningCipherTests.swift +++ /dev/null @@ -1,351 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistCoreTesting -@testable import TuistSigning -@testable import TuistSupportTesting - -final class SigningCipherTests: TuistUnitTestCase { - var subject: SigningCiphering! - var rootDirectoryLocator: MockRootDirectoryLocator! - var signingFilesLocator: MockSigningFilesLocator! - - override func setUp() { - super.setUp() - rootDirectoryLocator = MockRootDirectoryLocator() - signingFilesLocator = MockSigningFilesLocator() - subject = SigningCipher( - rootDirectoryLocator: rootDirectoryLocator, - signingFilesLocator: signingFilesLocator - ) - } - - override func tearDown() { - rootDirectoryLocator = nil - signingFilesLocator = nil - subject = nil - super.tearDown() - } - - func test_fails_when_no_master_key() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - try FileHandler.shared.createFolder(temporaryPath.appending(component: Constants.tuistDirectoryName)) - let masterKeyPath = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey) - rootDirectoryLocator.locateStub = temporaryPath - // Then - XCTAssertThrowsSpecific( - try subject.encryptSigning(at: temporaryPath, keepFiles: false), - SigningCipherError.masterKeyNotFound(masterKeyPath) - ) - } - - func test_encrypt_and_decrypt_signing() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - let signingDirectory = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try FileHandler.shared.createFolder(signingDirectory) - try FileHandler.shared.write( - "my-password", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - let certContent = "my-certificate" - let profileContent = "my-profile" - signingFilesLocator.locateUnencryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "CertFile.txt"), - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "ProfileFile.txt"), - ] - } - signingFilesLocator.locateEncryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - try AbsolutePath(validating: signingDirectory.pathString + "/CertFile.txt" + "." + Constants.encryptedExtension), - ] - } - signingFilesLocator.locateEncryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - try AbsolutePath( - validating: signingDirectory.pathString + "/ProfileFile.txt" + "." + Constants - .encryptedExtension - ), - ] - } - let certFile = signingDirectory.appending(component: "CertFile.txt") - let profileFile = signingDirectory.appending(component: "ProfileFile.txt") - try FileHandler.shared.write(certContent, path: certFile, atomically: true) - try FileHandler.shared.write(profileContent, path: profileFile, atomically: true) - - // When - try subject.encryptSigning(at: temporaryPath, keepFiles: false) - try subject.decryptSigning(at: temporaryPath, keepFiles: false) - - // Then - XCTAssertEqual(try fileHandler.readTextFile(certFile), certContent) - XCTAssertEqual(try fileHandler.readTextFile(profileFile), profileContent) - XCTAssertFalse( - fileHandler - .exists(try AbsolutePath( - validating: signingDirectory.pathString + "/ProfileFile.txt" + "." + Constants - .encryptedExtension - )) - ) - XCTAssertFalse( - fileHandler - .exists(try AbsolutePath( - validating: signingDirectory.pathString + "/CertFile.txt" + "." + Constants - .encryptedExtension - )) - ) - } - - func test_decrypt_signing_when_master_key_has_newline() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - let signingDirectory = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try FileHandler.shared.createFolder(signingDirectory) - try FileHandler.shared.write( - "my-password", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - let certContent = "my-certificate" - let profileContent = "my-profile" - signingFilesLocator.locateUnencryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "CertFile.txt"), - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "ProfileFile.txt"), - ] - } - signingFilesLocator.locateEncryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - try AbsolutePath(validating: signingDirectory.pathString + "/CertFile.txt" + "." + Constants.encryptedExtension), - ] - } - signingFilesLocator.locateEncryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - try AbsolutePath( - validating: signingDirectory.pathString + "/ProfileFile.txt" + "." + Constants - .encryptedExtension - ), - ] - } - let certFile = signingDirectory.appending(component: "CertFile.txt") - let profileFile = signingDirectory.appending(component: "ProfileFile.txt") - try FileHandler.shared.write(certContent, path: certFile, atomically: true) - try FileHandler.shared.write(profileContent, path: profileFile, atomically: true) - - // When - try subject.encryptSigning(at: temporaryPath, keepFiles: false) - try FileHandler.shared.write( - "my-password\n", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - try subject.decryptSigning(at: temporaryPath, keepFiles: false) - - // Then - XCTAssertEqual(try fileHandler.readTextFile(certFile), certContent) - XCTAssertEqual(try fileHandler.readTextFile(profileFile), profileContent) - XCTAssertFalse( - fileHandler - .exists(try AbsolutePath( - validating: signingDirectory.pathString + "/ProfileFile.txt" + "." + Constants - .encryptedExtension - )) - ) - XCTAssertFalse( - fileHandler - .exists(try AbsolutePath( - validating: signingDirectory.pathString + "/CertFile.txt" + "." + Constants - .encryptedExtension - )) - ) - } - - func test_encrypt_signing() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - let signingDirectory = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try FileHandler.shared.createFolder(signingDirectory) - try FileHandler.shared.write( - "my-password", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - let certContent = "my-certificate" - let profileContent = "my-profile" - let certFile = signingDirectory.appending(component: "CertFile.txt") - let profileFile = signingDirectory.appending(component: "ProfileFile.txt") - try FileHandler.shared.write(certContent, path: certFile, atomically: true) - try FileHandler.shared.write(profileContent, path: profileFile, atomically: true) - signingFilesLocator.locateUnencryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "CertFile.txt"), - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "ProfileFile.txt"), - ] - } - - let encryptedCertFile = try AbsolutePath(validating: certFile.pathString + "." + Constants.encryptedExtension) - let encryptedProfileFile = try AbsolutePath(validating: profileFile.pathString + "." + Constants.encryptedExtension) - - // When - try subject.encryptSigning(at: temporaryPath, keepFiles: false) - - // Then - XCTAssertNotEqual(try FileHandler.shared.readTextFile(encryptedCertFile), certContent) - XCTAssertNotEqual(try FileHandler.shared.readTextFile(encryptedProfileFile), profileContent) - } - - func test_encrypt_deletes_unencrypted_files() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - let signingDirectory = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try FileHandler.shared.createFolder(signingDirectory) - try FileHandler.shared.write( - "my-password", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - let certContent = "my-certificate" - let profileContent = "my-profile" - let certFile = signingDirectory.appending(component: "CertFile.txt") - let profileFile = signingDirectory.appending(component: "ProfileFile.txt") - try FileHandler.shared.write(certContent, path: certFile, atomically: true) - try FileHandler.shared.write(profileContent, path: profileFile, atomically: true) - signingFilesLocator.locateUnencryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "CertFile.txt"), - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "ProfileFile.txt"), - ] - } - - // When - try subject.encryptSigning(at: temporaryPath, keepFiles: false) - - // Then - XCTAssertFalse(fileHandler.exists(certFile)) - XCTAssertFalse(fileHandler.exists(profileFile)) - } - - func test_encrypt_does_not_delete_unencrypted_files_when_keep_files_true() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - let signingDirectory = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try FileHandler.shared.createFolder(signingDirectory) - try FileHandler.shared.write( - "my-password", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - let certContent = "my-certificate" - let profileContent = "my-profile" - let certFile = signingDirectory.appending(component: "CertFile.txt") - let profileFile = signingDirectory.appending(component: "ProfileFile.txt") - try FileHandler.shared.write(certContent, path: certFile, atomically: true) - try FileHandler.shared.write(profileContent, path: profileFile, atomically: true) - signingFilesLocator.locateUnencryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "CertFile.txt"), - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "ProfileFile.txt"), - ] - } - - // When - try subject.encryptSigning(at: temporaryPath, keepFiles: true) - - // Then - XCTAssertTrue(fileHandler.exists(certFile)) - XCTAssertTrue(fileHandler.exists(profileFile)) - } - - func test_encrypted_file_stays_the_same_when_unecrypted_file_has_not_changed() throws { - // Given - let temporaryPath = try temporaryPath() - rootDirectoryLocator.locateStub = temporaryPath - let signingDirectory = temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try FileHandler.shared.createFolder(signingDirectory) - try FileHandler.shared.write( - "my-password", - path: temporaryPath.appending(components: Constants.tuistDirectoryName, Constants.masterKey), - atomically: true - ) - let certContent = "my-certificate" - let profileContent = "my-profile" - let certFile = signingDirectory.appending(component: "CertFile.txt") - let profileFile = signingDirectory.appending(component: "ProfileFile.txt") - let encryptedCertFile = try AbsolutePath(validating: certFile.pathString + "." + Constants.encryptedExtension) - let encryptedProfileFile = try AbsolutePath(validating: profileFile.pathString + "." + Constants.encryptedExtension) - try FileHandler.shared.write(certContent, path: certFile, atomically: true) - try FileHandler.shared.write(profileContent, path: profileFile, atomically: true) - signingFilesLocator.locateUnencryptedCertificatesStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "CertFile.txt"), - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { path in - let signingDirectory = path.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - return [ - signingDirectory.appending(component: "ProfileFile.txt"), - ] - } - try subject.encryptSigning(at: temporaryPath, keepFiles: true) - let expectedCertFile = try fileHandler.readTextFile(encryptedCertFile) - let expectedProfileFile = try fileHandler.readTextFile(encryptedProfileFile) - signingFilesLocator.locateUnencryptedCertificatesStub = { _ in - [certFile] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { _ in - [profileFile] - } - - // When - try subject.encryptSigning(at: temporaryPath, keepFiles: true) - - // Then - XCTAssertEqual(try fileHandler.readTextFile(encryptedCertFile), expectedCertFile) - XCTAssertEqual(try fileHandler.readTextFile(encryptedProfileFile), expectedProfileFile) - } -} diff --git a/Tests/TuistSigningTests/SigningFilesLocatorTests.swift b/Tests/TuistSigningTests/SigningFilesLocatorTests.swift deleted file mode 100644 index d50c2ee7907..00000000000 --- a/Tests/TuistSigningTests/SigningFilesLocatorTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import TSCBasic -import TuistSupport -import XCTest -@testable import TuistCoreTesting -@testable import TuistSigning -@testable import TuistSupportTesting - -final class SigningFilesLocatorTests: TuistUnitTestCase { - var subject: SigningFilesLocator! - var rootDirectoryLocator: MockRootDirectoryLocator! - - override func setUp() { - super.setUp() - rootDirectoryLocator = MockRootDirectoryLocator() - subject = SigningFilesLocator(rootDirectoryLocator: rootDirectoryLocator) - } - - override func tearDown() { - subject = nil - rootDirectoryLocator = nil - super.tearDown() - } - - func test_locate_encrypted_certificates() throws { - // Given - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try fileHandler.createFolder(signingDirectory) - let expectedFileNames = ["file.cer.encrypted", "file2.cer.encrypted"] - try (["file", "file.txt", "file.encrypted"] + expectedFileNames) - .map(signingDirectory.appending) - .forEach(fileHandler.touch) - let expectedFiles = expectedFileNames.map(signingDirectory.appending) - - // When - let files = try subject.locateEncryptedCertificates(from: signingDirectory) - - // Then - XCTAssertEqual(files, expectedFiles) - } - - func test_locate_encrypted_private_keys() throws { - // Given - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try fileHandler.createFolder(signingDirectory) - let expectedFileNames = ["file.p12.encrypted", "file2.p12.encrypted"] - try (["file", "file.txt", "file.encrypted"] + expectedFileNames) - .map(signingDirectory.appending) - .forEach(fileHandler.touch) - let expectedFiles = expectedFileNames.map(signingDirectory.appending) - - // When - let files = try subject.locateEncryptedPrivateKeys(from: signingDirectory) - - // Then - XCTAssertEqual(files, expectedFiles) - } - - func test_locate_signing_directory_when_exists() throws { - // Given - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let expectedSigningDirectory = rootDirectory.appending( - components: Constants.tuistDirectoryName, - Constants.signingDirectoryName - ) - try fileHandler.createFolder(expectedSigningDirectory) - - // When - let signingDirectory = try subject.locateSigningDirectory(from: rootDirectory) - - // Then - XCTAssertEqual(signingDirectory, expectedSigningDirectory) - } - - func test_locate_provisioning_profiles() throws { - // Given - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try fileHandler.createFolder(signingDirectory) - let expectedFileNames = ["file.mobileprovision"] - try (["file.cer", "file.cer.encrypted"] + expectedFileNames) - .map(signingDirectory.appending) - .forEach(fileHandler.touch) - let expectedFiles = expectedFileNames.map(signingDirectory.appending) - - // When - let files = try subject.locateProvisioningProfiles(from: signingDirectory) - - // Then - XCTAssertEqual(files, expectedFiles) - } - - func test_locate_unencrypted_certificates() throws { - // Given - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try fileHandler.createFolder(signingDirectory) - let expectedFileNames = ["file.cer"] - try (["file.mobileprovision", "file.cer.encrypted"] + expectedFileNames) - .map(signingDirectory.appending) - .forEach(fileHandler.touch) - let expectedFiles = expectedFileNames.map(signingDirectory.appending) - - // When - let files = try subject.locateUnencryptedCertificates(from: signingDirectory) - - // Then - XCTAssertEqual(files, expectedFiles) - } - - func test_locate_unencrypted_private_keys() throws { - // Given - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) - try fileHandler.createFolder(signingDirectory) - let expectedFileNames = ["file.p12"] - try (["file.mobileprovision", "file.p12.encrypted"] + expectedFileNames) - .map(signingDirectory.appending) - .forEach(fileHandler.touch) - let expectedFiles = expectedFileNames.map(signingDirectory.appending) - - // When - let files = try subject.locateUnencryptedPrivateKeys(from: signingDirectory) - - // Then - XCTAssertEqual(files, expectedFiles) - } -} diff --git a/Tests/TuistSigningTests/SigningInstallerTests.swift b/Tests/TuistSigningTests/SigningInstallerTests.swift deleted file mode 100644 index 69389df9b65..00000000000 --- a/Tests/TuistSigningTests/SigningInstallerTests.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Foundation -import TSCBasic -import TuistCore -import TuistSupport -import XCTest -@testable import TuistCoreTesting -@testable import TuistSigning -@testable import TuistSigningTesting -@testable import TuistSupportTesting - -final class SigningInstallerTests: TuistUnitTestCase { - var subject: SigningInstalling! - var securityController: MockSecurityController! - - override func setUp() { - super.setUp() - securityController = MockSecurityController() - subject = SigningInstaller(securityController: securityController) - } - - override func tearDown() { - securityController = nil - subject = nil - super.tearDown() - } - - func test_installing_provisioning_profile_is_installed_warns_when_expired() throws { - // Given - let sourceProvisioningProfilePath = try generateTestProfileFile() - let provisioningProfile = ProvisioningProfile.test( - path: sourceProvisioningProfilePath, - expirationDate: Date().addingTimeInterval(-1) - ) - - // When - let issues = try subject.installProvisioningProfile(provisioningProfile) - XCTAssertEqual(issues.count, 1) - XCTAssertEqual( - issues.first, - LintingIssue.expiredProvisioningProfile(provisioningProfile) - ) - XCTAssertTrue(try isProfileInstalled(provisioningProfile)) - } - - func test_installing_provisioning_profile_fails_when_no_extension() throws { - // Given - let provisioningProfilePath = try temporaryPath().appending(component: "file") - let provisioningProfile = ProvisioningProfile.test(path: provisioningProfilePath) - - // When - let issues = try subject.installProvisioningProfile(provisioningProfile) - XCTAssertEqual(issues.count, 1) - XCTAssertEqual( - issues.first, - LintingIssue.noFileExtension(provisioningProfilePath) - ) - } - - func test_provisioning_profile_is_installed() throws { - // Given - let sourceProvisioningProfilePath = try generateTestProfileFile() - let provisioningProfile = ProvisioningProfile.test( - path: sourceProvisioningProfilePath, - uuid: UUID().uuidString - ) - - // When - let issues = try subject.installProvisioningProfile(provisioningProfile) - - // Then - XCTAssertEmpty(issues) - XCTAssertTrue(try isProfileInstalled(provisioningProfile)) - } - - func test_certificate_is_imported() throws { - // Given - let expectedCertificate = Certificate.test() - let expectedPath = try temporaryPath() - var certificate: Certificate? - var path: AbsolutePath? - securityController.importCertificateStub = { - certificate = $0 - path = $1 - } - - // When - try subject.installCertificate(expectedCertificate, keychainPath: expectedPath) - - // Then - XCTAssertEqual(expectedCertificate, certificate) - XCTAssertEqual(expectedPath, path) - } - - private func generateTestProfileFile() throws -> AbsolutePath { - let sourceProvisioningProfilePath = try temporaryPath().appending(component: "file.mobileprovision") - try "my provisioning".write(to: sourceProvisioningProfilePath.url, atomically: true, encoding: .utf8) - - return sourceProvisioningProfilePath - } - - private func isProfileInstalled(_ profile: ProvisioningProfile) throws -> Bool { - let homeDirectoryPath = try temporaryPath() - fileHandler.homeDirectoryStub = homeDirectoryPath - let provisioningProfilesDirectoryPath = homeDirectoryPath - .appending(try RelativePath(validating: "Library/MobileDevice/Provisioning Profiles")) - let destinationProvisioningProfilePath = provisioningProfilesDirectoryPath - .appending(component: "\(profile.uuid).mobileprovision") - - return fileHandler.exists(destinationProvisioningProfilePath) - } -} diff --git a/Tests/TuistSigningTests/SigningInteractorTests.swift b/Tests/TuistSigningTests/SigningInteractorTests.swift deleted file mode 100644 index fd56c5e73ea..00000000000 --- a/Tests/TuistSigningTests/SigningInteractorTests.swift +++ /dev/null @@ -1,281 +0,0 @@ -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport -import XCTest -@testable import TuistCoreTesting -@testable import TuistSigning -@testable import TuistSigningTesting -@testable import TuistSupportTesting - -final class SigningInteractorTests: TuistUnitTestCase { - var subject: SigningInteractor! - var signingFilesLocator: MockSigningFilesLocator! - var rootDirectoryLocator: MockRootDirectoryLocator! - var signingMatcher: MockSigningMatcher! - var signingInstaller: MockSigningInstaller! - var signingLinter: MockSigningLinter! - var securityController: MockSecurityController! - var signingCipher: MockSigningCipher! - - override func setUp() { - super.setUp() - signingFilesLocator = MockSigningFilesLocator() - rootDirectoryLocator = MockRootDirectoryLocator() - signingMatcher = MockSigningMatcher() - signingInstaller = MockSigningInstaller() - signingLinter = MockSigningLinter() - securityController = MockSecurityController() - signingCipher = MockSigningCipher() - - subject = SigningInteractor( - signingFilesLocator: signingFilesLocator, - rootDirectoryLocator: rootDirectoryLocator, - signingMatcher: signingMatcher, - signingInstaller: signingInstaller, - signingLinter: signingLinter, - securityController: securityController, - signingCipher: signingCipher - ) - } - - override func tearDown() { - signingFilesLocator = nil - rootDirectoryLocator = nil - signingMatcher = nil - signingInstaller = nil - signingLinter = nil - securityController = nil - signingCipher = nil - subject = nil - super.tearDown() - } - - func test_install_creates_keychain() throws { - // Given - let graph = Graph.test() - let graphTraverser = GraphTraverser(graph: graph) - let signingDirectory = try temporaryPath() - signingFilesLocator.locateSigningDirectoryStub = { _ in - signingDirectory - } - let masterKey = "master-key" - signingCipher.readMasterKeyStub = { _ in - masterKey - } - - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let keychainDirectory = rootDirectory - .appending(components: Constants.DerivedDirectory.name, Constants.DerivedDirectory.signingKeychain) - - var receivedKeychainDirectory: AbsolutePath? - var receivedMasterKey: String? - securityController.createKeychainStub = { - receivedKeychainDirectory = $0 - receivedMasterKey = $1 - } - - // When - _ = try subject.install(graphTraverser: graphTraverser) - - // Then - XCTAssertEqual(masterKey, receivedMasterKey) - XCTAssertEqual(keychainDirectory, receivedKeychainDirectory) - } - - func test_install_unlocks_keychain() throws { - // Given - let graph = Graph.test() - let graphTraverser = GraphTraverser(graph: graph) - let signingDirectory = try temporaryPath() - signingFilesLocator.locateSigningDirectoryStub = { _ in - signingDirectory - } - let masterKey = "master-key" - signingCipher.readMasterKeyStub = { _ in - masterKey - } - - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let keychainDirectory = rootDirectory - .appending(components: Constants.DerivedDirectory.name, Constants.DerivedDirectory.signingKeychain) - - var receivedKeychainDirectory: AbsolutePath? - var receivedMasterKey: String? - securityController.unlockKeychainStub = { - receivedKeychainDirectory = $0 - receivedMasterKey = $1 - } - - // When - _ = try subject.install(graphTraverser: graphTraverser) - - // Then - XCTAssertEqual(masterKey, receivedMasterKey) - XCTAssertEqual(keychainDirectory, receivedKeychainDirectory) - } - - func test_install_locks_keychain() throws { - // Given - let graph = Graph.test() - let graphTraverser = GraphTraverser(graph: graph) - signingFilesLocator.locateSigningDirectoryStub = { _ in - try self.temporaryPath() - } - let masterKey = "master-key" - signingCipher.readMasterKeyStub = { _ in - masterKey - } - - let rootDirectory = try temporaryPath() - rootDirectoryLocator.locateStub = rootDirectory - let keychainDirectory = rootDirectory - .appending(components: Constants.DerivedDirectory.name, Constants.DerivedDirectory.signingKeychain) - - var receivedKeychainDirectory: AbsolutePath? - var receivedMasterKey: String? - securityController.lockKeychainStub = { - receivedKeychainDirectory = $0 - receivedMasterKey = $1 - } - - // When - _ = try subject.install(graphTraverser: graphTraverser) - - // Then - XCTAssertEqual(masterKey, receivedMasterKey) - XCTAssertEqual(keychainDirectory, receivedKeychainDirectory) - } - - func test_install_decrypts_signing() throws { - // Given - let entryPath = try temporaryPath() - let graph = Graph.test(path: entryPath) - let graphTraverser = GraphTraverser(graph: graph) - signingFilesLocator.locateSigningDirectoryStub = { _ in - try self.temporaryPath() - } - signingCipher.readMasterKeyStub = { _ in - "master-key" - } - - rootDirectoryLocator.locateStub = try temporaryPath() - - var signingPath: AbsolutePath? - var keepFiles: Bool? - signingCipher.decryptSigningStub = { - signingPath = $0 - keepFiles = $1 - } - - // When - _ = try subject.install(graphTraverser: graphTraverser) - - // Then - XCTAssertEqual(signingPath, entryPath) - XCTAssertTrue(keepFiles ?? false) - } - - func test_install_encrypts_signing() throws { - // Given - let entryPath = try temporaryPath() - let graph = Graph.test(path: entryPath) - let graphTraverser = GraphTraverser(graph: graph) - signingFilesLocator.locateSigningDirectoryStub = { _ in - try self.temporaryPath() - } - signingCipher.readMasterKeyStub = { _ in - "master-key" - } - - rootDirectoryLocator.locateStub = try temporaryPath() - - var signingPath: AbsolutePath? - var keepFiles: Bool? - signingCipher.encryptSigningStub = { - signingPath = $0 - keepFiles = $1 - } - - // When - _ = try subject.install(graphTraverser: graphTraverser) - - // Then - XCTAssertEqual(signingPath, entryPath) - XCTAssertFalse(keepFiles ?? true) - } - - func test_installs_signing() throws { - // Given - try prepareSigning() - let targetName = "target" - let configuration = "configuration" - let expectedCertificate = Certificate.test(name: "certA") - let expectedProvisioningProfile = ProvisioningProfile.test( - name: "profileA", - developerCertificateFingerprints: ["fingerprint"] - ) - signingMatcher.matchStub = { _ in - ( - certificates: [ - "fingerprint": expectedCertificate, - "otherFingerprint": Certificate.test(name: "certB"), - ], - provisioningProfiles: [ - targetName: [ - configuration: expectedProvisioningProfile, - "some-other-config": ProvisioningProfile.test(), - ], - ] - ) - } - - let target = Target.test( - name: targetName, - settings: Settings( - configurations: [ - BuildConfiguration( - name: configuration, - variant: .debug - ): Configuration.test(), - ] - ) - ) - let project = Project.test(targets: [target]) - let graph = Graph.test(projects: [project.path: project], targets: [project.path: [target.name: target]]) - let graphTraverser = GraphTraverser(graph: graph) - - var installedCertificates: [Certificate] = [] - signingInstaller.installCertificateStub = { certificate, _ in - installedCertificates.append(certificate) - } - var installedProvisioningProfiles: [ProvisioningProfile] = [] - signingInstaller.installProvisioningProfileStub = { profile in - installedProvisioningProfiles.append(profile) - - return [] - } - - // When - _ = try subject.install(graphTraverser: graphTraverser) - - // Then - XCTAssertEqual([expectedCertificate], installedCertificates) - XCTAssertEqual([expectedProvisioningProfile], installedProvisioningProfiles) - } - - // MARK: - Helpers - - private func prepareSigning() throws { - signingFilesLocator.locateSigningDirectoryStub = { _ in - try self.temporaryPath() - } - signingCipher.readMasterKeyStub = { _ in - "master-key" - } - - rootDirectoryLocator.locateStub = try temporaryPath() - } -} diff --git a/Tests/TuistSigningTests/SigningLinterTests.swift b/Tests/TuistSigningTests/SigningLinterTests.swift deleted file mode 100644 index 8fed0f44e97..00000000000 --- a/Tests/TuistSigningTests/SigningLinterTests.swift +++ /dev/null @@ -1,222 +0,0 @@ -import TSCBasic -import TuistCore -import TuistGraph -import XCTest -@testable import TuistSigning -@testable import TuistSigningTesting -@testable import TuistSupportTesting - -final class SigningLinterTests: TuistUnitTestCase { - var subject: SigningLinter! - - override func setUp() { - super.setUp() - - subject = SigningLinter() - } - - override func tearDown() { - subject = nil - super.tearDown() - } - - func test_lint_when_development_team_and_team_id_mismatch() { - // Given - let certificate = Certificate.test(developmentTeam: "TeamA") - let provisioningProfile = ProvisioningProfile.test(teamId: "TeamB") - let expectedIssues = [ - LintingIssue( - reason: """ - Certificate \(certificate.name)'s development team \( - certificate - .developmentTeam - ) does not correspond to \(provisioningProfile.teamId). - Make sure they are the same. - """, - severity: .error - ), - ] - - // When - let got = subject.lint(certificate: certificate, provisioningProfile: provisioningProfile) - - // Then - XCTAssertEqual(got, expectedIssues) - } - - func test_lint_when_certificate_is_revoked() { - // Given - let certificate = Certificate.test(isRevoked: true) - let expectedIssues = [ - LintingIssue( - reason: "Certificate \(certificate.name) is revoked. Create a new one and replace it to resolve the issue.", - severity: .warning - ), - ] - - // When - let got = subject.lint(certificate: certificate) - - // Then - XCTAssertEqual(got, expectedIssues) - } - - func test_lint_when_provisioning_profile_and_app_id_match() { - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: "team.io.tuist" - ) - let target = Target.test(bundleId: "io.tuist") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEmpty(got) - } - - func test_lint_when_provisioning_profile_and_app_id_mismatch() { - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: "team.io.not-tuist" - ) - let target = Target.test(bundleId: "io.tuist") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEqual( - got, - [LintingIssue( - reason: """ - App id \(provisioningProfile.appId) does not correspond to \(provisioningProfile.teamId).\( - target - .bundleId - ). Make sure the provisioning profile has been added to the right target. - """, - severity: .error - )] - ) - } - - func test_lint_when_provisioning_profile_has_custom_id_prefix_and_app_id_match() { - let appIdPrefix = "appIdPrefix" - - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: appIdPrefix + ".io.tuist", - applicationIdPrefix: [appIdPrefix] - ) - let target = Target.test(bundleId: "io.tuist") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEmpty(got) - } - - func test_lint_when_provisioning_profile_has_custom_id_prefix_and_app_id_mismatch() { - let appIdPrefix = "appIdPrefix" - - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: appIdPrefix + ".io.not-tuist", - applicationIdPrefix: [appIdPrefix] - ) - let target = Target.test(bundleId: "io.tuist") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEqual( - got, - [LintingIssue( - reason: """ - App id \(provisioningProfile.appId) does not correspond to \(appIdPrefix).\( - target - .bundleId - ). Make sure the provisioning profile has been added to the right target. - """, - severity: .error - )] - ) - } - - func test_lint_when_provisioning_profile_has_wildcard() { - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: "team.io.*" - ) - let target = Target.test(bundleId: "io.tuist") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEmpty(got) - } - - func test_lint_when_provisioning_profile_has_wildcard_mismatch() { - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: "team.not-io.*" - ) - let target = Target.test(bundleId: "io.tuist") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEqual( - got, - [LintingIssue( - reason: """ - App id \(provisioningProfile.appId) does not correspond to \(provisioningProfile.teamId).\( - target - .bundleId - ). Make sure the provisioning profile has been added to the right target. - """, - severity: .error - )] - ) - } - - func test_lint_when_bundle_id_is_derived_from_build_settings_using_parentheses_pattern() { - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: "team.io.tuist" - ) - let target = Target.test(bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEmpty(got) - } - - func test_lint_when_bundle_id_is_derived_from_build_settings_using_braces_pattern() { - // Given - let provisioningProfile = ProvisioningProfile.test( - teamId: "team", - appId: "team.io.tuist" - ) - let target = Target.test(bundleId: "${PRODUCT_BUNDLE_IDENTIFIER}") - - // When - let got = subject.lint(provisioningProfile: provisioningProfile, target: target) - - // Then - XCTAssertEmpty(got) - } -} diff --git a/Tests/TuistSigningTests/SigningMapperTests.swift b/Tests/TuistSigningTests/SigningMapperTests.swift deleted file mode 100644 index c5cf576a90c..00000000000 --- a/Tests/TuistSigningTests/SigningMapperTests.swift +++ /dev/null @@ -1,115 +0,0 @@ -import TSCBasic -import TuistCore -import TuistGraph -import TuistSupport -import XCTest -@testable import TuistCoreTesting -@testable import TuistSigning -@testable import TuistSigningTesting -@testable import TuistSupportTesting - -final class SigningMapperTests: TuistUnitTestCase { - var subject: SigningMapper! - var signingFilesLocator: MockSigningFilesLocator! - var signingMatcher: MockSigningMatcher! - var signingCipher: MockSigningCipher! - - override func setUp() { - super.setUp() - signingFilesLocator = MockSigningFilesLocator() - signingMatcher = MockSigningMatcher() - signingCipher = MockSigningCipher() - - subject = SigningMapper( - signingFilesLocator: signingFilesLocator, - signingMatcher: signingMatcher, - signingCipher: signingCipher - ) - } - - override func tearDown() { - signingFilesLocator = nil - signingMatcher = nil - signingCipher = nil - subject = nil - super.tearDown() - } - - func test_signing_mapping() throws { - // Given - let signingDirectory = try temporaryPath() - signingFilesLocator.locateSigningDirectoryStub = { _ in - signingDirectory - } - - let targetName = "target" - let configuration = "configuration" - let certificate = Certificate.test(name: "certA") - let fingerprint = "fingerprint" - let provisioningProfile = ProvisioningProfile.test( - name: "profileA", - teamId: "TeamID", - appId: "TeamID.BundleID", - developerCertificateFingerprints: ["otherFingerPrint", fingerprint] - ) - signingMatcher.matchStub = { _ in - ( - certificates: [ - fingerprint: certificate, - ], - provisioningProfiles: [ - targetName: [ - configuration: provisioningProfile, - ], - ] - ) - } - - let target = Target.test( - name: targetName, - bundleId: "BundleID", - settings: Settings( - configurations: [ - BuildConfiguration( - name: configuration, - variant: .debug - ): Configuration.test(settings: [ - "SOME_SETTING": "Value", - ]), - ] - ) - ) - - let project = Project.test( - path: try temporaryPath(), - targets: [target] - ) - let derivedDirectory = project.path.appending(component: Constants.DerivedDirectory.name) - let keychainPath = derivedDirectory.appending(component: Constants.DerivedDirectory.signingKeychain) - - let expectedConfigurations: [BuildConfiguration: Configuration] = [ - BuildConfiguration( - name: configuration, - variant: .debug - ): Configuration.test(settings: [ - "SOME_SETTING": "Value", - "CODE_SIGN_STYLE": "Manual", - "CODE_SIGN_IDENTITY": SettingValue(stringLiteral: certificate.name), - "OTHER_CODE_SIGN_FLAGS": SettingValue(stringLiteral: "--keychain \(keychainPath.pathString)"), - "DEVELOPMENT_TEAM": SettingValue(stringLiteral: provisioningProfile.teamId), - "PROVISIONING_PROFILE_SPECIFIER": SettingValue(stringLiteral: provisioningProfile.uuid), - ]), - ] - - // When - let (mappedProject, sideEffects) = try subject.map(project: project) - - // Then - XCTAssertEmpty(sideEffects) - let configurations = mappedProject.targets - .map(\.settings) - .map { $0?.configurations } - - XCTAssertEqual(configurations.first, expectedConfigurations) - } -} diff --git a/Tests/TuistSigningTests/SigningMatcherTests.swift b/Tests/TuistSigningTests/SigningMatcherTests.swift deleted file mode 100644 index 45f8733266a..00000000000 --- a/Tests/TuistSigningTests/SigningMatcherTests.swift +++ /dev/null @@ -1,148 +0,0 @@ -import TSCBasic -import TuistCore -import XCTest -@testable import TuistSigning -@testable import TuistSigningTesting -@testable import TuistSupportTesting - -final class SigningMatcherTests: TuistUnitTestCase { - var subject: SigningMatcher! - var signingFilesLocator: MockSigningFilesLocator! - var provisioningProfileParser: MockProvisioningProfileParser! - var certificateParser: MockCertificateParser! - - override func setUp() { - super.setUp() - - signingFilesLocator = MockSigningFilesLocator() - provisioningProfileParser = MockProvisioningProfileParser() - certificateParser = MockCertificateParser() - - subject = SigningMatcher( - signingFilesLocator: signingFilesLocator, - provisioningProfileParser: provisioningProfileParser, - certificateParser: certificateParser - ) - } - - override func tearDown() { - subject = nil - signingFilesLocator = nil - provisioningProfileParser = nil - certificateParser = nil - super.tearDown() - } - - func test_locates_certificates_from_entry_path() throws { - // Given - let entryPath = try temporaryPath() - var locatePath: AbsolutePath? - signingFilesLocator.locateUnencryptedCertificatesStub = { - locatePath = $0 - return [] - } - - // When - _ = try subject.match(from: entryPath) - - // Then - XCTAssertEqual(entryPath, locatePath) - } - - func test_match_returns_pairs() throws { - // Given - let debugConfiguration = "debug" - let releaseConfiguration = "release" - let date = Date() - let targetName = "TargetOne" - let publicKeyPath = try AbsolutePath(validating: "/\(targetName)/\(debugConfiguration).cer") - let privateKeyPath = try AbsolutePath(validating: "/\(targetName)/\(debugConfiguration).p12") - let releasePublicKeyPath = try AbsolutePath(validating: "/\(targetName)/\(releaseConfiguration).cer") - let releasePrivateKeyPath = try AbsolutePath(validating: "/\(targetName)/\(releaseConfiguration).p12") - signingFilesLocator.locateUnencryptedCertificatesStub = { _ in - [ - publicKeyPath, - releasePublicKeyPath, - ] - } - signingFilesLocator.locateUnencryptedPrivateKeysStub = { _ in - [ - privateKeyPath, - releasePrivateKeyPath, - ] - } - certificateParser.parseStub = { publicKey, privateKey in - let fingerprint: String - if publicKey == publicKeyPath { - fingerprint = "fingerprint" - } else { - fingerprint = "otherFingerprint" - } - return Certificate.test( - publicKey: publicKey, - privateKey: privateKey, - fingerprint: fingerprint - ) - } - - let expectedCertificates: [String: Certificate] = [ - "fingerprint": Certificate.test( - publicKey: publicKeyPath, - privateKey: privateKeyPath, - fingerprint: "fingerprint" - ), - "otherFingerprint": Certificate.test( - publicKey: releasePublicKeyPath, - privateKey: releasePrivateKeyPath, - fingerprint: "otherFingerprint" - ), - ] - - let debugProvisioningProfilePath = try AbsolutePath(validating: "/\(targetName).\(debugConfiguration).mobileprovision") - let releaseProvisioningProfilePath = - try AbsolutePath(validating: "/\(targetName).\(releaseConfiguration).mobileprovision") - signingFilesLocator.locateProvisioningProfilesStub = { _ in - [ - debugProvisioningProfilePath, - releaseProvisioningProfilePath, - ] - } - provisioningProfileParser.parseStub = { profilePath in - let configurationName: String - if profilePath == debugProvisioningProfilePath { - configurationName = debugConfiguration - } else { - configurationName = releaseConfiguration - } - return ProvisioningProfile.test( - path: profilePath, - targetName: targetName, - configurationName: configurationName, - expirationDate: date - ) - } - let expectedProvisioningProfiles: [String: [String: ProvisioningProfile]] = [ - targetName: [ - debugConfiguration: ProvisioningProfile.test( - path: debugProvisioningProfilePath, - targetName: targetName, - configurationName: debugConfiguration, - expirationDate: date - ), - releaseConfiguration: ProvisioningProfile.test( - path: releaseProvisioningProfilePath, - targetName: targetName, - configurationName: releaseConfiguration, - expirationDate: date - ), - ], - ] - - // When - let (certificates, provisioningProfiles) = try subject.match(from: try temporaryPath()) - - // Then - XCTAssertEqual(certificates, expectedCertificates) - XCTAssertEqual(provisioningProfiles, expectedProvisioningProfiles) - } -} diff --git a/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift b/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift deleted file mode 100644 index 9c0b84fc03e..00000000000 --- a/Tests/TuistSupportIntegrationTests/System/SystemIntegrationTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -import TSCBasic -import XCTest -@testable import TuistSupport -@testable import TuistSupportTesting - -final class SystemIntegrationTests: TuistTestCase { - var subject: System! - - override func setUp() { - super.setUp() - subject = System() - } - - override func tearDown() { - subject = nil - super.tearDown() - } - - func test_run_valid_command() { - XCTAssertNoThrow(try subject.run(["ls"])) - } - - func test_run_invalid_command() { - XCTAssertThrowsError(try subject.run(["abcdef", "ghi"])) - } - - func test_run_valid_command_that_returns_nonzero_exit() { - XCTAssertThrowsError(try subject.run(["ls", "abcdefghi"])) - } - - func test_observable() throws { - // Given - let publisher = subject.publisher(["echo", "hola"]).mapToString() - - // When - let elements = try publisher.toBlocking() - - // Then - XCTAssertEqual(elements.count, 1) - XCTAssertTrue(elements.first?.value.spm_chomp() == "hola") - } - - func test_observable_when_it_errors() throws { - // Given - let publisher = subject.publisher(["/usr/bin/xcrun", "invalid"]).mapToString() - - do { - // When - _ = try publisher.toBlocking() - XCTFail("expected command to fail but it did not") - } catch { - // Then - XCTAssertTrue(error is TuistSupport.SystemError) - } - } - - func sandbox(_ name: String, value: String, do block: () throws -> Void) rethrows { - try? ProcessEnv.setVar(name, value: value) - _ = try? block() - try? ProcessEnv.unsetVar(name) - } -} diff --git a/Tests/TuistSupportTests/Extensions/AbsolutePath+ExtrasTests.swift b/Tests/TuistSupportTests/Extensions/AbsolutePath+ExtrasTests.swift index 9f091634d83..0c599836557 100644 --- a/Tests/TuistSupportTests/Extensions/AbsolutePath+ExtrasTests.swift +++ b/Tests/TuistSupportTests/Extensions/AbsolutePath+ExtrasTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistSupport diff --git a/Tests/TuistSupportTests/Extensions/FileManager+ExtrasTests.swift b/Tests/TuistSupportTests/Extensions/FileManager+ExtrasTests.swift index 463ce42cf4c..dfba2a5961e 100644 --- a/Tests/TuistSupportTests/Extensions/FileManager+ExtrasTests.swift +++ b/Tests/TuistSupportTests/Extensions/FileManager+ExtrasTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistSupport diff --git a/Tests/TuistSupportTests/Extensions/String+ExtrasTests.swift b/Tests/TuistSupportTests/Extensions/String+ExtrasTests.swift index a5fe6b63e2c..a66ba1f2048 100644 --- a/Tests/TuistSupportTests/Extensions/String+ExtrasTests.swift +++ b/Tests/TuistSupportTests/Extensions/String+ExtrasTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import TSCUtility import XCTest @@ -73,6 +73,50 @@ final class StringExtrasTests: TuistUnitTestCase { XCTAssertEqual(got, "ValidClassName") } + func test_to_valid_in_bundle_identifier_when_string_is_already_valid() { + // Given + let subject = "TestBundleIdentifier.tuist" + + // When + let got = subject.toValidInBundleIdentifier() + + // Then + XCTAssertEqual(got, "TestBundleIdentifier.tuist") + } + + func test_to_valid_in_bundle_identifier_when_string_contains_under_bars() { + // Given + let subject = "_test_bundle_identifier_" + + // When + let got = subject.toValidInBundleIdentifier() + + // Then + XCTAssertEqual(got, "-test-bundle-identifier-") + } + + func test_to_valid_in_bundle_identifier_when_string_contains_special_characters() { + // Given + let subject = "$test+bundle@identifier" + + // When + let got = subject.toValidInBundleIdentifier() + + // Then + XCTAssertEqual(got, "-test-bundle-identifier") + } + + func test_to_valid_in_bundle_identifier_when_string_contains_white_spaces() { + // Given + let subject = "test bundle identifier" + + // When + let got = subject.toValidInBundleIdentifier() + + // Then + XCTAssertEqual(got, "test--bundle--identifier") + } + func test_string_doesnt_match_GitURL_regex() { // Given let stringToEvaluate = "not a url string" diff --git a/Tests/TuistSupportTests/Models/InvalidGlobTests.swift b/Tests/TuistSupportTests/Models/InvalidGlobTests.swift index 16e0d2643a6..1a8739f91db 100644 --- a/Tests/TuistSupportTests/Models/InvalidGlobTests.swift +++ b/Tests/TuistSupportTests/Models/InvalidGlobTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistSupport @testable import TuistSupportTesting diff --git a/Tests/TuistSupportTests/SwiftPackageManager/PackageInfoTests.swift b/Tests/TuistSupportTests/SwiftPackageManager/PackageInfoTests.swift new file mode 100644 index 00000000000..f7e846a9fca --- /dev/null +++ b/Tests/TuistSupportTests/SwiftPackageManager/PackageInfoTests.swift @@ -0,0 +1,62 @@ +import Foundation +import TSCUtility +import XCTest + +@testable import TuistSupport + +final class PackageInfoTests: XCTestCase { + func test_packageInfo_codable() { + XCTAssertCodable( + PackageInfo( + name: "tuist", + products: [ + PackageInfo.Product(name: "tuist", type: .executable, targets: ["tuist"]), + PackageInfo.Product(name: "tuist", type: .library(.dynamic), targets: ["ProjectDescription"]), + ], + targets: [ + PackageInfo.Target( + name: "tuist", + path: nil, + url: nil, + sources: nil, + resources: [], + exclude: [], + dependencies: [ + .target(name: "TuistKit", condition: nil), + .byName(name: "TuistSupport", condition: nil), + .product( + name: "ArgumentParser", + package: "argument-parser", + moduleAliases: ["TuistSupport": "InternalTuistSupport"], + condition: nil + ), + .product( + name: "ArgumentParser", + package: "argument-parser", + moduleAliases: nil, + condition: PackageInfo.PackageConditionDescription(platformNames: ["macOS"], config: nil) + ), + ], + publicHeadersPath: nil, + type: .executable, + settings: [ + PackageInfo.Target.TargetBuildSettingDescription.Setting( + tool: .linker, + name: .linkedLibrary, + condition: PackageInfo.PackageConditionDescription(platformNames: ["iOS"], config: nil), + value: ["ProjectDescription"] + ), + ], + checksum: nil + ), + ], + platforms: [ + PackageInfo.Platform(platformName: "iOS", version: "17.2", options: []), + ], + cLanguageStandard: nil, + cxxLanguageStandard: nil, + swiftLanguageVersions: [Version(stringLiteral: "5.4.9")] + ) + ) + } +} diff --git a/Tests/TuistSupportTests/SwiftPackageManager/SwiftPackageManagerControllerTests.swift b/Tests/TuistSupportTests/SwiftPackageManager/SwiftPackageManagerControllerTests.swift index 5ca7b14c265..5f226826288 100644 --- a/Tests/TuistSupportTests/SwiftPackageManager/SwiftPackageManagerControllerTests.swift +++ b/Tests/TuistSupportTests/SwiftPackageManager/SwiftPackageManagerControllerTests.swift @@ -1,8 +1,8 @@ -import TSCBasic +import Path import TSCUtility import TuistCore -import TuistGraph import TuistSupport +import XcodeGraph import XCTest @testable import TuistSupportTesting @@ -12,7 +12,7 @@ final class SwiftPackageManagerControllerTests: TuistUnitTestCase { override func setUp() { super.setUp() - subject = SwiftPackageManagerController() + subject = SwiftPackageManagerController(system: system, fileHandler: fileHandler) } override func tearDown() { @@ -66,7 +66,7 @@ final class SwiftPackageManagerControllerTests: TuistUnitTestCase { ]) // When / Then - XCTAssertNoThrow(try subject.setToolsVersion(at: path, to: version)) + XCTAssertNoThrow(try subject.setToolsVersion(at: path, to: version!)) } func test_loadPackageInfo() throws { @@ -87,7 +87,7 @@ final class SwiftPackageManagerControllerTests: TuistUnitTestCase { let packageInfo = try subject.loadPackageInfo(at: path) // Then - XCTAssertEqual(packageInfo, PackageInfo.test) + XCTAssertBetterEqual(packageInfo, PackageInfo.test) } func test_loadPackageInfo_Xcode14() throws { diff --git a/Tests/TuistSupportTests/System/SystemEventTests.swift b/Tests/TuistSupportTests/System/SystemEventTests.swift deleted file mode 100644 index 4b39dd7baa6..00000000000 --- a/Tests/TuistSupportTests/System/SystemEventTests.swift +++ /dev/null @@ -1,102 +0,0 @@ -import Foundation -import XCTest -@testable import TuistSupport -@testable import TuistSupportTesting - -final class SystemEventTests: TuistUnitTestCase { - func test_value_when_standardOutput() { - // Given - let value = "test" - let subject = SystemEvent.standardOutput(value) - - // Then - XCTAssertEqual(subject.value, value) - } - - func test_value_when_standardError() { - // Given - let value = "test" - let subject = SystemEvent.standardError(value) - - // Then - XCTAssertEqual(subject.value, value) - } - - func test_mapToString_when_standardOutput() { - // Given - let value = "test" - let data = value.data(using: .utf8)! - let subject = SystemEvent.standardOutput(data) - - // When - let got = subject.mapToString() - - // Then - XCTAssertEqual(got, .standardOutput(value)) - } - - func test_mapToString_when_standardError() { - // Given - let value = "test" - let data = value.data(using: .utf8)! - let subject = SystemEvent.standardError(data) - - // When - let got = subject.mapToString() - - // Then - XCTAssertEqual(got, .standardError(value)) - } - - func test_isStandardOuptut_when_standardOutput() { - // Given - let value = "test" - let data = value.data(using: .utf8)! - let subject = SystemEvent.standardOutput(data) - - // When - let got = subject.isStandardOutput - - // Then - XCTAssertTrue(got) - } - - func test_isStandardError_when_standardError() { - // Given - let value = "test" - let data = value.data(using: .utf8)! - let subject = SystemEvent.standardError(data) - - // When - let got = subject.isStandardError - - // Then - XCTAssertTrue(got) - } - - func test_isStandardError_when_standardOutput() { - // Given - let value = "test" - let data = value.data(using: .utf8)! - let subject = SystemEvent.standardOutput(data) - - // When - let got = subject.isStandardError - - // Then - XCTAssertFalse(got) - } - - func test_isStandardOuptut_when_standardError() { - // Given - let value = "test" - let data = value.data(using: .utf8)! - let subject = SystemEvent.standardError(data) - - // When - let got = subject.isStandardOutput - - // Then - XCTAssertFalse(got) - } -} diff --git a/Tests/TuistSupportTests/System/SystemTests.swift b/Tests/TuistSupportTests/System/SystemTests.swift index 9484276282e..f184a3b176c 100644 --- a/Tests/TuistSupportTests/System/SystemTests.swift +++ b/Tests/TuistSupportTests/System/SystemTests.swift @@ -1,4 +1,5 @@ import Foundation +import Path import TSCBasic import XCTest @testable import TuistSupport diff --git a/Tests/TuistSupportTests/Utils/FileHandlerTests.swift b/Tests/TuistSupportTests/Utils/FileHandlerTests.swift index 9e6f9b93a7c..117186e6b3f 100644 --- a/Tests/TuistSupportTests/Utils/FileHandlerTests.swift +++ b/Tests/TuistSupportTests/Utils/FileHandlerTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistSupport @testable import TuistSupportTesting @@ -18,6 +18,8 @@ final class FileHandlerErrorTests: XCTestCase { } final class FileHandlerTests: TuistUnitTestCase { + struct TestDecodable: Decodable {} + private var subject: FileHandler! private let fileManager = FileManager.default @@ -41,13 +43,13 @@ final class FileHandlerTests: TuistUnitTestCase { let temporaryPath = try temporaryPath() let tempFile = temporaryPath.appending(component: "Temporary") let destFile = temporaryPath.appending(component: "Destination") - try "content".write(to: tempFile.asURL, atomically: true, encoding: .utf8) + try "content".write(to: URL(fileURLWithPath: tempFile.pathString), atomically: true, encoding: .utf8) // When try subject.replace(destFile, with: tempFile) // Then - let content = try String(contentsOf: destFile.asURL) + let content = try String(contentsOf: URL(fileURLWithPath: destFile.pathString)) XCTAssertEqual(content, "content") } @@ -87,7 +89,12 @@ final class FileHandlerTests: TuistUnitTestCase { func test_changeExtension() throws { // Given - let testZippedFrameworkPath = fixturePath(path: try RelativePath(validating: "uUI.xcframework.zip")) + let temporaryDirectory = try temporaryPath() + let testZippedFrameworkPath = temporaryDirectory.appending(component: "uUI.xcframework.zip") + try FileHandler.shared.copy( + from: fixturePath(path: try RelativePath(validating: "uUI.xcframework.zip")), + to: testZippedFrameworkPath + ) // When let result = try subject.changeExtension(path: testZippedFrameworkPath, to: "txt") @@ -99,6 +106,25 @@ final class FileHandlerTests: TuistUnitTestCase { _ = try subject.changeExtension(path: result, to: "zip") } + func test_readPlistFile_throwsAnError_when_invalidPlist() throws { + // Given + let temporaryDirectory = try temporaryPath() + let plistPath = temporaryDirectory.appending(component: "file.plist") + try FileHandler.shared.touch(plistPath) + + // When/Then + var _error: Error? = nil + do { + let _: TestDecodable = try subject.readPlistFile(plistPath) + } catch { + _error = error + } + XCTAssertEqual( + _error as? FileHandlerError, + FileHandlerError.propertyListDecodeError(plistPath, description: "The given data was not a valid property list.") + ) + } + // MARK: - Private private func countItemsInRootTempDirectory(appropriateFor url: URL) throws -> Int { @@ -109,7 +135,7 @@ final class FileHandlerTests: TuistUnitTestCase { create: true ).path) let rootTempPath = tempPath.parentDirectory - try fileManager.removeItem(at: tempPath.asURL) + try fileManager.removeItem(at: URL(fileURLWithPath: tempPath.pathString)) let content = try fileManager.contentsOfDirectory(atPath: rootTempPath.pathString) return content.count } diff --git a/Tests/TuistSupportTests/Utils/GitEnvironmentTests.swift b/Tests/TuistSupportTests/Utils/GitEnvironmentTests.swift deleted file mode 100644 index 97f8c21f350..00000000000 --- a/Tests/TuistSupportTests/Utils/GitEnvironmentTests.swift +++ /dev/null @@ -1,141 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistSupport -@testable import TuistSupportTesting - -final class GitEnvironmentErrorTests: TuistUnitTestCase { - func test_type_when_githubCredentialsFillError() { - XCTAssertEqual(GitEnvironmentError.githubCredentialsFillError("test").type, .bug) - } - - func test_description_when_githubCredentialsFillError() { - XCTAssertEqual( - GitEnvironmentError.githubCredentialsFillError("test").description, - "Trying to get your environment's credentials for https://github.com failed with the following error: test" - ) - } -} - -final class GitEnvironmentTests: TuistUnitTestCase { - var subject: GitEnvironment! - var envVariables: [String: String]! = [:] - - override func setUp() { - super.setUp() - subject = GitEnvironment(environment: { self.envVariables }) - } - - override func tearDown() { - subject = nil - envVariables = nil - super.tearDown() - } - - func test_githubAuthentication_returns_the_environment_variable_when_the_token_is_present() throws { - // Given - let expectation = XCTestExpectation(description: "Git authentication") - envVariables[Constants.EnvironmentVariables.githubAPIToken] = "TOKEN" - - // When - _ = subject.githubAuthentication() - .sink { _ in - } receiveValue: { value in - XCTAssertEqual(value, .token("TOKEN")) - expectation.fulfill() - } - wait(for: [expectation], timeout: 10.0) - } - - func test_githubAuthentication_returns_the_system_credentials_when_the_token_is_not_present() throws { - // Given - let expectation = XCTestExpectation(description: "Git authentication") - let output = """ - username=tuist - password=rocks - """ - system.succeedCommand(["/usr/bin/env", "echo", "url=https://github.com"], output: output) - system.succeedCommand(["/usr/bin/env", "git", "credential", "fill"], output: output) - - // When - _ = subject.githubAuthentication() - .sink { _ in - } receiveValue: { value in - XCTAssertEqual(value, .credentials(.init(username: "tuist", password: "rocks"))) - expectation.fulfill() - } - wait(for: [expectation], timeout: 10.0) - } - - func test_githubCredentials_when_the_command_fails() throws { - // Given - system.errorCommand(["/usr/bin/env", "echo", "url=https://github.com"]) - system.errorCommand(["/usr/bin/env", "git", "credential", "fill"]) - let expectation = XCTestExpectation(description: "Git credentials fill command") - - // When - _ = subject.githubCredentials() - .sink { completion in - switch completion { - case .failure: - expectation.fulfill() - case .finished: - XCTFail("Expected to receive an error but it did not.") - expectation.fulfill() - } - } receiveValue: { _ in } - wait(for: [expectation], timeout: 10.0) - } - - func test_githubCredentials_when_the_command_returns_the_expected_output() throws { - // Given - let output = """ - username=tuist - password=rocks - """ - system.succeedCommand(["/usr/bin/env", "echo", "url=https://github.com"], output: output) - system.succeedCommand(["/usr/bin/env", "git", "credential", "fill"], output: output) - let expectation = XCTestExpectation(description: "Git credentials fill command") - - // When - _ = subject.githubCredentials() - .sink { completion in - switch completion { - case .failure: - XCTFail("Expected to succeed but it failed") - expectation.fulfill() - case .finished: - expectation.fulfill() - } - } receiveValue: { value in - XCTAssertEqual(value?.username, "tuist") - XCTAssertEqual(value?.password, "rocks") - } - wait(for: [expectation], timeout: 10.0) - } - - func test_githubCredentials_when_the_commands_output_lacks_username_or_password() throws { - // Given - let output = """ - password=rocks - """ - system.succeedCommand(["/usr/bin/env", "echo", "url=https://github.com"], output: output) - system.succeedCommand(["/usr/bin/env", "git", "credential", "fill"], output: output) - let expectation = XCTestExpectation(description: "Git credentials fill command") - - // When - _ = subject.githubCredentials() - .sink { completion in - switch completion { - case .failure: - XCTFail("Expected to succeed but it failed") - expectation.fulfill() - case .finished: - expectation.fulfill() - } - } receiveValue: { value in - XCTAssertNil(value) - } - wait(for: [expectation], timeout: 10.0) - } -} diff --git a/Tests/TuistSupportTests/Utils/GlobTests.swift b/Tests/TuistSupportTests/Utils/GlobTests.swift new file mode 100644 index 00000000000..e854125a580 --- /dev/null +++ b/Tests/TuistSupportTests/Utils/GlobTests.swift @@ -0,0 +1,119 @@ +import Foundation +import Path +import XCTest +@testable import TuistSupport +@testable import TuistSupportTesting + +// Inspired by: https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3 + +final class GlobTests: TuistTestCase { + let temporaryFiles = ["foo", "bar", "baz", "dir1/file1.ext", "dir1/dir2/dir3/file2.ext", "dir1/**(_:_:)/file3.ext"] + private var temporaryDirectory: AbsolutePath! + + override func setUpWithError() throws { + super.setUp() + + temporaryDirectory = try temporaryPath() + try createFiles(temporaryFiles, content: "") + } + + func testNothingMatches() { + let pattern = "nothing" + XCTAssertEmpty(temporaryDirectory.glob(pattern)) + } + + func testBraces() { + let pattern = "ba{r,y,z}" + XCTAssertEqual( + temporaryDirectory.glob(pattern), + [temporaryDirectory.appending(component: "bar"), temporaryDirectory.appending(component: "baz")] + ) + } + + // MARK: - Globstar - Bash v4 + + func testGlobstarNoSlash() { + // Should be the equivalent of "ls -d -1 /(temporaryDirectory)/**" + let expected: [String] = [ + ".", + "bar", + "baz", + "dir1", + "dir1/**(_:_:)", + "dir1/**(_:_:)/file3.ext", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/file2.ext", + "dir1/file1.ext", + "foo", + ] + + let result = temporaryDirectory.glob("**").map { $0.relative(to: temporaryDirectory).pathString } + XCTAssertEqual(result, expected) + } + + func testGlobstarWithSlash() { + // `**/` is treated as same as `**` with Tuist since it converts pattern string to RelativePath. + // This is not an expected behavior for bash glob but this should be kept to avoid unexpected source file drops. + let expected: [String] = [ + ".", + "bar", + "baz", + "dir1", + "dir1/**(_:_:)", + "dir1/**(_:_:)/file3.ext", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/file2.ext", + "dir1/file1.ext", + "foo", + ] + + let result = temporaryDirectory.glob("**/").map { $0.relative(to: temporaryDirectory).pathString } + XCTAssertEqual(result, expected) + } + + func testGlobstarWithSlashAndWildcard() { + // Should be the equivalent of "ls -d -1 /(temporaryDirectory)/**/*" + let expected: [String] = [ + "bar", + "baz", + "dir1", + "dir1/**(_:_:)", + "dir1/**(_:_:)/file3.ext", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/file2.ext", + "dir1/file1.ext", + "foo", + ] + + let result = temporaryDirectory.glob("**/*").map { $0.relative(to: temporaryDirectory).pathString } + XCTAssertEqual(result, expected) + } + + func testPatternEndsWithGlobstar() { + let expected: [String] = [ + "dir1", + "dir1/**(_:_:)", + "dir1/**(_:_:)/file3.ext", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/file2.ext", + "dir1/file1.ext", + ] + + let result = temporaryDirectory.glob("dir1/**").map { $0.relative(to: temporaryDirectory).pathString } + XCTAssertEqual(result, expected) + } + + func testDoubleGlobstar() { + let expected: [String] = [ + "dir1/dir2/dir3", + "dir1/dir2/dir3/file2.ext", + ] + + let result = temporaryDirectory.glob("**/dir2/**/*").map { $0.relative(to: temporaryDirectory).pathString } + XCTAssertEqual(result, expected) + } +} diff --git a/Tests/TuistSupportTests/Utils/HTTPRedirectListenerTests.swift b/Tests/TuistSupportTests/Utils/HTTPRedirectListenerTests.swift deleted file mode 100644 index 986e1c0b0f0..00000000000 --- a/Tests/TuistSupportTests/Utils/HTTPRedirectListenerTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistSupport -@testable import TuistSupportTesting - -final class HTTPRedirectListenerErrorTests: XCTestCase { - func test_type_when_httpServer() { - // Given - let error = TestError("error") - let subject = HTTPRedirectListenerError.httpServer(error) - - // When - let got = subject.type - - // Then - XCTAssertEqual(got, .abort) - } - - func test_description_when_httpServer() { - // Given - let error = TestError("error") - let subject = HTTPRedirectListenerError.httpServer(error) - - // When - let got = subject.description - - // Then - XCTAssertEqual(got, "The redirect HTTP server faild to start with the following error: \(error).") - } -} diff --git a/Tests/TuistSupportTests/Utils/OpenerTests.swift b/Tests/TuistSupportTests/Utils/OpenerTests.swift index fe6d2eea216..7c2ed604fb2 100644 --- a/Tests/TuistSupportTests/Utils/OpenerTests.swift +++ b/Tests/TuistSupportTests/Utils/OpenerTests.swift @@ -1,5 +1,5 @@ import Foundation -import TSCBasic +import Path import XCTest @testable import TuistSupport diff --git a/Tests/TuistSupportTests/Utils/PrintableStringTests.swift b/Tests/TuistSupportTests/Utils/PrintableStringTests.swift deleted file mode 100644 index 747060c3b63..00000000000 --- a/Tests/TuistSupportTests/Utils/PrintableStringTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation -import XCTest - -@testable import TuistSupport -@testable import TuistSupportTesting - -final class PrintableStringTests: TuistUnitTestCase { - func test_raw_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.raw("test") - XCTAssertEqual(value.description, "test") - } - - func test_raw_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.raw("test") - XCTAssertEqual(value.description, "test") - } - - func test_command_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.command("test") - XCTAssertEqual(value.description, "test".cyan()) - } - - func test_command_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.command("test") - XCTAssertEqual(value.description, "test") - } - - func test_keystroke_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.keystroke("test") - XCTAssertEqual(value.description, "test".cyan()) - } - - func test_keystroke_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.keystroke("test") - XCTAssertEqual(value.description, "test") - } - - func test_bold_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.bold("test") - XCTAssertEqual(value.description, "test".bold()) - } - - func test_bold_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.bold("test") - XCTAssertEqual(value.description, "test") - } - - func test_error_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.error("test") - XCTAssertEqual(value.description, "test".red()) - } - - func test_error_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.error("test") - XCTAssertEqual(value.description, "test") - } - - func test_success_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.success("test") - XCTAssertEqual(value.description, "test".green()) - } - - func test_success_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.success("test") - XCTAssertEqual(value.description, "test") - } - - func test_warning_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.warning("test") - XCTAssertEqual(value.description, "test".yellow()) - } - - func test_warning_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.warning("test") - XCTAssertEqual(value.description, "test") - } - - func test_info_description_when_colored_output() { - environment.shouldOutputBeColoured = true - let value = PrintableString.Token.info("test") - XCTAssertEqual(value.description, "test".lightBlue()) - } - - func test_info_description_when_not_colored_output() { - environment.shouldOutputBeColoured = false - let value = PrintableString.Token.info("test") - XCTAssertEqual(value.description, "test") - } -} diff --git a/Tests/TuistSupportTests/Utils/TextTableTests.swift b/Tests/TuistSupportTests/Utils/TextTableTests.swift index df3beff7571..950b582b328 100644 --- a/Tests/TuistSupportTests/Utils/TextTableTests.swift +++ b/Tests/TuistSupportTests/Utils/TextTableTests.swift @@ -1,4 +1,4 @@ -import TSCBasic +import Path import XCTest @testable import TuistSupport diff --git a/Tests/TuistSupportTests/Utils/UserInputReaderTests.swift b/Tests/TuistSupportTests/Utils/UserInputReaderTests.swift new file mode 100644 index 00000000000..673e579cd02 --- /dev/null +++ b/Tests/TuistSupportTests/Utils/UserInputReaderTests.swift @@ -0,0 +1,132 @@ +import TuistSupportTesting +import XCTest +@testable import TuistSupport + +class UserInputReaderTests: TuistUnitTestCase { + func test_read_int_valid_input() { + // Given + var fakeReadLine = StringReader(input: "0") + let reader: UserInputReader = .init { _ in + fakeReadLine.readLine() + } + + // When + let result = reader.readInt(asking: "prompt", maxValueAllowed: 1) + + // Then + XCTAssertEqual(result, Int(fakeReadLine.input)) + } + + func test_read_int_after_incorrect_inputs() { + // Given + var fakeReadLine = StringReader(input: "a21") + let reader: UserInputReader = .init { _ in + fakeReadLine.readLine() + } + + // When + let result = reader.readInt(asking: "prompt", maxValueAllowed: 2) + + // Then + XCTAssertEqual(result, Int(String(fakeReadLine.input.last!))) + } + + func test_read_string() { + // Given + let reader: UserInputReader = .init { _ in + return "string-value" + } + + // When + let result = reader.readString(asking: "prompt") + + // Then + XCTAssertEqual(result, "string-value") + } + + struct Value: Equatable { + let name: String + } + + func test_read_value_when_only_value_provided() throws { + // Given + let reader: UserInputReader = .init { _ in + XCTFail("Value should be returned without reading a line") + return "string-value" + } + let value = Value(name: "value-one") + + // When + let got = try reader.readValue( + asking: "Choose value:", + values: [value], + valueDescription: \.name + ) + + // Then + XCTAssertEqual(got, value) + } + + func test_read_value_when_no_values_provided() throws { + // Given + let reader: UserInputReader = .init { _ in + XCTFail("Value should be returned without reading a line") + return "string-value" + } + let value = Value(name: "value-one") + + // When / Then + XCTAssertThrowsSpecific( + try reader.readValue( + asking: "Choose value:", + values: [Value](), + valueDescription: \.name + ), + UserInputReaderError.noValuesProvided("Choose value:") + ) + } + + func test_read_value_when_multiple_values_provided() throws { + // Given + var fakeReadLine = StringReader(input: "1") + let reader: UserInputReader = .init { _ in + fakeReadLine.readLine() + } + let valueOne = Value(name: "value-one") + let valueTwo = Value(name: "value-two") + + // When + let got = try reader.readValue( + asking: "Choose value:", + values: [valueOne, valueTwo], + valueDescription: \.name + ) + + // Then + XCTAssertEqual(got, valueTwo) + XCTAssertStandardOutput( + pattern: """ + Choose value: + \t0: value-one + \t1: value-two + """ + ) + } +} + +// Custom string reader to simulate user input +private struct StringReader { + let input: String + var index: String.Index + + init(input: String) { + self.input = input + index = input.startIndex + } + + mutating func readLine() -> String? { + guard index < input.endIndex else { return nil } + defer { index = input.index(after: index) } + return String(input[index...]) + } +} diff --git a/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift b/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift deleted file mode 100644 index 5a6705206a4..00000000000 --- a/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -import TSCBasic -import TuistAcceptanceTesting -import TuistSupport -import XCTest - -/// Test projects using tuist test -final class TestAcceptanceTests: TuistAcceptanceTestCase { - func test_with_app_with_framework_and_tests() async throws { - 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(.appWithTestPlan) - try await run(TestCommand.self) - try await run(TestCommand.self, "App", "--test-plan", "All") - } -} - -// Feature: Tests projects using Tuist test -// # TODO: Fix -// # Scenario: The project is an application with tests (app_with_tests) -// # Given that tuist is available -// # And I have a working directory -// # Then I copy the fixture app_with_tests into the working directory -// # Then tuist generates the project -// # Then tuist tests the project -// # Then tuist tests the scheme App-Workspace-iOS from the project -// # Then tuist tests the scheme App-Workspace-macOS from the project -// # Then tuist tests the scheme App-Workspace-tvOS from the project -// # Then tuist tests the scheme App from the project -// # Then tuist tests the scheme MacFramework from the project -// # Then tuist tests the scheme App and configuration Debug from the project -// -// # TODO: Fix -// # Scenario: The project is an application with tests (app_with_tests) -// # Given that tuist is available -// # And I have a working directory -// # Then I copy the fixture app_with_tests into the working directory -// # Then tuist tests the project -// # Then App-Workspace-iOS scheme has something to test -// # Then generated project is deleted -// # Then tuist tests the project -// # Then App-Workspace-iOS scheme has nothing to test -// # Then generated project is deleted -// # Then I add an empty line at the end of the file Targets/App/Sources/AppDelegate.swift -// # Then tuist tests the project -// # Then App-Workspace-iOS scheme has something to test - -// Then(/^generated project is deleted/) do -// FileUtils.rm_rf(@workspace_path) -// FileUtils.rm_rf(@xcodeproj_path) -// end - -// Then(/^([a-zA-Z-]+) scheme has nothing to test/) do |scheme_name| -// scheme_file = File.join(Xcodeproj::Workspace.new_from_xcworkspace(@workspace_path).schemes[scheme_name], -// 'xcshareddata', 'xcschemes', "#{scheme_name}.xcscheme") -// scheme = Xcodeproj::XCScheme.new(scheme_file) -// flunk("Project #{scheme_name} scheme has nothing to test") unless scheme.test_action.testables.empty? -// end - -// Then(/^([a-zA-Z-]+) scheme has something to test/) do |scheme_name| -// scheme_file = File.join(Xcodeproj::Workspace.new_from_xcworkspace(@workspace_path).schemes[scheme_name], -// 'xcshareddata', 'xcschemes', "#{scheme_name}.xcscheme") -// scheme = Xcodeproj::XCScheme.new(scheme_file) -// flunk("Project #{scheme_name} scheme has nothing to test") if scheme.test_action.testables.empty? -// end diff --git a/Tests/TuistGraphTests/Models/BinaryArchitectureTests.swift b/Tests/XcodeGraphTests/Models/BinaryArchitectureTests.swift similarity index 95% rename from Tests/TuistGraphTests/Models/BinaryArchitectureTests.swift rename to Tests/XcodeGraphTests/Models/BinaryArchitectureTests.swift index 51d6480e2f3..bc362aefbc2 100644 --- a/Tests/TuistGraphTests/Models/BinaryArchitectureTests.swift +++ b/Tests/XcodeGraphTests/Models/BinaryArchitectureTests.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic -import TuistGraph +import Path import TuistSupport +import XcodeGraph import XCTest @testable import TuistSupportTesting diff --git a/Tests/TuistGraphTests/Models/InfoPlistTests.swift b/Tests/XcodeGraphTests/Models/InfoPlistTests.swift similarity index 95% rename from Tests/TuistGraphTests/Models/InfoPlistTests.swift rename to Tests/XcodeGraphTests/Models/InfoPlistTests.swift index 76f93d8679c..d5415e3109f 100644 --- a/Tests/TuistGraphTests/Models/InfoPlistTests.swift +++ b/Tests/XcodeGraphTests/Models/InfoPlistTests.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import XCTest -@testable import TuistGraph @testable import TuistSupportTesting +@testable import XcodeGraph final class InfoPlistTests: XCTestCase { func test_codable_file() { diff --git a/Tests/TuistGraphTests/Models/LintingIssueTests.swift b/Tests/XcodeGraphTests/Models/LintingIssueTests.swift similarity index 96% rename from Tests/TuistGraphTests/Models/LintingIssueTests.swift rename to Tests/XcodeGraphTests/Models/LintingIssueTests.swift index d6bc6e6c1d7..3fb9c45219f 100644 --- a/Tests/TuistGraphTests/Models/LintingIssueTests.swift +++ b/Tests/XcodeGraphTests/Models/LintingIssueTests.swift @@ -1,9 +1,9 @@ import Foundation -import TSCBasic +import Path import TuistCore import XCTest -@testable import TuistGraph @testable import TuistSupportTesting +@testable import XcodeGraph final class LintingIssueTests: TuistUnitTestCase { func test_description() { diff --git a/Tests/TuistGraphTests/Models/ProductTests.swift b/Tests/XcodeGraphTests/Models/ProductTests.swift similarity index 98% rename from Tests/TuistGraphTests/Models/ProductTests.swift rename to Tests/XcodeGraphTests/Models/ProductTests.swift index 525845965a7..da4b1f85637 100644 --- a/Tests/TuistGraphTests/Models/ProductTests.swift +++ b/Tests/XcodeGraphTests/Models/ProductTests.swift @@ -1,7 +1,7 @@ import Foundation import XcodeProj import XCTest -@testable import TuistGraph +@testable import XcodeGraph final class ProductTests: XCTestCase { func test_codable_app() { @@ -36,7 +36,7 @@ final class ProductTests: XCTestCase { XCTAssertEqual(Product.app.description, "application") XCTAssertEqual(Product.staticLibrary.description, "static library") XCTAssertEqual(Product.dynamicLibrary.description, "dynamic library") - XCTAssertEqual(Product.framework.description, "framework") + XCTAssertEqual(Product.framework.description, "dynamic framework") XCTAssertEqual(Product.unitTests.description, "unit tests") XCTAssertEqual(Product.uiTests.description, "ui tests") XCTAssertEqual(Product.appExtension.description, "app extension") diff --git a/Tests/TuistGraphTests/Models/ProjectTests.swift b/Tests/XcodeGraphTests/Models/ProjectTests.swift similarity index 88% rename from Tests/TuistGraphTests/Models/ProjectTests.swift rename to Tests/XcodeGraphTests/Models/ProjectTests.swift index e6a06b00500..e14f58ba617 100644 --- a/Tests/TuistGraphTests/Models/ProjectTests.swift +++ b/Tests/XcodeGraphTests/Models/ProjectTests.swift @@ -1,10 +1,10 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistCoreTesting -import TuistGraphTesting +import XcodeGraphTesting import XCTest -@testable import TuistGraph +@testable import XcodeGraph final class ProjectTests: XCTestCase { func test_codable() { @@ -33,14 +33,6 @@ final class ProjectTests: XCTestCase { let graph = Graph.test( projects: [project.path: project], - targets: [ - project.path: [ - framework.name: framework, - frameworkTests.name: frameworkTests, - app.name: app, - appTests.name: appTests, - ], - ], dependencies: [ .target(name: frameworkTests.name, path: project.path): [ .target(name: framework.name, path: project.path), diff --git a/Tests/XcodeGraphTests/Models/SettingsTests.swift b/Tests/XcodeGraphTests/Models/SettingsTests.swift new file mode 100644 index 00000000000..d78f7f74d97 --- /dev/null +++ b/Tests/XcodeGraphTests/Models/SettingsTests.swift @@ -0,0 +1,178 @@ +import Foundation +import Path +import TuistSupportTesting +import XCTest +@testable import XcodeGraph + +final class SettingsTests: XCTestCase { + func test_codable() { + // Given + let subject = Settings.default + + // Then + XCTAssertCodable(subject) + } + + func testXcconfigs() { + // Given + let configurations: [BuildConfiguration: Configuration?] = [ + BuildConfiguration(name: "D", variant: .debug): Configuration( + settings: [:], + xcconfig: try! AbsolutePath(validating: "/D") + ), + .release("C"): nil, + .debug("A"): Configuration(settings: [:], xcconfig: try! AbsolutePath(validating: "/A")), + .release("B"): Configuration(settings: [:], xcconfig: try! AbsolutePath(validating: "/B")), + ] + + // When + let got = configurations.xcconfigs() + + // Then + XCTAssertEqual(got.map(\.pathString), ["/A", "/B", "/D"]) + } + + func testSortedByBuildConfigurationName() { + // Given + let configurations: [BuildConfiguration: Configuration?] = [ + BuildConfiguration(name: "D", variant: .debug): emptyConfiguration(), + .release("C"): nil, + .debug("A"): nil, + .release("B"): emptyConfiguration(), + ] + + // When + let got = configurations.sortedByBuildConfigurationName() + + // Then + XCTAssertEqual(got.map(\.0.name), ["A", "B", "C", "D"]) + } + + func testDefaultDebugConfigurationWhenDefaultExists() { + // Given + // .debug (i.e. name: "Debug", variant: .debug) is the default debug + let configurations: [BuildConfiguration: Configuration?] = [ + .release("C"): nil, + .debug("A"): nil, + .release("B"): nil, + .debug: nil, + ] + let settings = Settings(configurations: configurations) + + // When + let got = settings.defaultDebugBuildConfiguration() + + // Then + XCTAssertEqual(got, .debug) + } + + func testDefaultDebugConfigurationWhenDefaultDoesNotExist() { + // Given + // .debug (i.e. name: "Debug", variant: .debug) is the default debug + let configurations: [BuildConfiguration: Configuration?] = [ + .release("C"): nil, + .debug("A"): nil, + .release("B"): nil, + ] + let settings = Settings(configurations: configurations) + + // When + let got = settings.defaultDebugBuildConfiguration() + + // Then + XCTAssertEqual(got, .debug("A")) + } + + func testDefaultDebugConfigurationWhenNoDebugConfigurationsExist() { + // Given + let configurations: [BuildConfiguration: Configuration?] = [ + .release("C"): nil, + .release("B"): nil, + ] + let settings = Settings(configurations: configurations) + + // When + let got = settings.defaultDebugBuildConfiguration() + + // Then + XCTAssertNil(got) + } + + func testDefaultReleaseConfigurationWhenDefaultExist() { + // Given + // .release (i.e. name: "Release", variant: .release) is the default release + let configurations: [BuildConfiguration: Configuration?] = [ + .release("C"): nil, + .debug("A"): nil, + .release("B"): nil, + .release: nil, + ] + let settings = Settings(configurations: configurations) + + // When + let got = settings.defaultReleaseBuildConfiguration() + + // Then + XCTAssertEqual(got, .release) + } + + func testDefaultReleaseConfigurationWhenDefaultDoesNotExist() { + // Given + // .release (i.e. name: "Release", variant: .release) is the default release + let configurations: [BuildConfiguration: Configuration?] = [ + .release("C"): nil, + .debug("A"): nil, + .release("B"): nil, + ] + let settings = Settings(configurations: configurations) + + // When + let got = settings.defaultReleaseBuildConfiguration() + + // Then + XCTAssertEqual(got, .release("B")) + } + + func testDefaultReleaseConfigurationWhenNoReleaseConfigurationsExist() { + // Given + let configurations: [BuildConfiguration: Configuration?] = [ + .debug("A"): nil, + .debug("B"): nil, + ] + let settings = Settings(configurations: configurations) + + // When + let got = settings.defaultReleaseBuildConfiguration() + + // Then + XCTAssertNil(got) + } + + // MARK: - Helpers + + private func emptyConfiguration() -> Configuration { + Configuration(settings: [:], xcconfig: nil) + } +} + +final class DictionaryStringSettingValueExtensionTests: XCTestCase { + func testToAny() { + // Given + let buildConfig: [String: SettingValue] = [ + "A": ["A_VALUE_1", "A_VALUE_2"], + "B": "B_VALUE", + "C": ["C_VALUE"], + ] + let expected: [String: Any] = [ + "A": ["A_VALUE_1", "A_VALUE_2"], + "B": "B_VALUE", + "C": ["C_VALUE"], + ] + + // When + let got = buildConfig.toAny() + + // Then + XCTAssertEqualDictionaries(got, expected) + } +} diff --git a/Tests/TuistGraphTests/Models/TargetScriptTests.swift b/Tests/XcodeGraphTests/Models/TargetScriptTests.swift similarity index 93% rename from Tests/TuistGraphTests/Models/TargetScriptTests.swift rename to Tests/XcodeGraphTests/Models/TargetScriptTests.swift index c157af96d77..c613bb01ebd 100644 --- a/Tests/TuistGraphTests/Models/TargetScriptTests.swift +++ b/Tests/XcodeGraphTests/Models/TargetScriptTests.swift @@ -1,7 +1,7 @@ import Foundation -import TSCBasic +import Path import XCTest -@testable import TuistGraph +@testable import XcodeGraph private let script = """ echo 'Hello World' diff --git a/Tests/TuistGraphTests/Models/TargetTests.swift b/Tests/XcodeGraphTests/Models/TargetTests.swift similarity index 87% rename from Tests/TuistGraphTests/Models/TargetTests.swift rename to Tests/XcodeGraphTests/Models/TargetTests.swift index 1657cbb2256..ebbe512dba8 100644 --- a/Tests/TuistGraphTests/Models/TargetTests.swift +++ b/Tests/XcodeGraphTests/Models/TargetTests.swift @@ -1,11 +1,10 @@ import Foundation -import TSCBasic +import Path import TuistCore import TuistSupport import XCTest -@testable import TuistGraph -@testable import TuistGraphTesting @testable import TuistSupportTesting +@testable import XcodeGraph final class TargetErrorTests: TuistUnitTestCase { func test_description() { @@ -56,6 +55,54 @@ final class TargetTests: TuistUnitTestCase { ) } + func test_productNameWithExtension_when_buildSettingsProductNameStaticAndConsistentValue() { + // Given + let target = Target.test( + name: "Test", + product: .framework, + settings: .test( + base: ["PRODUCT_NAME": "Tuist"], + debug: .test(settings: ["PRODUCT_NAME": "Tuist"]), + release: .test(settings: ["PRODUCT_NAME": "Tuist"]) + ) + ) + + // When + XCTAssertEqual(target.productNameWithExtension, "Tuist.framework") + } + + func test_productNameWithExtension_when_buildSettingsProductNameDynamicAndConsistentValue() { + // Given + let target = Target.test( + name: "Test", + product: .framework, + settings: .test( + base: ["PRODUCT_NAME": "$OTHER_VARIABLE"], + debug: .test(settings: ["PRODUCT_NAME": "$OTHER_VARIABLE"]), + release: .test(settings: ["PRODUCT_NAME": "$OTHER_VARIABLE"]) + ) + ) + + // When + XCTAssertEqual(target.productNameWithExtension, "Test.framework") + } + + func test_productNameWithExtension_when_buildSettingsProductNameStaticAndInconsistentValue() { + // Given + let target = Target.test( + name: "Test", + product: .framework, + settings: .test( + base: ["PRODUCT_NAME": "Tuist"], + debug: .test(settings: ["PRODUCT_NAME": "TuistDebug"]), + release: .test(settings: ["PRODUCT_NAME": "TuistRelease"]) + ) + ) + + // When + XCTAssertEqual(target.productNameWithExtension, "Test.framework") + } + func test_productNameWithExtension_when_staticLibrary() { let target = Target.test(name: "Test", product: .staticLibrary) XCTAssertEqual(target.productNameWithExtension, "libTest.a") diff --git a/Tuist/Config.swift b/Tuist/Config.swift index 7afbc98fdc7..5ea567db25f 100644 --- a/Tuist/Config.swift +++ b/Tuist/Config.swift @@ -1,11 +1,10 @@ import ProjectDescription let config = Config( - cloud: .cloud( - projectId: "tuist/tuist", - url: "https://cloud.tuist.io", - options: [.optional] - ), - swiftVersion: .init("5.8"), - generationOptions: .options(enforceExplicitDependencies: true) + fullHandle: "tuist/tuist", + url: "https://cloud.tuist.io", + swiftVersion: .init("5.10"), + generationOptions: .options( + optionalAuthentication: true + ) ) diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift deleted file mode 100644 index 6b6ae4c7fa8..00000000000 --- a/Tuist/Dependencies.swift +++ /dev/null @@ -1,15 +0,0 @@ -import ProjectDescription - -let dependencies = Dependencies( - swiftPackageManager: .init( - productTypes: [ - "SystemPackage": .staticFramework, - "TSCBasic": .staticFramework, - "TSCUtility": .staticFramework, - "TSCclibc": .staticFramework, - "TSCLibc": .staticFramework, - "ArgumentParser": .staticFramework, - ] - ), - platforms: [.macOS] -) diff --git a/Tuist/Dependencies/Lockfiles/Package.resolved b/Tuist/Dependencies/Lockfiles/Package.resolved index 9114049c75c..f5464732a5b 100644 --- a/Tuist/Dependencies/Lockfiles/Package.resolved +++ b/Tuist/Dependencies/Lockfiles/Package.resolved @@ -198,14 +198,6 @@ "version" : "0.6.1" } }, - { - "identity" : "swifter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter.git", - "state" : { - "revision" : "1e4f51c92d7ca486242d8bf0722b99de2c3531aa" - } - }, { "identity" : "swiftgen", "kind" : "remoteSourceControl", diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved deleted file mode 100644 index e8653e8167e..00000000000 --- a/Tuist/Package.resolved +++ /dev/null @@ -1,247 +0,0 @@ -{ - "pins" : [ - { - "identity" : "aexml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tadija/AEXML.git", - "state" : { - "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version" : "4.6.1" - } - }, - { - "identity" : "anycodable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Flight-School/AnyCodable", - "state" : { - "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", - "version" : "0.6.7" - } - }, - { - "identity" : "colorizer", - "kind" : "remoteSourceControl", - "location" : "https://github.com/getGuaka/Colorizer.git", - "state" : { - "revision" : "2ccc99bf1715e73c4139e8d40b6e6b30be975586", - "version" : "0.2.1" - } - }, - { - "identity" : "combineext", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CombineCommunity/CombineExt.git", - "state" : { - "revision" : "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", - "version" : "1.8.1" - } - }, - { - "identity" : "cryptoswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", - "state" : { - "revision" : "db51c407d3be4a051484a141bf0bff36c43d3b1e", - "version" : "1.8.0" - } - }, - { - "identity" : "graphviz", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftDocOrg/GraphViz.git", - "state" : { - "revision" : "70bebcf4597b9ce33e19816d6bbd4ba9b7bdf038", - "version" : "0.2.0" - } - }, - { - "identity" : "kanna", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tid-kijyun/Kanna.git", - "state" : { - "revision" : "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", - "version" : "5.2.7" - } - }, - { - "identity" : "keychainaccess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state" : { - "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", - "version" : "4.2.2" - } - }, - { - "identity" : "komondor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/shibapm/Komondor.git", - "state" : { - "revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60", - "version" : "1.1.3" - } - }, - { - "identity" : "packageconfig", - "kind" : "remoteSourceControl", - "location" : "https://github.com/shibapm/PackageConfig.git", - "state" : { - "revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb", - "version" : "1.1.3" - } - }, - { - "identity" : "pathkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/PathKit.git", - "state" : { - "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", - "version" : "1.0.1" - } - }, - { - "identity" : "queuer", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FabrizioBrancati/Queuer.git", - "state" : { - "revision" : "52515108d0ac4616d9e15ffcc7ad986e300d31ff", - "version" : "2.1.1" - } - }, - { - "identity" : "shellout", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/ShellOut.git", - "state" : { - "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", - "version" : "2.3.0" - } - }, - { - "identity" : "spectre", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/Spectre.git", - "state" : { - "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", - "version" : "0.10.1" - } - }, - { - "identity" : "stencil", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stencilproject/Stencil.git", - "state" : { - "revision" : "4f222ac85d673f35df29962fc4c36ccfdaf9da5b", - "version" : "0.15.1" - } - }, - { - "identity" : "stencilswiftkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftGen/StencilSwiftKit.git", - "state" : { - "revision" : "20e2de5322c83df005939d9d9300fab130b49f97", - "version" : "2.10.1" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", - "version" : "1.2.1" - } - }, - { - "identity" : "swift-tools-support-core", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-tools-support-core.git", - "state" : { - "revision" : "3b13e439a341bbbfe0f710c7d1be37221745ef1a", - "version" : "0.6.1" - } - }, - { - "identity" : "swifter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter.git", - "state" : { - "revision" : "1e4f51c92d7ca486242d8bf0722b99de2c3531aa" - } - }, - { - "identity" : "swiftgen", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftGen/SwiftGen", - "state" : { - "revision" : "1cf6d7eebd70c2157f69d5a991bc57c1ef182ed1", - "version" : "6.6.2" - } - }, - { - "identity" : "xcbeautify", - "kind" : "remoteSourceControl", - "location" : "https://github.com/cpisciotta/xcbeautify", - "state" : { - "revision" : "84d24a9854e6fdcd2c91122d50a3189b072e8136", - "version" : "1.4.0" - } - }, - { - "identity" : "xcodeproj", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tuist/XcodeProj", - "state" : { - "revision" : "3797181813ee963fe305d939232bc576d23ddbb0", - "version" : "8.15.0" - } - }, - { - "identity" : "xmlcoder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MaxDesiatov/XMLCoder.git", - "state" : { - "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", - "version" : "0.17.1" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams.git", - "state" : { - "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", - "version" : "5.0.6" - } - }, - { - "identity" : "zipfoundation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/weichsel/ZIPFoundation.git", - "state" : { - "revision" : "a3f5c2bae0f04b0bce9ef3c4ba6bd1031a0564c4", - "version" : "0.9.17" - } - } - ], - "version" : 2 -} diff --git a/Tuist/Package.swift b/Tuist/Package.swift deleted file mode 100644 index 54e22a9c051..00000000000 --- a/Tuist/Package.swift +++ /dev/null @@ -1,24 +0,0 @@ -// 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-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"), - .package(url: "https://github.com/FabrizioBrancati/Queuer", from: "2.1.1"), - .package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.7"), - .package(url: "https://github.com/weichsel/ZIPFoundation", from: "0.9.17"), - .package(url: "https://github.com/httpswift/swifter", revision: "1e4f51c92d7ca486242d8bf0722b99de2c3531aa"), - .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.8.0"), - .package(url: "https://github.com/stencilproject/Stencil", from: "0.15.1"), - .package(url: "https://github.com/SwiftDocOrg/GraphViz", exact: "0.2.0"), - .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/cpisciotta/xcbeautify", from: "1.4.0"), - ] -) diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift new file mode 100644 index 00000000000..34d7e067159 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Module.swift @@ -0,0 +1,829 @@ +import Foundation +import ProjectDescription + +public enum Module: String, CaseIterable { + case tuist + case tuistBenchmark = "tuistbenchmark" + case tuistFixtureGenerator = "tuistfixturegenerator" + case projectDescription = "ProjectDescription" + case projectAutomation = "ProjectAutomation" + case acceptanceTesting = "TuistAcceptanceTesting" + case support = "TuistSupport" + case kit = "TuistKit" + case core = "TuistCore" + case generator = "TuistGenerator" + case scaffold = "TuistScaffold" + case loader = "TuistLoader" + case asyncQueue = "TuistAsyncQueue" + case plugin = "TuistPlugin" + case analytics = "TuistAnalytics" + case migration = "TuistMigration" + case dependencies = "TuistDependencies" + case automation = "TuistAutomation" + case server = "TuistServer" + case hasher = "TuistHasher" + case cache = "TuistCache" + + public var isRunnable: Bool { + switch self { + case .tuistFixtureGenerator, .tuist, .tuistBenchmark: + return true + default: + return false + } + } + + public var acceptanceTestTargets: [Target] { + var targets: [Target] = [] + + if let acceptanceTestsTargetName { + targets.append(target( + name: acceptanceTestsTargetName, + product: .unitTests, + dependencies: acceptanceTestDependencies, + isTestingTarget: false + )) + } + + return targets + } + + public var unitTestTargets: [Target] { + var targets: [Target] = [] + + if let unitTestsTargetName { + targets.append( + target( + name: unitTestsTargetName, + product: .unitTests, + dependencies: unitTestDependencies, + isTestingTarget: false + ) + ) + } + + if let integrationTestsTargetName { + targets.append( + target( + name: integrationTestsTargetName, + product: .unitTests, + dependencies: integrationTestsDependencies, + isTestingTarget: false + ) + ) + } + + return targets + } + + public var testTargets: [Target] { + return unitTestTargets + acceptanceTestTargets + } + + public var targets: [Target] { + var targets: [Target] = sourceTargets + + if let testingTargetName { + targets.append( + target( + name: testingTargetName, + product: product, + dependencies: testingDependencies, + isTestingTarget: true + ) + ) + } + + return targets + testTargets + } + + public var sourceTargets: [Target] { + let isStaticProduct = product == .staticLibrary || product == .staticFramework + let isTestingTarget = targetName == Module.acceptanceTesting.targetName + return [ + target( + name: targetName, + product: product, + dependencies: dependencies + (isStaticProduct ? [.external(name: "Mockable")] : []), + isTestingTarget: isTestingTarget + ), + ] + } + + fileprivate var sharedDependencies: [TargetDependency] { + return [ + .external(name: "Path"), + .external(name: "SystemPackage"), + ] + } + + public var acceptanceTestsTargetName: String? { + switch self { + case .kit, .automation, .dependencies, .generator: + return "\(rawValue)AcceptanceTests" + default: + return nil + } + } + + public var testingTargetName: String? { + switch self { + case .tuist, .tuistBenchmark, .tuistFixtureGenerator, .kit, .projectAutomation, .projectDescription, .analytics, + .dependencies, .acceptanceTesting, .server, .hasher, .cache: + return nil + default: + return "\(rawValue)Testing" + } + } + + public var unitTestsTargetName: String? { + switch self { + case .analytics, .tuist, .tuistBenchmark, .tuistFixtureGenerator, .projectAutomation, .projectDescription, + .acceptanceTesting: + return nil + default: + return "\(rawValue)Tests" + } + } + + public var integrationTestsTargetName: String? { + switch self { + case .tuist, .tuistBenchmark, .tuistFixtureGenerator, .projectAutomation, .projectDescription, + .asyncQueue, + .plugin, .analytics, .dependencies, .acceptanceTesting, .server, .hasher: + return nil + default: + return "\(rawValue)IntegrationTests" + } + } + + public var targetName: String { + rawValue + } + + public var product: Product { + switch self { + case .tuist, .tuistBenchmark, .tuistFixtureGenerator: + return .commandLineTool + case .projectAutomation, .projectDescription: + return .framework + default: + return .staticFramework + } + } + + public var acceptanceTestDependencies: [TargetDependency] { + let dependencies: [TargetDependency] = switch self { + case .generator: + [ + .target(name: Module.support.targetName), + .target(name: Module.acceptanceTesting.targetName), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeProj"), + ] + case .automation: + [ + .target(name: Module.acceptanceTesting.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.kit.targetName), + .target(name: Module.support.targetName), + ] + case .dependencies: + [ + .target(name: Module.acceptanceTesting.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.support.targetName), + .external(name: "XcodeProj"), + ] + case .kit: + [ + .target(name: Module.acceptanceTesting.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.kit.targetName), + .target(name: Module.support.targetName), + .target(name: Module.server.targetName), + .target(name: Module.core.targetName), + .external(name: "XcodeProj"), + ] + default: + [] + } + return dependencies + sharedDependencies + } + + public var strictConcurrencySetting: String? { + switch self { + case .projectAutomation, .projectDescription: + return "complete" + case .support: + return "targeted" + default: + return nil + } + } + + public var dependencies: [TargetDependency] { + var dependencies: [TargetDependency] = switch self { + case .acceptanceTesting: + [ + .target(name: Module.projectDescription.targetName), + .target(name: Module.kit.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.targetName), + .external(name: "XcodeProj"), + .external(name: "XcodeGraph"), + ] + case .tuist: + [ + .target(name: Module.support.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.kit.targetName), + .target(name: Module.projectDescription.targetName), + .target(name: Module.automation.targetName), + .external(name: "GraphViz"), + .external(name: "ArgumentParser"), + .external(name: "SwiftToolsSupport"), + ] + case .tuistBenchmark: + [ + .external(name: "SwiftToolsSupport"), + .external(name: "ArgumentParser"), + ] + case .tuistFixtureGenerator: + [ + .external(name: "SwiftToolsSupport"), + .external(name: "ArgumentParser"), + ] + case .projectAutomation, .projectDescription: + [] + case .support: + [ + .target(name: Module.projectDescription.targetName), + .external(name: "FileSystem"), + .external(name: "SwiftToolsSupport"), + .external(name: "AnyCodable"), + .external(name: "XcodeProj"), + .external(name: "KeychainAccess"), + .external(name: "Logging"), + .external(name: "ZIPFoundation"), + .external(name: "Difference"), + ] + case .kit: + [ + .target(name: Module.core.targetName), + .target(name: Module.hasher.targetName), + .target(name: Module.support.targetName), + .target(name: Module.generator.targetName), + .target(name: Module.automation.targetName), + .target(name: Module.server.targetName), + .target(name: Module.projectDescription.targetName), + .target(name: Module.projectAutomation.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.scaffold.targetName), + .target(name: Module.dependencies.targetName), + .target(name: Module.migration.targetName), + .target(name: Module.asyncQueue.targetName), + .target(name: Module.analytics.targetName), + .target(name: Module.plugin.targetName), + .target(name: Module.cache.targetName), + .external(name: "FileSystem"), + .external(name: "SwiftToolsSupport"), + .external(name: "XcodeGraph"), + .external(name: "ArgumentParser"), + .external(name: "GraphViz"), + .external(name: "AnyCodable"), + .external(name: "OpenAPIRuntime"), + ] + case .core: + [ + .target(name: Module.projectDescription.targetName), + .target(name: Module.support.targetName), + .external(name: "XcodeGraph"), + .external(name: "XcodeProj"), + .external(name: "SwiftToolsSupport"), + .external(name: "AnyCodable"), + ] + case .generator: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "FileSystem"), + .external(name: "XcodeGraph"), + .external(name: "SwiftGenKit"), + .external(name: "PathKit"), + .external(name: "StencilSwiftKit"), + .external(name: "XcodeProj"), + .external(name: "GraphViz"), + .external(name: "SwiftToolsSupport"), + ] + case .scaffold: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "FileSystem"), + .external(name: "XcodeGraph"), + .external(name: "PathKit"), + .external(name: "StencilSwiftKit"), + ] + case .loader: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.projectDescription.targetName), + .external(name: "XcodeGraph"), + .external(name: "FileSystem"), + .external(name: "XcodeProj"), + .external(name: "SwiftToolsSupport"), + ] + case .asyncQueue: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "FileSystem"), + .external(name: "XcodeGraph"), + .external(name: "Queuer"), + .external(name: "XcodeProj"), + ] + case .plugin: + [ + .target(name: Module.core.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.support.targetName), + .target(name: Module.scaffold.targetName), + .external(name: "FileSystem"), + .external(name: "SwiftToolsSupport"), + ] + case .analytics: + [ + .target(name: Module.asyncQueue.targetName), + .target(name: Module.core.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.support.targetName), + .external(name: "AnyCodable"), + .external(name: "XcodeGraph"), + ] + case .migration: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "PathKit"), + .external(name: "XcodeProj"), + .external(name: "SwiftToolsSupport"), + .external(name: "XcodeGraph"), + ] + case .dependencies: + [ + .target(name: Module.projectDescription.targetName), + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "XcodeGraph"), + ] + case .automation: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "FileSystem"), + .external(name: "XcodeProj"), + .external(name: "XcbeautifyLib"), + .external(name: "XcodeGraph"), + ] + case .server: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "FileSystem"), + .external(name: "OpenAPIRuntime"), + .external(name: "OpenAPIURLSession"), + .external(name: "XcodeGraph"), + ] + case .hasher: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .external(name: "XcodeGraph"), + ] + case .cache: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.hasher.targetName), + .external(name: "XcodeGraph"), + ] + } + if self != .projectDescription, self != .projectAutomation { + dependencies.append(contentsOf: sharedDependencies) + } + return dependencies + } + + public var unitTestDependencies: [TargetDependency] { + var dependencies: [TargetDependency] = switch self { + case .tuist, .tuistBenchmark, .acceptanceTesting: + [] + case .tuistFixtureGenerator: + [ + .target(name: Module.projectDescription.targetName), + ] + case .support: + [ + .target(name: Module.core.targetName), + .external(name: "XcodeGraph"), + ] + case .projectDescription: + [ + .target(name: Module.support.testingTargetName!), + .target(name: Module.support.targetName), + ] + case .projectAutomation: + [] + case .kit: + [ + .target(name: Module.support.targetName), + .target(name: Module.automation.targetName), + .target(name: Module.cache.targetName), + .target(name: Module.server.targetName), + .target(name: Module.scaffold.targetName), + .target(name: Module.analytics.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.core.targetName), + .target(name: Module.generator.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .target(name: Module.projectDescription.targetName), + .target(name: Module.projectAutomation.targetName), + .target(name: Module.loader.testingTargetName!), + .target(name: Module.generator.testingTargetName!), + .target(name: Module.scaffold.testingTargetName!), + .target(name: Module.automation.testingTargetName!), + .target(name: Module.migration.testingTargetName!), + .target(name: Module.asyncQueue.testingTargetName!), + .target(name: Module.plugin.targetName), + .target(name: Module.plugin.testingTargetName!), + .external(name: "ArgumentParser"), + .external(name: "GraphViz"), + .external(name: "AnyCodable"), + .external(name: "Difference"), + .external(name: "XcodeProj"), + .external(name: "FileSystem"), + .external(name: "Mockable"), + .external(name: "XcodeGraph"), + ] + case .core: + [ + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeGraph"), + ] + case .generator: + [ + .external(name: "PathKit"), + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.core.testingTargetName!), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeProj"), + .external(name: "GraphViz"), + .external(name: "XcodeGraph"), + ] + case .scaffold: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + ] + case .loader: + [ + .target(name: Module.projectDescription.targetName), + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .external(name: "Mockable"), + .external(name: "FileSystem"), + .external(name: "XcodeGraph"), + ] + case .asyncQueue: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .external(name: "Queuer"), + ] + case .plugin: + [ + .target(name: Module.projectDescription.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.core.targetName), + .target(name: Module.scaffold.targetName), + .target(name: Module.loader.testingTargetName!), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.scaffold.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .external(name: "XcodeGraph"), + ] + case .analytics: + [ + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + ] + case .migration: + [ + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + ] + case .dependencies: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.core.testingTargetName!), + .target(name: Module.loader.testingTargetName!), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeGraph"), + ] + case .automation: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .external(name: "XcodeGraph"), + .external(name: "FileSystem"), + ] + case .server: + [ + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .external(name: "Mockable"), + .external(name: "XcodeGraph"), + .external(name: "OpenAPIRuntime"), + ] + case .hasher: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeGraph"), + ] + case .cache: + [ + .target(name: Module.core.targetName), + .target(name: Module.hasher.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.support.targetName), + .external(name: "Mockable"), + .external(name: "XcodeGraph"), + ] + } + dependencies = dependencies + sharedDependencies + [.target(name: targetName), .external(name: "MockableTest")] + if let testingTargetName { + dependencies.append(.target(name: testingTargetName)) + } + return dependencies + } + + public var testingDependencies: [TargetDependency] { + let dependencies: [TargetDependency] = switch self { + case .tuist, .projectAutomation, .projectDescription, .acceptanceTesting, .server, .hasher, .analytics, + .migration, .tuistFixtureGenerator, .cache: + [] + case .asyncQueue: + [ + .target(name: Module.core.targetName), + ] + case .tuistBenchmark: + [ + .external(name: "ArgumentParser"), + ] + case .support: + [ + .target(name: Module.projectDescription.targetName), + .target(name: Module.core.targetName), + .external(name: "XcodeGraph"), + .external(name: "Difference"), + ] + case .kit: + [] + case .core: + [ + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.projectDescription.targetName), + .external(name: "XcodeGraph"), + ] + case .generator: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.core.testingTargetName!), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeProj"), + .external(name: "XcodeGraph"), + ] + case .scaffold: + [ + .target(name: Module.core.targetName), + .external(name: "XcodeGraph"), + ] + case .loader: + [ + .target(name: Module.support.targetName), + .target(name: Module.core.targetName), + .target(name: Module.projectDescription.targetName), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeGraph"), + ] + case .plugin: + [ + .target(name: Module.core.targetName), + .external(name: "XcodeGraph"), + ] + case .dependencies: + [ + .target(name: Module.projectDescription.targetName), + ] + case .automation: + [ + .target(name: Module.core.targetName), + .target(name: Module.core.testingTargetName!), + .target(name: Module.projectDescription.targetName), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeGraph"), + ] + } + return dependencies + sharedDependencies + [.target(name: targetName)] + } + + public var integrationTestsDependencies: [TargetDependency] { + var dependencies: [TargetDependency] = switch self { + case .tuistBenchmark, .tuistFixtureGenerator, .support, .projectAutomation, .projectDescription, .acceptanceTesting, + .asyncQueue, .plugin, .analytics, .dependencies, .server, .hasher: + [] + case .cache: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.hasher.targetName), + .external(name: "XcodeGraph"), + ] + case .tuist: + [ + .target(name: Module.generator.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.support.targetName), + .target(name: Module.core.testingTargetName!), + .target(name: Module.loader.testingTargetName!), + .external(name: "SwiftToolsSupport"), + .external(name: "XcodeProj"), + ] + case .kit: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.loader.targetName), + .target(name: Module.core.testingTargetName!), + .target(name: Module.support.testingTargetName!), + .target(name: Module.projectDescription.targetName), + .target(name: Module.automation.targetName), + .target(name: Module.loader.testingTargetName!), + .external(name: "XcodeProj"), + .external(name: "XcodeGraph"), + ] + case .core: + [ + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + ] + case .generator: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.loader.testingTargetName!), + .target(name: Module.core.testingTargetName!), + .target(name: Module.support.testingTargetName!), + .external(name: "XcodeProj"), + .external(name: "XcodeGraph"), + ] + case .scaffold: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + ] + case .loader: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.projectDescription.targetName), + ] + case .migration: + [ + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + .target(name: Module.core.testingTargetName!), + ] + case .automation: + [ + .target(name: Module.core.targetName), + .target(name: Module.support.targetName), + .target(name: Module.support.testingTargetName!), + ] + } + dependencies.append(contentsOf: sharedDependencies) + dependencies.append(.target(name: targetName)) + dependencies.append(.external(name: "MockableTest")) + if let testingTargetName { + dependencies.append(contentsOf: [.target(name: testingTargetName)]) + } + return dependencies + } + + fileprivate func target( + name: String, + product: Product, + dependencies: [TargetDependency], + isTestingTarget: Bool + ) -> Target { + let rootFolder: String + switch product { + case .unitTests: + rootFolder = "Tests" + default: + rootFolder = "Sources" + } + var debugSettings: ProjectDescription.SettingsDictionary = ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING"] + var releaseSettings: ProjectDescription.SettingsDictionary = [:] + if isTestingTarget { + debugSettings["ENABLE_TESTING_SEARCH_PATHS"] = "YES" + releaseSettings["ENABLE_TESTING_SEARCH_PATHS"] = "YES" + } + + if let strictConcurrencySetting, product == .framework { + debugSettings["SWIFT_STRICT_CONCURRENCY"] = .string(strictConcurrencySetting) + releaseSettings["SWIFT_STRICT_CONCURRENCY"] = .string(strictConcurrencySetting) + } + + let settings = Settings.settings( + configurations: [ + .debug( + name: "Debug", + settings: debugSettings, + xcconfig: nil + ), + .release( + name: "Release", + settings: releaseSettings, + xcconfig: nil + ), + ] + ) + return .target( + name: name, + destinations: [.mac], + product: product, + bundleId: "io.tuist.\(name)", + deploymentTargets: .macOS("12.0"), + infoPlist: .default, + sources: ["\(rootFolder)/\(name)/**/*.swift"], + dependencies: dependencies, + settings: settings + ) + } + + fileprivate var settings: Settings { + switch self { + case .tuist: + return .settings( + base: [ + "LD_RUNPATH_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", + ], + configurations: [ + .debug(name: "Debug", settings: [:], xcconfig: nil), + .release(name: "Release", settings: [:], xcconfig: nil), + ] + ) + default: + return .settings( + configurations: [ + .debug( + name: "Debug", + settings: ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING"], + xcconfig: nil + ), + .release( + name: "Release", + + settings: [:], + xcconfig: nil + ), + ] + ) + } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift b/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift deleted file mode 100644 index 50cf6ad1244..00000000000 --- a/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift +++ /dev/null @@ -1,105 +0,0 @@ -import ProjectDescription - -public enum TargetType { - case tests - case sources -} - -extension Target { - public static func target( - name: String, - product: Product, - dependencies: [TargetDependency], - settings _: Settings = .settings( - configurations: [ - .debug(name: "Debug", settings: [:], xcconfig: nil), - .release(name: "Release", settings: [:], xcconfig: nil), - ] - ) - ) -> Target { - let rootFolder: String - switch product { - case .unitTests: - rootFolder = "Tests" - default: - rootFolder = "Sources" - } - return Target( - name: name, - destinations: [.mac], - product: product, - bundleId: "io.tuist.\(name)", - deploymentTargets: .macOS("12.0"), - infoPlist: .default, - sources: ["\(rootFolder)/\(name)/**/*.swift"], - dependencies: dependencies - ) - } - - public static func module( - name: String, - product: Product = .staticFramework, - hasTests: Bool = true, - hasTesting: Bool = true, - hasIntegrationTests: Bool = false, - dependencies: [TargetDependency] = [], - testDependencies: [TargetDependency] = [], - testingDependencies: [TargetDependency] = [], - integrationTestsDependencies: [TargetDependency] = [] - ) -> [Target] { - var targets: [Target] = [ - .target( - name: name, - product: product, - dependencies: dependencies - ), - ] - var testTargets: [Target] = [] - if hasTests { - testTargets.append( - .target( - name: "\(name)Tests", - product: .unitTests, - dependencies: testDependencies + [ - .target(name: name), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ] - + (hasTesting ? [.target(name: "\(name)Testing")] : []) - ) - ) - } - - if hasTesting { - targets.append( - .target( - name: "\(name)Testing", - product: product, - dependencies: testingDependencies + [ - .target(name: name), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - .sdk(name: "XCTest", type: .framework, status: .optional), - ] - ) - ) - } - - if hasIntegrationTests { - testTargets.append( - .target( - name: "\(name)IntegrationTests", - product: .unitTests, - dependencies: integrationTestsDependencies + [ - .target(name: name), - .external(name: "SwiftToolsSupport"), - .external(name: "SystemPackage"), - ] - + (hasTesting ? [.target(name: "\(name)Testing")] : []) - ) - ) - } - - return targets + testTargets - } -} diff --git a/Workspace.swift b/Workspace.swift new file mode 100644 index 00000000000..5461298529a --- /dev/null +++ b/Workspace.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let workspace = Workspace(name: "Tuist", projects: ["."], generationOptions: .options(autogeneratedWorkspaceSchemes: .disabled)) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000000..24b244f9c45 --- /dev/null +++ b/app/.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/.build \ No newline at end of file diff --git a/app/Package.resolved b/app/Package.resolved new file mode 100644 index 00000000000..970fae2340b --- /dev/null +++ b/app/Package.resolved @@ -0,0 +1,330 @@ +{ + "originHash" : "e1acc6da7ed6b9f7c56b6f9eaaa9b5824dd9ed3b8f8d008c6431e6ae5be20f1b", + "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version" : "4.6.1" + } + }, + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + }, + { + "identity" : "colorizer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getGuaka/Colorizer.git", + "state" : { + "revision" : "2ccc99bf1715e73c4139e8d40b6e6b30be975586", + "version" : "0.2.1" + } + }, + { + "identity" : "command", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/command", + "state" : { + "revision" : "27270402bfb9cd65f6a8b83bdf59941a8c9ae368", + "version" : "0.8.0" + } + }, + { + "identity" : "difference", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzysztofzablocki/Difference.git", + "state" : { + "revision" : "7eb73c5d28c87dd6c4bac805aa28f757648aa92c", + "version" : "1.1.0" + } + }, + { + "identity" : "filesystem", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/FileSystem.git", + "state" : { + "revision" : "8d8f801c510cbb1592c53140772bf8b73a2df73c", + "version" : "0.3.0" + } + }, + { + "identity" : "graphviz", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/GraphViz.git", + "state" : { + "branch" : "0.2.1", + "revision" : "e4e0796a8fa74b000aba54b0256601abf75d0307" + } + }, + { + "identity" : "kanna", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tid-kijyun/Kanna.git", + "state" : { + "revision" : "41c3d28ea0eac07e4551b28def9de1ede702e739", + "version" : "5.3.0" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "komondor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/Komondor.git", + "state" : { + "revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60", + "version" : "1.1.3" + } + }, + { + "identity" : "mockable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kolos65/Mockable.git", + "state" : { + "revision" : "da977ecb20974c4b1cf185f5fd38771b2d4674fb", + "version" : "0.0.10" + } + }, + { + "identity" : "packageconfig", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/PackageConfig.git", + "state" : { + "revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb", + "version" : "1.1.3" + } + }, + { + "identity" : "path", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/Path", + "state" : { + "revision" : "25b8619f75e3d2141940a64a7b870d893164c352", + "version" : "0.3.3" + } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, + { + "identity" : "queuer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FabrizioBrancati/Queuer", + "state" : { + "revision" : "52515108d0ac4616d9e15ffcc7ad986e300d31ff", + "version" : "2.1.1" + } + }, + { + "identity" : "shellout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/ShellOut.git", + "state" : { + "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version" : "2.3.0" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" + } + }, + { + "identity" : "stencil", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stencilproject/Stencil", + "state" : { + "revision" : "4f222ac85d673f35df29962fc4c36ccfdaf9da5b", + "version" : "0.15.1" + } + }, + { + "identity" : "stencilswiftkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftGen/StencilSwiftKit", + "state" : { + "revision" : "20e2de5322c83df005939d9d9300fab130b49f97", + "version" : "2.10.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "e4abde8be0e49dc7d66e6eed651254accdcd9533", + "version" : "2.69.0" + } + }, + { + "identity" : "swift-openapi-runtime", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/swift-openapi-runtime", + "state" : { + "branch" : "swift-tools-version", + "revision" : "863d51ca0b55a72143462f4517065d906a24e0fb" + } + }, + { + "identity" : "swift-openapi-urlsession", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/swift-openapi-urlsession", + "state" : { + "branch" : "swift-tools-version", + "revision" : "91b1694705584a09827049b690e07f9118cdab5d" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "6a9e38e7bd22a3b8ba80bddf395623cf68f57807", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core", + "state" : { + "revision" : "3b13e439a341bbbfe0f710c7d1be37221745ef1a", + "version" : "0.6.1" + } + }, + { + "identity" : "swiftgen", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftGen/SwiftGen", + "state" : { + "revision" : "1cf6d7eebd70c2157f69d5a991bc57c1ef182ed1", + "version" : "6.6.2" + } + }, + { + "identity" : "xcbeautify", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cpisciotta/xcbeautify", + "state" : { + "revision" : "2486f4f69967da39ae2611e6286d60c8d3440864", + "version" : "2.8.0" + } + }, + { + "identity" : "xcodegraph", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeGraph.git", + "state" : { + "revision" : "179fbd4fa5da6ddbdd67e541f4eb4afe6438f86d", + "version" : "0.10.0" + } + }, + { + "identity" : "xcodeproj", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeProj", + "state" : { + "revision" : "75e787fb3eb5a8397c3d06d5a71e667ac2d20ac1", + "version" : "8.19.0" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MaxDesiatov/XMLCoder.git", + "state" : { + "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", + "version" : "0.17.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } + } + ], + "version" : 3 +} diff --git a/app/Package.swift b/app/Package.swift new file mode 100644 index 00000000000..0964818d446 --- /dev/null +++ b/app/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +#if TUIST + import struct ProjectDescription.PackageSettings + + let packageSettings = PackageSettings( + baseSettings: .settings(base: ["GENERATE_MASTER_OBJECT_FILE": "YES"]) + ) +#endif + +let package = Package( + name: "Tuist", + dependencies: [ + .package(path: "../"), + .package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.2.0")), + .package(url: "https://github.com/Kolos65/Mockable.git", from: "0.0.9"), + .package(url: "https://github.com/tuist/XcodeGraph.git", from: "0.8.1"), + .package(url: "https://github.com/tuist/command", from: "0.8.0"), + ] +) diff --git a/app/Project.swift b/app/Project.swift new file mode 100644 index 00000000000..a733dbe9828 --- /dev/null +++ b/app/Project.swift @@ -0,0 +1,72 @@ +import ProjectDescription + +func tuistAppDependencies() -> [TargetDependency] { + [ + .external(name: "Path"), + .external(name: "TuistSupport"), + .external(name: "TuistCore"), + .external(name: "TuistServer"), + .external(name: "TuistAutomation"), + .external(name: "XcodeGraph"), + .external(name: "Command"), + ] +} + +let project = Project( + name: "Tuist", + settings: .settings( + debug: [ + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING", + ] + ), + targets: [ + .target( + name: "Tuist", + destinations: .macOS, + product: .app, + bundleId: "io.tuist.app", + deploymentTargets: .macOS("14.0.0"), + infoPlist: .extendingDefault( + with: [ + "CFBundleURLTypes": .array( + [ + Plist.Value.dictionary( + [ + "CFBundleTypeRole": "Viewer", + "CFBundleURLName": "io.tuist.app", + "CFBundleURLSchemes": .array(["tuist"]), + ] + ), + ] + ), + "LSUIElement": .boolean(true), + ] + ), + sources: ["TuistApp/Sources/**"], + resources: ["TuistApp/Resources/**"], + dependencies: tuistAppDependencies(), + settings: .settings( + base: [ + "DEVELOPMENT_TEAM": "U6LC622NKF", + "CODE_SIGN_STYLE": "Automatic", + "CODE_SIGN_IDENTITY": "Apple Development", + ] + ) + ), + .target( + name: "TuistAppTests", + destinations: .macOS, + product: .unitTests, + bundleId: "io.tuist.TuistAppTests", + deploymentTargets: .macOS("14.0.0"), + infoPlist: .default, + sources: ["TuistApp/Tests/**"], + resources: [], + dependencies: tuistAppDependencies() + [ + .target(name: "Tuist"), + .external(name: "TuistSupportTesting"), + .external(name: "MockableTest"), + ] + ), + ] +) diff --git a/app/Tuist/Config.swift b/app/Tuist/Config.swift new file mode 100644 index 00000000000..130ea275dc1 --- /dev/null +++ b/app/Tuist/Config.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +let config = Config( + fullHandle: "tuist/tuist-app" +) diff --git a/app/TuistApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/app/TuistApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/app/TuistApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..64dc11ee743 --- /dev/null +++ b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 00000000000..c76cf9583c2 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000000..5327feb8690 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 00000000000..398a083e82c Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000000..20b3235543a Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 00000000000..5327feb8690 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000000..4808645b309 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 00000000000..20b3235543a Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000000..37713e854ed Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 00000000000..4808645b309 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000000..ac11cda918f Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/Contents.json b/app/TuistApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/app/TuistApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Resources/Assets.xcassets/Dark.colorset/Contents.json b/app/TuistApp/Resources/Assets.xcassets/Dark.colorset/Contents.json new file mode 100644 index 00000000000..d890719186e --- /dev/null +++ b/app/TuistApp/Resources/Assets.xcassets/Dark.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Resources/Assets.xcassets/Light.colorset/Contents.json b/app/TuistApp/Resources/Assets.xcassets/Light.colorset/Contents.json new file mode 100644 index 00000000000..04256378ab0 --- /dev/null +++ b/app/TuistApp/Resources/Assets.xcassets/Light.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Contents.json b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Contents.json new file mode 100644 index 00000000000..5b3cb3e8108 --- /dev/null +++ b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Tuist-Filled.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Tuist-Filled@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Tuist-Filled@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled.png b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled.png new file mode 100644 index 00000000000..fa8ce86af30 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled@2x.png b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled@2x.png new file mode 100644 index 00000000000..9178abd4085 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled@2x.png differ diff --git a/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled@3x.png b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled@3x.png new file mode 100644 index 00000000000..813728837c1 Binary files /dev/null and b/app/TuistApp/Resources/Assets.xcassets/MenuBarIcon.imageset/Tuist-Filled@3x.png differ diff --git a/app/TuistApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/app/TuistApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/app/TuistApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/TuistApp/Sources/AppDelegate.swift b/app/TuistApp/Sources/AppDelegate.swift new file mode 100644 index 00000000000..5433e192008 --- /dev/null +++ b/app/TuistApp/Sources/AppDelegate.swift @@ -0,0 +1,12 @@ +import AppKit +import Combine +import Foundation + +@MainActor +final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { + let onChangeOfURLs = PassthroughSubject<[URL], Never>() + + func application(_: NSApplication, open urls: [URL]) { + onChangeOfURLs.send(urls) + } +} diff --git a/app/TuistApp/Sources/Modifiers/HoverStyle.swift b/app/TuistApp/Sources/Modifiers/HoverStyle.swift new file mode 100644 index 00000000000..efe94629931 --- /dev/null +++ b/app/TuistApp/Sources/Modifiers/HoverStyle.swift @@ -0,0 +1,20 @@ +import Foundation +import SwiftUI + +private struct HoverStyle: ViewModifier { + @State private var hovering = false + + func body(content: Content) -> some View { + content + .onHover(perform: { hovering in + self.hovering = hovering + }) + .background(hovering ? .gray.opacity(0.2) : .clear) + } +} + +extension View { + func hoverStyle() -> some View { + modifier(HoverStyle()) + } +} diff --git a/app/TuistApp/Sources/Modifiers/MenuItemStyle.swift b/app/TuistApp/Sources/Modifiers/MenuItemStyle.swift new file mode 100644 index 00000000000..d962645465d --- /dev/null +++ b/app/TuistApp/Sources/Modifiers/MenuItemStyle.swift @@ -0,0 +1,19 @@ +import Foundation +import SwiftUI + +private struct MenuItemSyle: ViewModifier { + func body(content: Content) -> some View { + content + .buttonStyle(.plain) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6)) + .hoverStyle() + .cornerRadius(5.0) + } +} + +extension View { + func menuItemStyle() -> some View { + modifier(MenuItemSyle()) + } +} diff --git a/app/TuistApp/Sources/TuistApp.swift b/app/TuistApp/Sources/TuistApp.swift new file mode 100644 index 00000000000..016eb0dfc24 --- /dev/null +++ b/app/TuistApp/Sources/TuistApp.swift @@ -0,0 +1,13 @@ +import SwiftUI + +@main +struct TuistApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + + var body: some Scene { + MenuBarExtra("Tuist", image: "MenuBarIcon") { + MenuBarView(appDelegate: appDelegate) + } + .menuBarExtraStyle(.window) + } +} diff --git a/app/TuistApp/Sources/Utilities/AppStorage.swift b/app/TuistApp/Sources/Utilities/AppStorage.swift new file mode 100644 index 00000000000..3e0b567cc1f --- /dev/null +++ b/app/TuistApp/Sources/Utilities/AppStorage.swift @@ -0,0 +1,30 @@ +import Foundation +import Mockable +import TuistCore + +protocol AppStorageKey: Hashable { + associatedtype Value: Codable + static var defaultValue: Self.Value { get } + static var key: String { get } +} + +@Mockable +protocol AppStoring { + func get(_ key: Key.Type) throws -> Key.Value + func set(_ key: Key.Type, value: Key.Value) throws +} + +final class AppStorage: AppStoring { + func get(_ key: Key.Type) throws -> Key.Value { + guard let data = UserDefaults.standard.data(forKey: key.key) else { return key.defaultValue } + + let decoder = JSONDecoder() + return try decoder.decode(Key.Value.self, from: data) + } + + func set(_ key: Key.Type, value: Key.Value) throws { + let encoder = JSONEncoder() + let data = try encoder.encode(value) + UserDefaults.standard.setValue(data, forKey: key.key) + } +} diff --git a/app/TuistApp/Sources/Utilities/ErrorHandling.swift b/app/TuistApp/Sources/Utilities/ErrorHandling.swift new file mode 100644 index 00000000000..f1b933aef39 --- /dev/null +++ b/app/TuistApp/Sources/Utilities/ErrorHandling.swift @@ -0,0 +1,18 @@ +import AppKit +import Foundation +import TuistSupport + +final class ErrorHandling: ObservableObject, Sendable { + func handle(error: Error) { + DispatchQueue.main.async { + let alert = NSAlert() + if let error = error as? FatalError { + alert.messageText = error.description + } else { + alert.messageText = error.localizedDescription + } + alert.alertStyle = .warning + alert.runModal() + } + } +} diff --git a/app/TuistApp/Sources/Utilities/ErrorViewHandling.swift b/app/TuistApp/Sources/Utilities/ErrorViewHandling.swift new file mode 100644 index 00000000000..58a800104d7 --- /dev/null +++ b/app/TuistApp/Sources/Utilities/ErrorViewHandling.swift @@ -0,0 +1,26 @@ +import Foundation +import SwiftUI + +protocol ErrorViewHandling: View { + var errorHandling: ErrorHandling { get } +} + +extension ErrorViewHandling { + func tryWithErrorHandler(_ operation: @escaping () async throws -> Void) { + Task { + do { + try await operation() + } catch { + errorHandling.handle(error: error) + } + } + } + + func tryWithErrorErrorHandler(_ operation: @escaping () throws -> Void) { + do { + try operation() + } catch { + errorHandling.handle(error: error) + } + } +} diff --git a/app/TuistApp/Sources/Views/MenuBarView.swift b/app/TuistApp/Sources/Views/MenuBarView.swift new file mode 100644 index 00000000000..d3aab86d792 --- /dev/null +++ b/app/TuistApp/Sources/Views/MenuBarView.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftUI + +struct MenuBarView: View { + @State var isExpanded = false + @StateObject var errorHandling = ErrorHandling() + let simulatorsView: SimulatorsView + + init( + appDelegate: AppDelegate + ) { + simulatorsView = SimulatorsView(appDelegate: appDelegate) + } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Tuist") + .font(.title3) + .bold() + .background(Color.clear) + .padding([.leading, .trailing], 8) + + Divider() + .padding([.leading, .trailing], 8) + + simulatorsView + + Divider() + .padding([.leading, .trailing], 8) + + Button("Quit Tuist") { + NSApplication.shared.terminate(nil) + } + .buttonStyle(.plain) + .menuItemStyle() + .padding([.leading, .trailing], 8) + } + .padding([.top, .bottom], 8) + .environmentObject(errorHandling) + } +} diff --git a/app/TuistApp/Sources/Views/SimulatorRow/SimulatorRow.swift b/app/TuistApp/Sources/Views/SimulatorRow/SimulatorRow.swift new file mode 100644 index 00000000000..9f86b6d7c95 --- /dev/null +++ b/app/TuistApp/Sources/Views/SimulatorRow/SimulatorRow.swift @@ -0,0 +1,87 @@ +import class AppKit.NSPasteboard +import Foundation +import SwiftUI +import TuistCore +import TuistSupport + +struct SimulatorRow: View, ErrorViewHandling { + let simulator: SimulatorDeviceAndRuntime + let selected: Bool + let pinned: Bool + let onSelected: (SimulatorDeviceAndRuntime) -> Void + let onPinned: (SimulatorDeviceAndRuntime, Bool) -> Void + + @State var viewModel = SimulatorRowViewModel() + @EnvironmentObject var errorHandling: ErrorHandling + @State private var highlighted = false + + private func deviceImage() -> some View { + switch simulator.runtime.platform { + case .iOS, .none: + if simulator.device.name.contains("iPad") { + Image(systemName: "ipad") + } else { + Image(systemName: "iphone") + } + case .visionOS: + Image(systemName: "visionpro") + case .tvOS: + Image(systemName: "tv") + case .macOS: + Image(systemName: "macbook") + case .watchOS: + Image(systemName: "applewatch") + } + } + + var body: some View { + HStack(alignment: .center) { + ZStack { + Circle() + .fill(selected ? .blue : .gray.opacity(0.4)) + + deviceImage() + .foregroundColor(selected ? TuistAsset.Assets.light.swiftUIColor : TuistAsset.Assets.dark.swiftUIColor) + } + .frame(width: 24, height: 24) + VStack(alignment: .leading) { + Text(simulator.device.name) + .font(.title3) + Text("\(simulator.runtime.name)") + } + Spacer() + + Menu { + Button( + "Launch" + ) { + tryWithErrorHandler { + try await viewModel.launchSimulator(simulator) + } + } + Divider() + Button("Copy name") { + NSPasteboard.general.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) + NSPasteboard.general.setString(simulator.device.name, forType: .string) + } + Button("Copy identifier") { + NSPasteboard.general.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) + NSPasteboard.general.setString(simulator.device.udid, forType: .string) + } + Divider() + Button(pinned ? "Unpin" : "Pin") { + onPinned(simulator, !pinned) + } + } label: { + Image(systemName: "ellipsis.circle") + .font(.system(size: 16)) + .opacity(0.8) + } + .buttonStyle(.plain) + } + .menuItemStyle() + .onTapGesture { + onSelected(simulator) + } + } +} diff --git a/app/TuistApp/Sources/Views/SimulatorRow/SimulatorRowViewModel.swift b/app/TuistApp/Sources/Views/SimulatorRow/SimulatorRowViewModel.swift new file mode 100644 index 00000000000..8c369a7b59e --- /dev/null +++ b/app/TuistApp/Sources/Views/SimulatorRow/SimulatorRowViewModel.swift @@ -0,0 +1,23 @@ +import Command +import Foundation +import TuistCore +import TuistSupport + +@Observable +final class SimulatorRowViewModel { + private let simulatorController: SimulatorControlling + private let commandRunner: CommandRunning + + init( + simulatorController: SimulatorControlling = SimulatorController(), + commandRunner: CommandRunning = CommandRunner() + ) { + self.simulatorController = simulatorController + self.commandRunner = commandRunner + } + + func launchSimulator(_ simulator: SimulatorDeviceAndRuntime) async throws { + _ = try simulatorController.booted(device: simulator.device, forced: true) + _ = try await commandRunner.run(arguments: ["open", "-a", "Simulator"]).concatenatedString() + } +} diff --git a/app/TuistApp/Sources/Views/SimulatorsView/SimulatorsView.swift b/app/TuistApp/Sources/Views/SimulatorsView/SimulatorsView.swift new file mode 100644 index 00000000000..dcfffc6068e --- /dev/null +++ b/app/TuistApp/Sources/Views/SimulatorsView/SimulatorsView.swift @@ -0,0 +1,92 @@ +import Combine +import Path +import SwiftUI +import TuistCore +import TuistServer +import TuistSupport + +struct SimulatorsView: View, ErrorViewHandling { + @State var viewModel = SimulatorsViewModel() + @EnvironmentObject var errorHandling: ErrorHandling + @State var isExpanded = false + private var cancellables = Set() + + init(appDelegate: AppDelegate) { + // We can't rely on the SimulatorsView to be rendered before a deeplink is triggered. + // Instead, we listen to the deeplink URL through an `AppDelegate` callback + // that's eagerly set up in this `init` on startup. + let viewModel = viewModel + let errorHandling = ErrorHandling() + Task { + do { + try await viewModel.onAppear() + } catch { + errorHandling.handle(error: error) + } + } + appDelegate.onChangeOfURLs.sink { urls in + Task { + do { + try await viewModel.onChangeOfURL(urls.first) + } catch { + errorHandling.handle(error: error) + } + } + } + .store(in: &cancellables) + } + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 4) { + if !viewModel.pinnedSimulators.isEmpty { + Text("Pinned simulators") + .font(.headline) + .fontWeight(.medium) + .padding(.leading, 4) + + simulators(viewModel.pinnedSimulators) + + Divider() + } + + HStack { + Text("Other simulators") + .font(.headline) + .fontWeight(.medium) + Spacer() + Image(systemName: "chevron.right") + .rotationEffect(.degrees(isExpanded ? 90 : 0)) + .frame(height: 16) + } + .contentShape(Rectangle()) + .onTapGesture { + isExpanded.toggle() + } + .menuItemStyle() + + if isExpanded { + VStack(spacing: 0) { + simulators(viewModel.unpinnedSimulators) + } + } + } + .padding([.leading, .trailing], 8) + } + .frame(height: isExpanded ? NSScreen.main.map { $0.visibleFrame.size.height - 300 } ?? 500 : nil) + } + + private func simulators(_ simulators: [SimulatorDeviceAndRuntime]) -> some View { + ForEach(simulators) { simulator in + SimulatorRow( + simulator: simulator, + selected: simulator.device.udid == viewModel.selectedSimulator?.device.udid, + pinned: viewModel.pinnedSimulators.contains(where: { $0.device.udid == simulator.device.udid }) + ) { simulator in + viewModel.selectSimulator(simulator) + } onPinned: { simulator, pinned in + viewModel.simulatorPinned(simulator, pinned: pinned) + } + } + } +} diff --git a/app/TuistApp/Sources/Views/SimulatorsView/SimulatorsViewModel.swift b/app/TuistApp/Sources/Views/SimulatorsView/SimulatorsViewModel.swift new file mode 100644 index 00000000000..caecddcbe05 --- /dev/null +++ b/app/TuistApp/Sources/Views/SimulatorsView/SimulatorsViewModel.swift @@ -0,0 +1,188 @@ +import Foundation +import PathKit +import SwiftUI +import TuistAutomation +import TuistCore +import TuistServer +import TuistSupport +import XcodeGraph + +enum SimulatorsViewModelError: FatalError, Equatable { + case noSelectedSimulator + case invalidDeeplink(String) + case invalidDownloadURL(String) + case appDownloadFailed(String) + case appNotFound(SimulatorDeviceAndRuntime, [Platform]) + + var description: String { + switch self { + case .noSelectedSimulator: + return "To run a preview, you must have a simulator selected." + case let .invalidDownloadURL(url): + return "The preview download url \(url) is invalid." + case let .appDownloadFailed(url): + return "The app at \(url) was not found." + case let .appNotFound(selectedSimulator, platforms): + return "Couldn't install the app for \(selectedSimulator.device.name). The \(selectedSimulator.device.name)'s platform is \(selectedSimulator.runtime.platform?.caseValue ?? selectedSimulator.runtime.name) and the app includes only the following platforms: \(platforms.map(\.caseValue).joined(separator: ", "))" + case let .invalidDeeplink(deeplink): + return "The preview deeplink \(deeplink) is invalid." + } + } + + var type: ErrorType { + switch self { + case .invalidDownloadURL: + return .bug + case .noSelectedSimulator, .appDownloadFailed, .appNotFound, .invalidDeeplink: + return .abort + } + } +} + +struct PinnedSimulatorsKey: AppStorageKey { + static let key = "pinnedSimulators" + static let defaultValue: [SimulatorDeviceAndRuntime] = [] +} + +struct SelectedSimulatorKey: AppStorageKey { + static let key = "selectedSimulator" + static let defaultValue: SimulatorDeviceAndRuntime? = nil +} + +@Observable +final class SimulatorsViewModel: Sendable { + private(set) var pinnedSimulators: [SimulatorDeviceAndRuntime] = [] + private(set) var unpinnedSimulators: [SimulatorDeviceAndRuntime] = [] + private(set) var selectedSimulator: SimulatorDeviceAndRuntime? + + private let simulatorController: SimulatorControlling + private let downloadPreviewService: DownloadPreviewServicing + private let fileArchiverFactory: FileArchivingFactorying + private let remoteArtifactDownloader: RemoteArtifactDownloading + private let fileHandler: FileHandling + private let appBundleLoader: AppBundleLoading + private let appStorage: AppStoring + + init( + simulatorController: SimulatorControlling = SimulatorController(), + downloadPreviewService: DownloadPreviewServicing = DownloadPreviewService(), + fileArchiverFactory: FileArchivingFactorying = FileArchivingFactory(), + remoteArtifactDownloader: RemoteArtifactDownloading = RemoteArtifactDownloader(), + fileHandler: FileHandling = FileHandler.shared, + appBundleLoader: AppBundleLoading = AppBundleLoader(), + appStorage: AppStoring = AppStorage() + ) { + self.simulatorController = simulatorController + self.downloadPreviewService = downloadPreviewService + self.fileArchiverFactory = fileArchiverFactory + self.remoteArtifactDownloader = remoteArtifactDownloader + self.fileHandler = fileHandler + self.appBundleLoader = appBundleLoader + self.appStorage = appStorage + } + + func selectSimulator(_ simulator: SimulatorDeviceAndRuntime) { + selectedSimulator = simulator + try? appStorage.set(SelectedSimulatorKey.self, value: simulator) + } + + func simulatorPinned(_ simulator: SimulatorDeviceAndRuntime, pinned: Bool) { + if pinned { + pinnedSimulators = (pinnedSimulators + [simulator]).sorted() + unpinnedSimulators = unpinnedSimulators.filter { $0.device.udid != simulator.device.udid } + } else { + pinnedSimulators = pinnedSimulators.filter { $0.device.udid != simulator.device.udid } + unpinnedSimulators = (unpinnedSimulators + [simulator]).sorted() + } + try? appStorage.set(PinnedSimulatorsKey.self, value: pinnedSimulators) + } + + func onAppear() async throws { + let simulators = try await simulatorController.devicesAndRuntimes() + .sorted() + + if let selectedSimulator = try appStorage.get(SelectedSimulatorKey.self) { + self.selectedSimulator = selectedSimulator + } else { + selectedSimulator = simulators.first(where: { !$0.device.isShutdown }) + } + + pinnedSimulators = try appStorage.get(PinnedSimulatorsKey.self) + + unpinnedSimulators = Set(simulators) + .subtracting(Set(pinnedSimulators)) + .map { $0 } + .sorted() + } + + func onChangeOfURL(_ url: URL?) async throws { + guard let previewURL = url else { return } + + guard let selectedSimulator else { throw SimulatorsViewModelError.noSelectedSimulator } + + let urlComponents = URLComponents(url: previewURL, resolvingAgainstBaseURL: false) + guard let previewId = urlComponents?.queryItems?.first(where: { $0.name == "preview_id" })?.value, + let fullHandle = urlComponents?.queryItems?.first(where: { $0.name == "full_handle" })?.value, + let serverURLString = urlComponents?.queryItems?.first(where: { $0.name == "server_url" })?.value, + let serverURL = URL(string: serverURLString) + else { throw SimulatorsViewModelError.invalidDeeplink(previewURL.absoluteString) } + + let downloadURL = try await downloadPreviewService.downloadPreview( + previewId, + fullHandle: fullHandle, + serverURL: serverURL + ) + + guard let downloadURL = URL(string: downloadURL) else { throw SimulatorsViewModelError.invalidDownloadURL(downloadURL) } + + guard let archivePath = try await remoteArtifactDownloader.download(url: downloadURL) + else { throw SimulatorsViewModelError.appDownloadFailed(previewURL.absoluteString) } + let fileUnarchiver = try fileArchiverFactory.makeFileUnarchiver(for: archivePath) + let unarchivedDirectory = try fileUnarchiver.unzip() + + let apps = try await fileHandler.glob(unarchivedDirectory, glob: "*.app").concurrentMap { + try await self.appBundleLoader.load($0) + } + + guard let app = apps.first( + where: { + $0.infoPlist.supportedPlatforms.contains( + where: { + switch $0 { + case .device: + return false + case let .simulator(platform): + return selectedSimulator.runtime.platform == platform + } + } + ) + } + ) + else { + throw SimulatorsViewModelError.appNotFound( + selectedSimulator, + apps.flatMap(\.infoPlist.supportedPlatforms).compactMap { + switch $0 { + case .device: + return nil + case let .simulator(platform): + return platform + } + } + ) + } + + let bootedDevice = try simulatorController.booted(device: selectedSimulator.device, forced: true) + try simulatorController.installApp(at: app.path, device: bootedDevice) + try simulatorController.launchApp(bundleId: app.infoPlist.bundleId, device: bootedDevice, arguments: []) + } +} + +extension [SimulatorDeviceAndRuntime] { + fileprivate func sorted() -> Self { + sorted(by: { + if $0.device.name == $1.device.name { return $0.runtime.name < $1.runtime.name } + else { return $0.device.name < $1.device.name } + }) + } +} diff --git a/app/TuistApp/Tests/SimulatorRowViewModelTests.swift b/app/TuistApp/Tests/SimulatorRowViewModelTests.swift new file mode 100644 index 00000000000..a0a6bfe5881 --- /dev/null +++ b/app/TuistApp/Tests/SimulatorRowViewModelTests.swift @@ -0,0 +1,63 @@ +import Command +import Foundation +import MockableTest +import TuistCore +import TuistSupport +import TuistSupportTesting + +@testable import Tuist + +final class SimulatorRowViewModelTests: TuistUnitTestCase { + private var subject: SimulatorRowViewModel! + private var simulatorController: MockSimulatorControlling! + private var commandRunner: MockCommandRunning! + + private let iPhone15: SimulatorDeviceAndRuntime = .test( + device: .test( + udid: "iphone-15-id", + name: "iPhone 15" + ) + ) + + override func setUp() { + super.setUp() + + simulatorController = .init() + commandRunner = .init() + subject = SimulatorRowViewModel( + simulatorController: simulatorController, + commandRunner: commandRunner + ) + } + + override func tearDown() { + simulatorController = nil + commandRunner = nil + subject = nil + + super.tearDown() + } + + func test_launchSimulator() async throws { + // Given + given(simulatorController) + .booted(device: .any, forced: .any) + .willReturn(.test()) + given(commandRunner) + .run( + arguments: .value(["open", "-a", "Simulator"]), + environment: .any, + workingDirectory: .any + ) + .willReturn( + .init( + unfolding: { + nil + } + ) + ) + + // When / Then + try await subject.launchSimulator(iPhone15) + } +} diff --git a/app/TuistApp/Tests/SimulatorsViewModelTests.swift b/app/TuistApp/Tests/SimulatorsViewModelTests.swift new file mode 100644 index 00000000000..3653921af6c --- /dev/null +++ b/app/TuistApp/Tests/SimulatorsViewModelTests.swift @@ -0,0 +1,533 @@ +import Foundation +import MockableTest +import TuistAutomation +import TuistCore +import TuistServer +import TuistSupport +import TuistSupportTesting +import XCTest + +@testable import Tuist + +final class SimulatorsViewModelTests: TuistUnitTestCase { + private var subject: SimulatorsViewModel! + private var simulatorController: MockSimulatorControlling! + private var downloadPreviewService: MockDownloadPreviewServicing! + private var fileArchiverFactory: MockFileArchivingFactorying! + private var remoteArtifactDownloader: MockRemoteArtifactDownloading! + private var appBundleLoader: MockAppBundleLoading! + private var appStorage: MockAppStoring! + + private let previewURL = + URL( + string: "tuist:open-preview?server_url=https://cloud.tuist.io&preview_id=01912892-3778-7297-8ca9-d66ac7ee2a53&full_handle=tuist/ios_app_with_frameworks" + )! + + private let iPhone15: SimulatorDeviceAndRuntime = .test( + device: .test( + udid: "iphone-15-id", + name: "iPhone 15" + ) + ) + private let iPhone15Pro: SimulatorDeviceAndRuntime = .test( + device: .test( + udid: "iphone-15-pro-id", + name: "iPhone 15 Pro" + ) + ) + private let appleTV: SimulatorDeviceAndRuntime = .test( + device: .test( + udid: "apple-tv-id", + name: "Apple TV" + ) + ) + + override func setUp() { + super.setUp() + + simulatorController = .init() + downloadPreviewService = .init() + fileArchiverFactory = .init() + remoteArtifactDownloader = .init() + appBundleLoader = .init() + appStorage = .init() + subject = SimulatorsViewModel( + simulatorController: simulatorController, + downloadPreviewService: downloadPreviewService, + fileArchiverFactory: fileArchiverFactory, + remoteArtifactDownloader: remoteArtifactDownloader, + fileHandler: fileHandler, + appBundleLoader: appBundleLoader, + appStorage: appStorage + ) + + Matcher.register(SimulatorDeviceAndRuntime?.self) + Matcher.register([SimulatorDeviceAndRuntime].self) + } + + override func tearDown() { + simulatorController = nil + downloadPreviewService = nil + fileArchiverFactory = nil + remoteArtifactDownloader = nil + appBundleLoader = nil + appStorage = nil + subject = nil + + Matcher.reset() + + super.tearDown() + } + + func test_onAppear_when_appStorage_is_empty_and_no_simulator_is_booted() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(nil) + + let simulators: [SimulatorDeviceAndRuntime] = [ + .test( + device: .test( + name: "iPhone 15" + ) + ), + .test( + device: .test( + name: "iPhone 15 Pro" + ) + ), + ] + + given(simulatorController) + .devicesAndRuntimes() + .willReturn(simulators) + + // When + try await subject.onAppear() + + // Then + XCTAssertEqual(subject.selectedSimulator, nil) + XCTAssertEmpty(subject.pinnedSimulators) + XCTAssertEqual(subject.unpinnedSimulators, simulators) + } + + func test_onAppear_when_appStorage_is_empty_and_a_simulator_is_booted() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(nil) + + let simulators: [SimulatorDeviceAndRuntime] = [ + .test( + device: .test( + name: "iPhone 15" + ) + ), + .test( + device: .test( + state: "Booted", + name: "iPhone 15 Pro" + ) + ), + ] + + given(simulatorController) + .devicesAndRuntimes() + .willReturn(simulators) + + // When + try await subject.onAppear() + + // Then + XCTAssertEqual(subject.selectedSimulator, simulators.last) + XCTAssertEmpty(subject.pinnedSimulators) + XCTAssertEqual(subject.unpinnedSimulators, simulators) + } + + func test_onAppear_when_appStorage_contains_selected_and_pinned_simulators() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn( + [ + appleTV, + iPhone15, + ] + ) + + given(appStorage) + .get(.any as Parameter) + .willReturn(appleTV) + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + + // When + try await subject.onAppear() + + // Then + XCTAssertEqual(subject.selectedSimulator, appleTV) + XCTAssertEqual(subject.pinnedSimulators, [appleTV, iPhone15]) + XCTAssertEqual(subject.unpinnedSimulators, [iPhone15Pro]) + } + + func test_selectSimulator() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(appleTV) + + given(appStorage) + .set(.any as Parameter, value: .any) + .willReturn() + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + // When + subject.selectSimulator(iPhone15Pro) + + // Then + XCTAssertEqual(subject.selectedSimulator, iPhone15Pro) + verify(appStorage) + .set( + .any as Parameter, + value: .value(iPhone15Pro) + ) + .called(1) + } + + func test_pin_simulator() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([iPhone15]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(nil) + + given(appStorage) + .set(.any as Parameter, value: .any) + .willReturn() + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + // When + subject.simulatorPinned(iPhone15Pro, pinned: true) + + // Then + XCTAssertEqual(subject.pinnedSimulators, [iPhone15, iPhone15Pro]) + XCTAssertEqual(subject.unpinnedSimulators, [appleTV]) + verify(appStorage) + .set(.any as Parameter, value: .value([iPhone15, iPhone15Pro])) + .called(1) + } + + func test_unpin_simulator() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([iPhone15]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(nil) + + given(appStorage) + .set(.any as Parameter, value: .any) + .willReturn() + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + // When + subject.simulatorPinned(iPhone15, pinned: false) + + // Then + XCTAssertEqual(subject.pinnedSimulators, []) + XCTAssertEqual(subject.unpinnedSimulators, [appleTV, iPhone15, iPhone15Pro]) + verify(appStorage) + .set(.any as Parameter, value: .value([])) + .called(1) + } + + func test_onChangeOfURL() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([iPhone15]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(iPhone15) + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + given(downloadPreviewService) + .downloadPreview( + .value("01912892-3778-7297-8ca9-d66ac7ee2a53"), + fullHandle: .value("tuist/ios_app_with_frameworks"), + serverURL: .value(Constants.URLs.production) + ) + .willReturn("https://tuist.io/download-link") + + let downloadedArchive = try temporaryPath().appending(component: "archive") + + given(remoteArtifactDownloader) + .download(url: .any) + .willReturn(downloadedArchive) + + let fileUnarchiver = MockFileUnarchiving() + given(fileArchiverFactory) + .makeFileUnarchiver(for: .any) + .willReturn(fileUnarchiver) + + let unarchivedPath = try temporaryPath().appending(component: "unarchived") + + given(fileUnarchiver) + .unzip() + .willReturn(unarchivedPath) + + let appPath = unarchivedPath.appending(component: "App.app") + try fileHandler.touch(appPath) + + given(appBundleLoader) + .load(.any) + .willReturn( + .test( + path: appPath, + infoPlist: .test( + bundleId: "tuist.app", + supportedPlatforms: [ + .simulator(.iOS), + ] + ) + ) + ) + + given(simulatorController) + .booted(device: .any, forced: .any) + .willProduce { device, _ in device } + + given(simulatorController) + .installApp(at: .any, device: .any) + .willReturn() + + given(simulatorController) + .launchApp(bundleId: .any, device: .any, arguments: .any) + .willReturn() + + // When + try await subject.onChangeOfURL(previewURL) + + // Then + verify(simulatorController) + .installApp( + at: .value(appPath), + device: .value(iPhone15.device) + ) + .called(1) + verify(simulatorController) + .launchApp( + bundleId: .value("tuist.app"), + device: .value(iPhone15.device), + arguments: .value([]) + ) + .called(1) + } + + func test_onChangeOfURL_when_no_simulator_selected() async throws { + // When / Then + await XCTAssertThrowsSpecific( + try await subject.onChangeOfURL(previewURL), + SimulatorsViewModelError.noSelectedSimulator + ) + } + + func test_onChangeOfURL_when_deeplink_is_invalid() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([iPhone15]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(iPhone15) + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + let invalidDeeplinkURL = + "tuist:open-preview?server_url=https://cloud.tuist.io&preview_id=01912892-3778-7297-8ca9-d66ac7ee2a53" + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.onChangeOfURL( + try XCTUnwrap(URL(string: invalidDeeplinkURL)) + ), + SimulatorsViewModelError.invalidDeeplink(invalidDeeplinkURL) + ) + } + + func test_onChangeOfURL_when_appDownloadFailed() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([iPhone15]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(iPhone15) + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + given(downloadPreviewService) + .downloadPreview(.any, fullHandle: .any, serverURL: .any) + .willReturn("https://tuist.io/download-link") + + given(remoteArtifactDownloader) + .download(url: .any) + .willReturn(nil) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.onChangeOfURL(previewURL), + SimulatorsViewModelError.appDownloadFailed(previewURL.absoluteString) + ) + } + + func test_onChangeOfURL_when_appNotFound() async throws { + // Given + given(appStorage) + .get(.any as Parameter) + .willReturn([iPhone15]) + + given(appStorage) + .get(.any as Parameter) + .willReturn(iPhone15) + + given(simulatorController) + .devicesAndRuntimes() + .willReturn( + [ + iPhone15, + iPhone15Pro, + appleTV, + ] + ) + try await subject.onAppear() + + given(downloadPreviewService) + .downloadPreview(.any, fullHandle: .any, serverURL: .any) + .willReturn("https://tuist.io/download-link") + + let downloadedArchive = try temporaryPath().appending(component: "archive") + + given(remoteArtifactDownloader) + .download(url: .any) + .willReturn(downloadedArchive) + + let fileUnarchiver = MockFileUnarchiving() + given(fileArchiverFactory) + .makeFileUnarchiver(for: .any) + .willReturn(fileUnarchiver) + + let unarchivedPath = try temporaryPath().appending(component: "unarchived") + + given(fileUnarchiver) + .unzip() + .willReturn(unarchivedPath) + + try fileHandler.touch(unarchivedPath.appending(component: "App.app")) + + given(appBundleLoader) + .load(.any) + .willReturn( + .test( + infoPlist: .test( + supportedPlatforms: [ + .device(.visionOS), + .simulator(.visionOS), + ] + ) + ) + ) + + // When / Then + await XCTAssertThrowsSpecific( + try await subject.onChangeOfURL(previewURL), + SimulatorsViewModelError.appNotFound(iPhone15, [.visionOS]) + ) + } +} diff --git a/assets/companies/Bending Spoons - Logo, black.svg b/assets/companies/Bending Spoons - Logo, black.svg deleted file mode 100644 index f2b534ccc97..00000000000 --- a/assets/companies/Bending Spoons - Logo, black.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/assets/companies/GetYourGuide.svg b/assets/companies/GetYourGuide.svg deleted file mode 100644 index 199b9c47e81..00000000000 --- a/assets/companies/GetYourGuide.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/companies/Trendyol_online.png b/assets/companies/Trendyol.png similarity index 100% rename from assets/companies/Trendyol_online.png rename to assets/companies/Trendyol.png diff --git a/assets/companies/altel.svg b/assets/companies/altel.svg new file mode 100644 index 00000000000..b000eeeb02d --- /dev/null +++ b/assets/companies/altel.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/companies/angry_nerds_logo_black.svg b/assets/companies/angrynerds.svg similarity index 100% rename from assets/companies/angry_nerds_logo_black.svg rename to assets/companies/angrynerds.svg diff --git a/assets/companies/Asana.png b/assets/companies/asana.png similarity index 100% rename from assets/companies/Asana.png rename to assets/companies/asana.png diff --git a/assets/companies/BendingSpoons-white.png b/assets/companies/bendingspoons-darkmode.png similarity index 100% rename from assets/companies/BendingSpoons-white.png rename to assets/companies/bendingspoons-darkmode.png diff --git a/assets/companies/BendingSpoons-black.png b/assets/companies/bendingspoons.png similarity index 100% rename from assets/companies/BendingSpoons-black.png rename to assets/companies/bendingspoons.png diff --git a/assets/companies/codemagic-logo.svg b/assets/companies/codemagic.svg similarity index 100% rename from assets/companies/codemagic-logo.svg rename to assets/companies/codemagic.svg diff --git a/assets/companies/Compass Black Logo.png b/assets/companies/compass.png similarity index 100% rename from assets/companies/Compass Black Logo.png rename to assets/companies/compass.png diff --git a/assets/companies/crunchyroll_logo_vertical.svg b/assets/companies/crunchyroll.svg similarity index 100% rename from assets/companies/crunchyroll_logo_vertical.svg rename to assets/companies/crunchyroll.svg diff --git a/assets/companies/Depop Logo.svg b/assets/companies/depop.svg similarity index 100% rename from assets/companies/Depop Logo.svg rename to assets/companies/depop.svg diff --git a/assets/companies/DodoPizzaLogo.svg b/assets/companies/dodopizza.svg similarity index 100% rename from assets/companies/DodoPizzaLogo.svg rename to assets/companies/dodopizza.svg diff --git a/assets/companies/emerge-tools-vertical-white.svg b/assets/companies/emergetools-darkmode.svg similarity index 100% rename from assets/companies/emerge-tools-vertical-white.svg rename to assets/companies/emergetools-darkmode.svg diff --git a/assets/companies/emerge-tools-vertical-black.svg b/assets/companies/emergetools.svg similarity index 100% rename from assets/companies/emerge-tools-vertical-black.svg rename to assets/companies/emergetools.svg diff --git a/assets/companies/emplate_logo_full_black.svg b/assets/companies/emplate.svg similarity index 100% rename from assets/companies/emplate_logo_full_black.svg rename to assets/companies/emplate.svg diff --git a/assets/companies/getyourguide.png b/assets/companies/getyourguide.png new file mode 100644 index 00000000000..6c73734dd03 Binary files /dev/null and b/assets/companies/getyourguide.png differ diff --git a/assets/companies/Hedvig.svg b/assets/companies/hedvig.svg similarity index 100% rename from assets/companies/Hedvig.svg rename to assets/companies/hedvig.svg diff --git a/assets/companies/hh mono.svg b/assets/companies/hh.svg similarity index 100% rename from assets/companies/hh mono.svg rename to assets/companies/hh.svg diff --git a/assets/companies/izi.svg b/assets/companies/izi.svg new file mode 100644 index 00000000000..7955eeef89a --- /dev/null +++ b/assets/companies/izi.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/companies/kinopoisk_logo.svg b/assets/companies/kinopoisk.svg similarity index 100% rename from assets/companies/kinopoisk_logo.svg rename to assets/companies/kinopoisk.svg diff --git a/assets/companies/OlimpBet Logo.svg b/assets/companies/olimpbet.svg similarity index 100% rename from assets/companies/OlimpBet Logo.svg rename to assets/companies/olimpbet.svg diff --git a/assets/companies/olx.png b/assets/companies/olx.png new file mode 100644 index 00000000000..244b53f5707 Binary files /dev/null and b/assets/companies/olx.png differ diff --git a/assets/companies/olx.svg b/assets/companies/olx.svg deleted file mode 100644 index 51a2674a225..00000000000 --- a/assets/companies/olx.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/assets/companies/ozontech.svg b/assets/companies/ozontech.svg new file mode 100644 index 00000000000..5bf57768d7a --- /dev/null +++ b/assets/companies/ozontech.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/companies/smlab.svg b/assets/companies/smlab.svg new file mode 100644 index 00000000000..fe04267bdd0 --- /dev/null +++ b/assets/companies/smlab.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/companies/stream-white.png b/assets/companies/stream-darkmode.png similarity index 100% rename from assets/companies/stream-white.png rename to assets/companies/stream-darkmode.png diff --git a/assets/companies/takeout_central.svg b/assets/companies/takeoutcentral.svg similarity index 100% rename from assets/companies/takeout_central.svg rename to assets/companies/takeoutcentral.svg diff --git a/assets/companies/tele2.svg b/assets/companies/tele2.svg new file mode 100644 index 00000000000..70df773f78f --- /dev/null +++ b/assets/companies/tele2.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/companies/TV2_Logo.svg b/assets/companies/tv2.svg similarity index 100% rename from assets/companies/TV2_Logo.svg rename to assets/companies/tv2.svg diff --git a/assets/companies/VK_logo.svg b/assets/companies/vk.svg similarity index 100% rename from assets/companies/VK_logo.svg rename to assets/companies/vk.svg diff --git a/assets/companies/Wefox_2021.png b/assets/companies/wefox.png similarity index 100% rename from assets/companies/Wefox_2021.png rename to assets/companies/wefox.png diff --git a/assets/companies/yandexTravel.svg b/assets/companies/yandexTravel.svg new file mode 100644 index 00000000000..c55f5961c3b --- /dev/null +++ b/assets/companies/yandexTravel.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/favicon.ico b/assets/favicon.ico index 98133190a16..c38147e182e 100644 Binary files a/assets/favicon.ico and b/assets/favicon.ico differ diff --git a/assets/header.gif b/assets/header.gif deleted file mode 100644 index ebcca03f1b4..00000000000 Binary files a/assets/header.gif and /dev/null differ diff --git a/assets/header.jpg b/assets/header.jpg new file mode 100644 index 00000000000..58e637b8f49 Binary files /dev/null and b/assets/header.jpg differ diff --git a/codemagic.yaml b/codemagic.yaml new file mode 100644 index 00000000000..79d2551869e --- /dev/null +++ b/codemagic.yaml @@ -0,0 +1,332 @@ +workflows: + lint: + name: Lint + max_build_duration: 60 + environment: + xcode: 15.3 + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Lint + script: mise run lint + triggering: &branch_triggering + events: + - push + - pull_request + branch_patterns: + - pattern: '*' + include: true + cancel_previous_builds: true + triggering: &main_triggering + events: + - push + branch_patterns: + - pattern: 'main' + include: true + cancel_previous_builds: true + spm_build: + name: SPM Build + max_build_duration: 60 + environment: + xcode: 15.3 + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Build + script: swift build --configuration debug + triggering: + << : *branch_triggering + build_docs: + name: Build Documentation + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Install dependencies + script: mise run install + - name: Generate manifest docs + script: mise run docs:generate-manifests-docs + - name: Build the documentation + script: mise run docs:build + triggering: + << : *branch_triggering + unit_tests: + name: Unit tests + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + cache: + cache_paths: + - $CM_BUILD_DIR/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Install Tuist dependencies + script: mise x -- tuist install + - name: Run tests + script: mise x -- tuist test TuistUnitTests + triggering: + << : *branch_triggering + cache: + name: Cache + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + cache: + cache_paths: + - $CM_BUILD_DIR/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Install Tuist dependencies + script: mise x -- tuist install + - name: Print hashes + script: mise x -- tuist cache --print-hashes + - name: Cache + script: mise x -- tuist cache + triggering: + << : *main_triggering + + deploy_docs: + name: Deploy Documentation + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + echo "CLOUDFLARE_ACCOUNT_ID=cc0237353f2f825680b0463629cd4a86" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Install dependencies + script: mise run install + - name: Generate manifest docs + script: mise run docs:generate-manifests-docs + - name: Build the documentation + script: mise run docs:build + - name: Deploy to Cloudflare Pages + script: | + mise x npm:wrangler@latest -- wrangler pages deploy --commit-hash $CM_COMMIT --branch $CM_BRANCH --project-name tuist-docs docs/.vitepress/dist + triggering: + events: + - push + branch_patterns: + - pattern: 'main' + include: true + cancel_previous_builds: true + + # FIXME: Merge all the schemes into one and remove all of the following workflows + tuist_automation_acceptance_tests: + name: Tuist automation acceptance tests + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + cache: + cache_paths: + - $CM_BUILD_DIR/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Skip Xcode Macro Fingerprint Validation + script: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + - name: Skip Xcode Package Validation + script: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Install Tuist dependencies + script: mise x -- tuist install + - name: Run tests + script: mise x -- tuist test TuistAutomationAcceptanceTests + triggering: + << : *branch_triggering + + tuist_dependencies_acceptance_tests: + name: Tuist dependencies acceptance tests + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + cache: + cache_paths: + - $CM_BUILD_DIR/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Skip Xcode Macro Fingerprint Validation + script: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + - name: Skip Xcode Package Validation + script: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Install Tuist dependencies + script: mise x -- tuist install + - name: Run tests + script: mise x -- tuist test TuistDependenciesAcceptanceTests + triggering: + << : *branch_triggering + + tuist_generator_acceptance_tests: + name: Tuist generator acceptance tests + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + cache: + cache_paths: + - $CM_BUILD_DIR/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Skip Xcode Macro Fingerprint Validation + script: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + - name: Skip Xcode Package Validation + script: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Install Tuist dependencies + script: mise x -- tuist install + - name: Run tests + script: mise x -- tuist test TuistGeneratorAcceptanceTests + triggering: + << : *branch_triggering + + tuist_kit_acceptance_tests: + name: TuistKit acceptance tests + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist + cache: + cache_paths: + - $CM_BUILD_DIR/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + script: | + curl https://mise.run | sh + mise install + - name: Skip Xcode Macro Fingerprint Validation + script: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + - name: Skip Xcode Package Validation + script: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Install Tuist dependencies + script: mise x -- tuist install + - name: Run tests + script: mise x -- tuist test TuistKitAcceptanceTests + triggering: + << : *branch_triggering + app_tests: + name: App tests + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist-app + cache: + cache_paths: + - $CM_BUILD_DIR/app/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + working_directory: $CM_BUILD_DIR/app + script: | + curl https://mise.run | sh + mise install + - name: Skip Xcode Macro Fingerprint Validation + script: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + - name: Skip Xcode Package Validation + script: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Install Tuist dependencies + working_directory: $CM_BUILD_DIR/app + script: mise x -- tuist install + - name: Run tests + working_directory: $CM_BUILD_DIR/app + script: mise x -- tuist test + triggering: + << : *branch_triggering + warm_app_cache: + name: Warm app cache + max_build_duration: 60 + environment: + xcode: 15.3 + groups: + - tuist-app + cache: + cache_paths: + - $CM_BUILD_DIR/app/.build + scripts: + - name: Set environment variables + script: | + echo "MISE_EXPERIMENTAL=1" >> $CM_ENV + - name: Install Mise + working_directory: $CM_BUILD_DIR/app + script: | + curl https://mise.run | sh + mise install + - name: Install Tuist dependencies + working_directory: $CM_BUILD_DIR/app + script: mise x -- tuist install + - name: Print hashes + working_directory: $CM_BUILD_DIR/app + script: mise x -- tuist cache --print-hashes + - name: Cache + working_directory: $CM_BUILD_DIR/app + script: mise x -- tuist cache + triggering: + << : *main_triggering \ No newline at end of file diff --git a/docs/.github/workflows/deploy.yml b/docs/.github/workflows/deploy.yml new file mode 100644 index 00000000000..44dc2f0d467 --- /dev/null +++ b/docs/.github/workflows/deploy.yml @@ -0,0 +1,25 @@ +name: Deploy +on: + push: + branches: + - 'main' + +concurrency: + group: deploy-${{ github.head_ref }} + cancel-in-progress: true + + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: jdx/mise-action@v2 + with: + experimental: true + - run: pnpm install + - run: mise run deploy \ No newline at end of file diff --git a/docs/.github/workflows/docs.yml b/docs/.github/workflows/docs.yml new file mode 100644 index 00000000000..c1de322a515 --- /dev/null +++ b/docs/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: Docs + +on: + push: + branches: + - main + pull_request: + merge_group: + +concurrency: + group: docs-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: 'ubuntu-latest' + timeout-minutes: 15 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: jdx/mise-action@v2 + with: + experimental: true + - run: pnpm install + - run: mise run build \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000000..6365118ed5e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,177 @@ +# Created by https://www.toptal.com/developers/gitignore/api/osx,node +# Edit at https://www.toptal.com/developers/gitignore?templates=osx,node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### OSX ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +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 + +# End of https://www.toptal.com/developers/gitignore/api/osx,node + +.vitepress/cache/ + +docs/generated \ No newline at end of file diff --git a/docs/.vitepress/badges.mjs b/docs/.vitepress/badges.mjs new file mode 100644 index 00000000000..0674a6fcf79 --- /dev/null +++ b/docs/.vitepress/badges.mjs @@ -0,0 +1,7 @@ +export function comingSoonBadge() { + return `Coming soon`; +} + +export function requiresAccount() { + return `Account required`; +} diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs new file mode 100644 index 00000000000..348feb44f90 --- /dev/null +++ b/docs/.vitepress/config.mjs @@ -0,0 +1,167 @@ +import { defineConfig } from "vitepress"; +import * as path from "node:path"; +import * as fs from "node:fs/promises"; +import { + guidesSidebar, + contributorsSidebar, + referencesSidebar, +} from "./sidebars.mjs"; + +export default defineConfig({ + title: "Tuist", + titleTemplate: ":title | Tuist", + description: "Scale your Xcode app development", + srcDir: "docs", + lastUpdated: true, + locales: { + root: { + label: "English", + lange: "en", + }, + }, + cleanUrls: true, + head: [ + [ + "script", + {}, + ` + !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n + + +`; +} + +export function cube02Icon(size = 15) { + return ` + + +`; +} + +export function cube01Icon(size = 15) { + return ` + + + + `; +} + +export function barChartSquare02Icon(size = 15) { + return ` + + + `; +} + +export function code02Icon(size = 15) { + return ` + + +`; +} + +export function dataIcon(size = 15) { + return ` + + + + +`; +} + +export function checkCircleIcon(size = 15) { + return ` + + +`; +} + +export function tuistIcon(size = 15) { + return ` + +`; +} + +export function cloudBlank02Icon(size = 15) { + return ` + + +`; +} + +export function server04Icon(size = 15) { + return ` + + +`; +} diff --git a/docs/.vitepress/sidebars.mjs b/docs/.vitepress/sidebars.mjs new file mode 100644 index 00000000000..510afe6f23f --- /dev/null +++ b/docs/.vitepress/sidebars.mjs @@ -0,0 +1,341 @@ +import { comingSoonBadge } from "./badges.mjs"; +import { + cubeOutlineIcon, + cube02Icon, + cube01Icon, + barChartSquare02Icon, + code02Icon, + dataIcon, + checkCircleIcon, + tuistIcon, + cloudBlank02Icon, + server04Icon, +} from "./icons.mjs"; +import examplesDataLoader from "../docs/references/examples/examples.data"; +import projectDescriptionTypesDataLoader from "../docs/references/project-description/types.data"; +import cliDataLoader from "../docs/references/cli/commands.data"; + +const projectDescriptionTypesData = projectDescriptionTypesDataLoader.load(); + +const projectDescriptionSidebar = { + text: "Project Description", + collapsed: true, + items: [], +}; + +function capitalize(text) { + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); +} + +["structs", "enums", "extensions", "typealiases"].forEach((category) => { + if (projectDescriptionTypesData.find((item) => item.category === category)) { + projectDescriptionSidebar.items.push({ + text: capitalize(category), + collapsed: true, + items: projectDescriptionTypesData + .filter((item) => item.category === category) + .map((item) => ({ + text: item.title, + link: `/references/project-description/${item.identifier}`, + })), + }); + } +}); + +function generateNestedSidebarItems(items) { + const nestedItems = {}; + + items.forEach((item) => { + const category = item.category; + if (!nestedItems[category]) { + nestedItems[category] = { + text: capitalize(category), + collapsed: true, + items: [], + }; + } + nestedItems[category].items.push({ + text: item.title, + link: `/references/cli/${item.command}`, + }); + }); + + function isLinkItem(item) { + return typeof item.link === "string"; + } + + function convertToArray(obj) { + return Object.values(obj).reduce((acc, item) => { + if (Array.isArray(item.items) && item.items.every(isLinkItem)) { + acc.push(item); + } else { + acc.push({ + text: item.text, + collapsed: true, + items: convertToArray(item.items), + }); + } + return acc; + }, []); + } + + return convertToArray(nestedItems); +} + +const cliData = cliDataLoader.load(); + +const cliSidebar = { + text: "CLI", + items: generateNestedSidebarItems(cliData), +}; + +export const referencesSidebar = [ + { + text: "Reference", + items: [ + cliSidebar, + projectDescriptionSidebar, + { + text: "Examples", + collapsed: true, + items: examplesDataLoader.load().map((item) => { + return { + text: item.title, + link: `/references/examples/${item.name}`, + }; + }), + }, + { + text: "Migrations", + collapsed: true, + items: [ + { + text: "From v3 to v4", + link: "/references/migrations/from-v3-to-v4", + }, + ], + }, + ], + }, +]; + +export const contributorsSidebar = [ + { + text: "Contributors", + items: [ + { + text: "Get started", + link: "/contributors/get-started", + }, + { + text: "Issue reporting", + link: "/contributors/issue-reporting", + }, + { + text: "Code reviews", + link: "/contributors/code-reviews", + }, + { + text: "Principles", + link: "/contributors/principles", + }, + ], + }, +]; + +export const guidesSidebar = [ + { + text: `Quick start ${tuistIcon()}`, + link: "/", + items: [ + { + text: "Install Tuist", + link: "/guides/quick-start/install-tuist", + }, + { + text: "Create a project", + link: "/guides/quick-start/create-a-project", + }, + { + text: "Add dependencies", + link: "/guides/quick-start/add-dependencies", + }, + { + text: "Gather insights", + link: "/guides/quick-start/gather-insights", + }, + { + text: "Optimize workflows", + link: "/guides/quick-start/optimize-workflows", + }, + ], + }, + { + text: `Start ${cubeOutlineIcon()}`, + items: [ + { + text: "Create a new project", + link: "/guides/start/new-project", + }, + { + text: "Try with a Swift Package", + link: "/guides/start/swift-package", + }, + { + text: "Migrate", + collapsed: true, + items: [ + { + text: "An Xcode project", + link: "/guides/start/migrate/xcode-project", + }, + { + text: "A Swift Package", + link: "/guides/start/migrate/swift-package", + }, + { + text: "An XcodeGen project", + link: "/guides/start/migrate/xcodegen-project", + }, + { + text: "A Bazel project", + link: "/guides/start/migrate/bazel-project", + }, + ], + }, + ], + }, + { + text: `Develop ${cube02Icon()}`, + items: [ + { + text: `Projects ${code02Icon()}`, + collapsed: true, + link: "guides/develop/projects", + items: [ + { + text: "Manifests", + link: "guides/develop/projects/manifests", + }, + { + text: "Directory structure", + link: "guides/develop/projects/directory-structure", + }, + { + text: "Editing", + link: "guides/develop/projects/editing", + }, + { + text: "Dependencies", + link: "guides/develop/projects/dependencies", + }, + { + text: "Code sharing", + link: "guides/develop/projects/code-sharing", + }, + { + text: "Synthesized files", + link: "guides/develop/projects/synthesized-files", + }, + { + text: "Dynamic configuration", + link: "guides/develop/projects/dynamic-configuration", + }, + { + text: "Templates", + link: "guides/develop/projects/templates", + }, + { + text: "Plugins", + link: "guides/develop/projects/plugins", + }, + { + text: "Hashing", + link: "guides/develop/projects/hashing", + }, + { + text: "The cost of convenience", + link: "guides/develop/projects/cost-of-convenience", + }, + { + text: "Modular architecture", + link: "guides/develop/projects/tma-architecture", + }, + ], + }, + { + text: `Build ${dataIcon()}`, + link: "guides/develop/build", + collapsed: true, + items: [ + { + text: "Cache", + link: "guides/develop/build/cache", + }, + ], + }, + { + text: `Test ${checkCircleIcon()}`, + link: "guides/develop/test", + collapsed: true, + items: [ + { + text: "Smart runner", + link: "guides/develop/test/smart-runner", + }, + { + text: "Flakiness", + link: "guides/develop/test/flakiness", + }, + ], + }, + { + text: `Automate ${cloudBlank02Icon()}`, + collapsed: true, + items: [ + { + text: `Continuous Integration`, + link: "guides/develop/automate/continuous-integration", + }, + { + text: `Workflows ${comingSoonBadge()}`, + link: "guides/develop/automate/workflows", + }, + ], + }, + ], + }, + { + text: `Share ${cube01Icon()}`, + items: [ + { + text: "Previews", + link: "guides/share/previews", + }, + ], + }, + // { + // text: `Measure ${barChartSquare02Icon()} ${comingSoonBadge()}`, + // items: [], + // }, + { + text: `Dashboard ${server04Icon()}`, + collapsed: true, + items: [ + { + text: "On-premise", + collapsed: true, + items: [ + { + text: "Install", + link: "guides/dashboard/on-premise/install", + }, + { + text: "Metrics", + link: "guides/dashboard/on-premise/metrics", + }, + ], + }, + ], + }, +]; diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 00000000000..865c25d2663 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,10 @@ +.VPLink { + overflow: scroll; + scrollbar-width: none; /* Firefox */ + + &::-webkit-scrollbar { + /* WebKit */ + width: 0; + height: 0; + } +} diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 00000000000..e8c2960a070 --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1,7 @@ +import DefaultTheme from "vitepress/theme"; +import "./custom.css"; +/** @type {import('vitepress').Theme} */ +export default { + extends: DefaultTheme, + enhanceApp({ app }) {}, +}; diff --git a/docs/Package.resolved b/docs/Package.resolved deleted file mode 100644 index e65252d0740..00000000000 --- a/docs/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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 deleted file mode 100644 index fd6ef01fadf..00000000000 --- a/docs/Package.swift +++ /dev/null @@ -1,21 +0,0 @@ -// 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/README.md b/docs/README.md deleted file mode 100644 index 54beab78e3b..00000000000 --- a/docs/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Contributor docs - -This directory contains documentation for Tuist contributors - -- [Benchmarking](./benchmarking.md) \ No newline at end of file diff --git a/docs/Sources/tuist/ProjectDescription b/docs/Sources/tuist/ProjectDescription deleted file mode 120000 index 3ace68863b9..00000000000 --- a/docs/Sources/tuist/ProjectDescription +++ /dev/null @@ -1 +0,0 @@ -../../../Sources/ProjectDescription \ No newline at end of file diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-deployment.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-deployment.tutorial deleted file mode 100644 index a2ff31e8652..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-deployment.tutorial +++ /dev/null @@ -1,104 +0,0 @@ -@Tutorial(time: 25) { - @Intro(title: "Deployment") { - Learn how to deploy Tuist Cloud's Docker image to production. - @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") - } - - @Section(title: "Pulling the image from GitHub's registry") { - @ContentAndMedia(layout: "horizontal") {} - - For our enterprise clients, we grant access to the repository located at [github.com/tuist/cloud](https://github.com/cloud/cloud). This repository also comes with a linked container registry for pulling images. Currently, the container registry allows authentication only as an individual user. Therefore, users with repository access must generate a **personal access token** within the Tuist organization, ensuring they have the necessary permissions to read packages. After submission, we will promptly approve this token. - - > Important: Using a personal access token presents a challenge because it's associated with an individual who might eventually depart from the enterprise organization. GitHub recognizes this limitation and is actively developing a solution to allow GitHub apps to authenticate with app-generated tokens. - - ### Pulling the Docker image - - After generating the token, you can retrieve the image by executing the following command: - - ```bash - echo $TOKEN | docker login ghcr.io -u USERNAME --password-stdin - docker pull ghcr.io/tuist/tuist:latest - ``` - } - - @Section(title: "Deploying a Docker image") { - @ContentAndMedia(layout: "horizontal") {} - - The deployment process for the Docker image will differ based on your chosen cloud provider and your organization's continuous deployment approach. Since most cloud solutions and tools, like [Kubernetes](https://kubernetes.io/), utilize Docker images as fundamental units, the examples in this section should align well with your existing setup. - - We recommend establishing a continuous deployment pipeline that operates daily, pulling and deploying fresh images. This ensures you consistently benefit from the latest improvements. If preferred, you can synchronize with Tuist Cloud's release schedule and deploy weekly, coinciding with each new official release. - - Subsequent sections provide examples of deployments using popular cloud providers. - - > Important: If your deployment pipeline needs to validate that the server is up and running, you can send a `GET` HTTP request to `/ready` and assert a `200` status code in the response. - - ### Docker - - You can deploy your app using docker directly. Firstly, set up a Postgres database and get the connection URL to it. - - You can then deploy the actual Tuist Cloud app via: - ```sh - docker run -d ghcr.io/tuist/tuist:latest -e DATABASE_URL=**** - ``` - - ### Fly - - To deploy the app on [Fly](https://fly.io/), you'll require a fly.toml configuration file. Consider generating it dynamically within your Continuous Deployment (CD) pipeline. Below is a reference example for your use: - - ```toml - app = "tuist-cloud-enterprise-example" - primary_region = "ams" - kill_signal = "SIGINT" - kill_timeout = "5s" - - [experimental] - auto_rollback = true - - [build] - - [deploy] - release_command = "bin/rails fly:release" - - [env] - PORT = "8080" - - [processes] - app = "bin/rails fly:server" - worker = "bundle exec que" - - [[services]] - protocol = "tcp" - internal_port = 8080 - auto_stop_machines = true - auto_start_machines = true - min_machines_running = 1 - processes = ["app"] - - [[services.ports]] - port = 80 - handlers = ["http"] - force_https = true - - [[services.ports]] - port = 443 - handlers = ["tls", "http"] - [services.concurrency] - type = "connections" - hard_limit = 25 - soft_limit = 20 - - [[services.tcp_checks]] - interval = "15s" - timeout = "2s" - grace_period = "1s" - - [[statics]] - guest_path = "/app/public" - url_prefix = "/" - ``` - - Then you can run `fly launch --local-only --no-deploy` to launch the app. On subsequent deploys, instead of running `fly launch --local-only`, you will need to run `fly deploy --local-only`. Fly.io doesn't allow to pull private Docker images, which is why we need to use the `--local-only` flag. - } - - -} diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-environment.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-environment.tutorial deleted file mode 100644 index 6db888d1565..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-environment.tutorial +++ /dev/null @@ -1,87 +0,0 @@ -@Tutorial(time: 15) { - @Intro(title: "Environment configuration") { - In this chapter you'll learn how to configure Tuist Cloud through environment variables. - @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") - } - - @Section(title: "Environment variables") { - @ContentAndMedia(layout: "horizontal") {} - - After ensuring the system-level requirements are met, it's time to set up the production environment. - - The configuration of Tuist Cloud is done using **environment variables**, which must be present in the service's operating environment. Given the sensitive nature of these variables, we advise encrypting and storing them in secure password management solutions. Rest assured, Tuist Cloud handles these variables with utmost care, ensuring they are never displayed in logs. - - > Note: The necessary variables are verified at startup. If any are missing, the launch will fail and the error message will detail the absent variables. - } - - @Section(title: "Base configuration") { - @ContentAndMedia(layout: "horizontal") {} - - Below is a table listing the environment variables essential for the base configuration: - - | Environment variable | Description | Required | Default | Example | - | --- | --- | --- | --- | --- | - | `DATABASE_URL` | The URL to access the Postgres database. Note that the URL should contain the authentication information | Yes | | `postgres://username:password@cloud.us-east-2.aws.test.com/production` | - | `TUIST_APP_URL` | The base URL to access the instance from the Internet | Yes | | https://cloud.tuist.io | - | `TUIST_SECRET_KEY_BASE` | The key to use to encrypt information (e.g. sessions in a cookie) | Yes | | | `c5786d9f869239cbddeca645575349a570ffebb332b64400c37256e1c9cb7ec831345d03dc0188edd129d09580d8cbf3ceaf17768e2048c037d9c31da5dcacfa` | - | `TUIST_SECRET_KEY_PASSWORD` | The key to use to encrypt a password when storing it | No | `$TUIST_SECRET_KEY_BASE` | | - | `TUIST_SECRET_KEY_TOKENS` | The key to use to encrypt tokens when storing them (e.g. token for a confirmation email) | No | `$TUIST_SECRET_KEY_BASE` | | - } - - @Section(title: "Authentication") { - @ContentAndMedia(layout: "horizontal") {} - - We facilitate authentication through [identity providers (IdP)](https://en.wikipedia.org/wiki/Identity_provider). To utilize this, ensure all necessary environment variables for the chosen provider are present in the Tuist Cloud's operating environment. Missing variables will result in Tuist Cloud bypassing that provider. - - > Note: For integrations with IdPs not currently supported, please reach out to us at [cloud@tuist.io](mailto:cloud@tuist.io). - - #### GitHub - - We recommend authenticating using a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps) but you can also use the [OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). Make sure to include all essential environment variables specified by GitHub in the Tuist Cloud environment. Absent variables will cause Tuist Cloud to overlook the GitHub authentication. To properly set up the GitHub app: - - In the GitHub app's general settings: - - Copy the `Client ID` and set it as `TUIST_GITHUB_OAUTH_ID` - - Create and copy a new `client secret` and set it as `TUIST_GITHUB_OAUTH_SECRET` - - Set the `Callback URL` as `http://YOUR_APP_URL/users/auth/github/callback`. `YOUR_APP_URL` can also be your server's IP address. - - In the `Permissions and events`'s `Account permissions` section, set the `Email addresses` permission to `Read-only`. - - | Environment variable | Description | Required | Default | Example | - | --- | --- | --- | --- | --- | - | `TUIST_GITHUB_OAUTH_ID` | The client ID of the application | Yes | | `Iv1.a629723000043722` | - | `TUIST_GITHUB_OAUTH_SECRET` | The client secret of the application | Yes | | `232f972951033b89799b0fd24566a04d83f44ccc` | - - #### Okta - - You can enable authentication with Okta through the [OAuth 2.0](https://oauth.net/2/) protocol. You'll have to [create an app](https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/#create-an-oauth-2-0-app-in-okta) on Okta, and set the following environment variables: - - | Environment variable | Description | Required | Default | Example | - | --- | --- | --- | --- | --- | - | `TUIST_OKTA_SITE` | The URL of your Okta organization | Yes | | `https://your-org.okta.com` | - | `TUIST_OKTA_CLIENT_ID` | The client ID to authenticate against Okta | Yes | | | - | `TUIST_OKTA_CLIENT_SECRET` | The client secret to authenticate against Okta | Yes | | | - | `TUIST_OKTA_AUTHORIZE_URL` | The authorize URL | No | `{OKTA_SITE}/oauth2//v1/authorize` | | - | `TUIST_OKTA_TOKEN_URL` | The token URL | No | `{OKTA_SITE}/oauth2//v1/token` | | - | `TUIST_OKTA_USER_INFO_URL` | The token URL | No | `{OKTA_SITE}/oauth2//v1/userinfo` | | - - } - - - @Section(title: "Storage") { - @ContentAndMedia(layout: "horizontal") {} - - Tuist Cloud needs storage to house artifacts uploaded through the API. It's **essential to configure one of the supported storage solutions** for Tuist Cloud to operate effectively. - - #### AWS - - | Environment variable | Description | Required | Default | Example | - | --- | --- | --- | --- | --- | - | TUIST_AWS_ACCESS_KEY_ID | The access key identifier | Yes | | `AKIAA2LQP3CCOZ6WT6CF` | - | TUIST_AWS_SECRET_ACCESS_KEY | The access key secret | Yes | | `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` | - | TUIST_AWS_BUCKET_NAME | Name of the AWS bucket | Yes | | `my-bucket` | - | TUIST_AWS_REGION | The bucket's AWS region | No | `eu-west-1` | `us-east-1` | - | TUIST_AWS_ENDPOINT | Custom AWS endpoint | No | `https://amazonaws.com` | `https://custom-domain.com` | - } - - - - -} diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/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 deleted file mode 100644 index bf4f59a1e67..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-infrastructure-requirements.tutorial +++ /dev/null @@ -1,35 +0,0 @@ -@Tutorial(time: 5) { - @Intro(title: "Infrastructure") { - In this chapter you'll learn about what you'll need at the infrastructure level to be able to host Tuist Cloud. - @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") - } - - @Section(title: "Docker") { - @ContentAndMedia(layout: "horizontal") {} - - We distribute Tuist Cloud as a **Docker image** via [GitHub's Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry). We utilize GitHub's Container Registry to synchronize the authorization for registry access with access to this repository. In essence, if you have access to this repository, you can download the Tuist Cloud Enterprise images. - - To run it, **your infrastructure must support running Docker images**. Note that most infrastructure providers support it because it's become the standard container for distributing and running software in production environments. - } - - @Section(title: "Postgres Database") { - @ContentAndMedia(layout: "horizontal") {} - - In addition to running the Docker images, you'll need a [**Postgres database**](https://www.postgresql.org/) to store relational data. Most infrastructure providers include Posgres databases in their offering (e.g., [AWS](https://aws.amazon.com/rds/postgresql/) & [Google Cloud](https://cloud.google.com/sql/docs/postgres)). - - > Note: The Docker image includes a workflow to automate the migration of schemas. - - } - - @Section(title: "Storage") { - @ContentAndMedia(layout: "horizontal") {} - - You'll also need a solution to **store large files** (e.g. framework and library binaries). The following solutions are supported: - - - [AWS S3](https://aws.amazon.com/s3/) - - If you need support for additional storage solutions let us know at [cloud@tuist.io](mailto:cloud@tuist.io). - } - - -} diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/tuist-cloud-tutorials.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/tuist-cloud-tutorials.tutorial deleted file mode 100644 index a375402f743..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/tuist-cloud-tutorials.tutorial +++ /dev/null @@ -1,16 +0,0 @@ -@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. - } - - @Chapter(name: "Self-hosting") { - Discover how to set up your infrastructure for deploying and continuously updating Tuist Cloud. - - @Image(source: "Tuist Cloud Enterprise Server.png", alt: "An illustration that shows the icon of a server") - - @TutorialReference(tutorial: "doc:enterprise-infrastructure-requirements") - @TutorialReference(tutorial: "doc:enterprise-environment") - @TutorialReference(tutorial: "doc:enterprise-deployment") - } -} - diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/binary-caching.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/binary-caching.md deleted file mode 100644 index d3e8ad84baa..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/binary-caching.md +++ /dev/null @@ -1,63 +0,0 @@ -# Binary Caching - -Speeding up your clean build and test runs across environments - -## Overview - -Xcode's build system is designed for incremental builds, enhancing efficiency under normal circumstances. However, this feature falls short in Continuous Integration (CI) environments, where data essential for incremental builds is not shared across different builds. Additionally, developers often reset this data locally to troubleshoot complex compilation problems, leading to more frequent clean builds. This results in teams spending excessive time waiting for local builds to finish or for Continuous Integration pipelines to provide feedback on pull requests. Furthermore, the frequent context switching in such an environment compounds this unproductiveness. - -Tuist addresses these challenges effectively with its binary caching feature. This tool optimizes the build process by caching compiled binaries, significantly reducing build times both in local development and CI environments. This approach not only accelerates feedback loops but also minimizes the need for context switching, ultimately boosting productivity. - -### Cache warming - -Tuist efficiently utilizes **fingerprints** for each target in the dependency graph to detect changes. Utilizing this data, it builds and assigns unique identifiers to binaries derived from these targets. At the time of graph generation, Tuist then seamlessly substitutes the original targets with their corresponding binary versions. - -This operation, known as *"warming,"* produces binaries for local use or for sharing with teammates and CI environments via Tuist Cloud. The process of warming the cache is straightforward and can be initiated with a simple command: - - -```bash -tuist cache warm -``` - -### Using the cache binaries - -By default, when Tuist commands necessitate project generation, they automatically substitute dependencies with their binary equivalents from the cache, if available. Additionally, if you specify a list of targets to focus on, Tuist will also replace any dependent targets with their cached binaries, provided they are available. For those who prefer a different approach, there is an option to opt out of this behavior entirely by using a specific flag: - -```bash -tuist generate # Only dependencies -tuist generate Search # Dependencies + Search dependencies -tuist generate Search Settings # Dependencies, and Search and Settings dependencies -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: - -```bash -tuist cloud auth # Authenticate -tuist cloud organization create my-organization # Create organization -tuist cloud project create my-project -o my-organization # Create a project -``` - -After creating the project, modify your `Tuist/Config.swift` file to reference the new project: - -```swift -import ProjectDescription - -let config = Config(cloud: .cloud(projectId: "my-organization/my-project")) -``` - -Developers on your team can access the cache if they are authenticated and added as members of the organization, which you can do using the Tuist CLI. For CI environments, authentication is managed differently; it's done using **project-scoped tokens**. These tokens possess restricted permissions compared to those of the organization, including the ability to warm the cache with binaries. To obtain this token, you can execute the following command: - - -```swift -tuist cloud project token my-project -o my-organization -``` - -You will then need to set the token as an environment variable named `TUIST_CONFIG_CLOUD_TOKEN` to make it accessible. - -> Tip: While utilizing the cache for release builds is feasible, we advise restricting binary usage to debug builds only. This approach ensures absolute certainty that the compiled code corresponds exactly to the version intended for release. - 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 deleted file mode 100644 index fb70fafe7f4..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/selective-testing.md +++ /dev/null @@ -1,34 +0,0 @@ -# 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/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/tuist-cloud.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/tuist-cloud.md deleted file mode 100644 index c11c7c1aecc..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/tuist-cloud.md +++ /dev/null @@ -1,49 +0,0 @@ -# What is Tuist Cloud - -Addressing large-scale challenges necessitates persisting state and service integration, for which Tuist Cloud is key. This paid solution is vital for Tuist project integration and its open-source project's longevity. - -## Overview - -Utilizing a graph of dependencies for representing projects and converting them into Xcode projects showed that this approach could be **foundational for optimizing workflows**, thereby preventing unnecessary time wastage for organization's developers. Key features that leverage this foundational approach include **local binary caching**, which enables the generation of projects with some targets replaced by their pre-compiled binary counterparts, and selective testing, which allows running tests only for the targets impacted by changes. - -As we advanced towards enhancing productivity, it became evident that **certain solutions necessitated an HTTP server for state storage and building integrations with other HTTP services** like [GitHub](https://github.com), [Slack](https://slack.com), or [Apple Store Connect](https://appstoreconnect.apple.com/). For instance, caching could leverage a server to share binaries across local and CI environments, thereby accelerating build and test times. This realization led to the creation of Tuist Cloud. - -Tuist Cloud, a closed-source paid service, enhances Tuist by adding server-requisite functionalities. Integration of Tuist projects with Tuist Cloud not only augments existing functionalities but also introduces new ones. This service encapsulates years of experience in developing tools for mobile developers at [Shopify](https://shopify.com) (e.g., [Mobile Tophat](https://shopify.engineering/mobile-tophatting-at-shopify-1), [Mobile Release Engineering at Scale](https://shopify.engineering/mobile-release-engineering-scale-shipit-mobile), [Scaling iOS CI with Anka](https://shopify.engineering/scaling-ios-ci-with-anka)) and is envisioned as **the copilot for your platform teams**. Our objective is to help organizations cultivate a productive development environment. - -> Note: Similar to many other open-source projects, Tuist also necessitated full-time dedicated personnel to adequately meet the demand for support and feature requests. Tuist Cloud plays a crucial role in fulfilling this requirement by enabling the financing of full-time personnel for the project. - -## Features - -### Available - -#### 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. - -#### 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. - -> Info: You might be familiar with Spotify's open-source tool, [XCMetrics](https://xcmetrics.io/). While it shares a similar objective, its integration demands extra tool installations in developers' settings, and it lacks the ability to correlate data with project specifics. In contrast, Tuist offers enhanced analytics, drawing from the synergy between data and the project graph, and is seamlessly integrated without needing any extra installations. - -### In development - -#### 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: - -- Introduce instability in the test suite. -- Modify build settings, causing a target's compilation time to double. -- Add a new static target to the graph, leading to a substantial increase in the final app size. - -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. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/championing-projects.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/championing-projects.md deleted file mode 100644 index 6ef34f3420a..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/championing-projects.md +++ /dev/null @@ -1,27 +0,0 @@ -# Championing Projects - -This page proposes a light-weight framework to propose, execute, and roll-out projects in Tuist. Projects move through 2 phases: **Explore** and **Build**. - -## Explore - -This phase starts with an idea. It's usually a user need or problem to be tackled. The goal of this phase is three-fold: - -1. Identify **what** we are trying to solve. -2. Justify **why** it's worth to solve it. -3. **Align the core team** with a proposed solution. - -The outcome of this phase must be a discussions on the RFCs category of the [community forum](https://github.com/tuist/tuist/discussions/categories/rfcs). The proposal should follow the [default template](https://github.com/tuist/tuist/discussions/2189). The Tuist core team, maintainers, and contributors will dump any concerns or thoughts about the proposed solution. The goal of the discussion is to seek alignment and introduce any necessary modifications to achieve that. - -## Roles - -As part of the explore phase, you should identify who will be the **steward**, and if there are **contributors** that want to join the project. The list below describes what the responsibilities of each role are: - -- **Champion:** It's the person proposing the project, and they'll be responsible for ensuring that the project moves forward. -- **Contributor:** Contributors are Tuist users or contributors interested in the proposed solution and would like to participate in its execution. -- **Steward:** A person from the core team that ensures that the project's execution aligns with Tuist’s design principles and best practices. It’s also the point person to answer any question that might arise. - -## Build - -The goal of this phase is to implement the proposed solution. You must create a **GitHub Milestone** with the project's name and a reference to the topic on Discourse. Moreover, you must break down the project into **small tasks represented by GitHub issues** and assign them to the milestone. If there are contributors to the project other than the champion, we recommend creating a public channel in Slack with the following naming convention `#project-xxx`. - -A project is **done** when the code is implemented, well structured and written, tested, and follows Tuist's conventions and best practices. Moreover, the documentation must be updated accordingly. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/code-reviews.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/code-reviews.md deleted file mode 100644 index 842e9b1866c..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/code-reviews.md +++ /dev/null @@ -1,62 +0,0 @@ -# Code Reviews - -Reviewing pull requests is a common type of contribution. -Despite continuous integration (CI) ensuring the code does what's supposed to do, it's not enough. -There are contribution traits that can't be automated: _design, code structure & architecture, tests quality, or typos._ - -This document puts together traits that we should look out for when reviewing pull requests: - -## 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. -Suggest splitting the code into smaller abstractions that are easier to understand. -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 - -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 - -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 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 are a bad user experience for users of Tuist. -Contributions should avoid introducing breaking changes unless it's strictly necessary. -There are many language features that we can leverage to evolve the interface of Tuist without resorting to a breaking change. -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 - -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 - -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. - -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 - -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/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/get-started-as-contributor.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/get-started-as-contributor.md deleted file mode 100644 index fc24a578bab..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/get-started-as-contributor.md +++ /dev/null @@ -1,56 +0,0 @@ -# Get Started - -Tuist is a command line interface (CLI), written in Swift, that helps developers maintain and interact with their Xcode projects. -It also abstracts them from the complexities that Xcode exposes. - -If you have experience building apps for Apple platforms, like iOS, adding code to Tuist shouldn't be much different. There are two differences compared to developing apps that are worth mentioning: - -- **The interactions with CLIs happen through the terminal.** - The user executes Tuist, which performs the desired task, and then returns successfully or with an error code. - During the execution, the user can be notified by sending output information to the standard output and standard error. - There are no gestures, or graphical interactions, just the user intent. -- **There's no runloop** that keeps the process alive waiting for input, like it happens in an iOS app when the app receives system or user events. - CLIs run in its process and finishes when the work is done. - Asynchronous work can be done using system APIs like `DispatchQueue`, but need to make sure the process is running while the asynchronous work is being executed. - Otherwise, the process will terminate the asynchronous work. - -If you don't have any experience with Swift, we recommend [Apple's official book](https://docs.swift.org/swift-book/) to get familiar with the language and the most used elements from the Foundation's API. - -## Minimum requirements - -To contribute to Tuist, minimum requirements are: - -- `macOS 14.0+` -- `Xcode 15.0+` - -## Set up the project locally - -To start working on the project, we can follow the steps below: - -- Clone the repository by running: `git clone git@github.com:tuist/tuist.git` -- Set up [asdf](https://asdf-vm.com/) to ensure a stable local environment. -- Run `tuist fetch` to get all the external dependencies needed by tuist -- Run `tuist generate` to generate the Tuist Xcode project using tuist itself - -The generated project opens automatically. If you need to open again without generating it, run `open Tuist.xcworkspace` (or ue Finder). Note that `xed .` will open the package, and not the project generated by tuist. - - -### Edit tuist with tuist - -To edit Tuist manifests, run `tuist edit`. - -To run other commands, you can use `swift run tuist command` (tuist with current changes) or `tuist run command` (tuist in your system). -If you have not built `ProjectDescription` before (or you have recently changed the framework), you might need to run `swift build --product ProjectDescription` before. - -You can also leverage all the other tuist features that you know and love with the additional benefit of being able to validate your current changes on a complex project. If _any_ feature does not work as expected, feel free to raise an issue. - -### Run Tuist from Xcode - -You can run Tuist from Xcode like you'd do from your terminal. - -With the project opened in Xcode: - -- Edit the `tuist` scheme: - - In the **Arguments** tab inside the **Run** section specify the arguments that you'd like to pass to Tuist - - Change the **working directory** in the **options** section to point to the directory that contains the project -- Run the `tuist` scheme diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/manifesto.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/manifesto.md deleted file mode 100644 index 82efd68d830..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/manifesto.md +++ /dev/null @@ -1,69 +0,0 @@ -# Manifesto - -This page describes principles that are pillars to the design and development of Tuist. They evolve with the project and are meant to ensure a sustainable growth that is well-aligned with the project foundation. - -Here follows a list of the most important principles: - -## 1. Default to conventions - -One of the reasons why Tuist exists is because Xcode is weak in conventions and that leads to complex projects that are hard to scale up and maintain. For that reason, Tuist takes a different approach by defaulting to simple and thoroughly designed conventions. Developers can opt-out from the conventions, but that’s a conscious decision that doesn’t feel natural. - -For example, there’s a convention for defining dependencies between targets by using the provided public interface. By doing that, Tuist ensures that the projects are generated with the right configurations for the linking to work. Developers have the option to define the dependencies through build settings, but they’d be doing it implicitly and therefore breaking Tuist features such as "graph" that rely on some conventions being followed. - -The reason why we default to conventions is that the more decision we can make on behalf of the developers, the more focus they’ll have crafting features for their apps. When we are left with no conventions like it’s the case in many projects, we have to make decisions that will end up not being consistent with other decisions and as a consequence, there’ll be an accidental complexity that will be hard to manage. - -## 2. Manifests are the source of truth - -Having many layers of configurations and contracts between them results in a project setup that is hard to reason about and maintain. Think for a second on an average project. The definition of the project lives in the _.xcodeproj_ directories, the CLI in scripts _(e.g Fastfiles)_, and the CI logic in pipelines. Those are three layers with contracts between them that we need to maintain. How often have you been in a situation where you changed something in your projects, and then a week later you realized that the release scripts broke? - -We can simplify this by having a single source of truth, the manifest files. Those files provide Tuist with the information that it needs to generate Xcode projects that developers can use to edit their files. Moreover, it allows having standard commands for building projects from a local or CI environment. - -Tuist should own the complexity and expose a simple, safe, and enjoyable interface to describe their projects as explicitly as possible. - -## 3. Make the implicit explicit - -Xcode supports implicit configurations. A good example of that is inferring the implicitly defined dependencies. While implicitness is fine for small projects, where configurations are simple, as projects get larger it might cause slowness or odd behaviors. - -Tuist should provide explicit APIs for implicit Xcode behaviors. It should also support defining Xcode implicitness but implemented in such a way that encourages developers to opt for the explicit approach. Supporting Xcode implicitness and intricacies facilitates the adoption of Tuist, after which teams can take some time to get rid of the implicitness. - -The definition of dependencies is a good example of that. While developers can define dependencies through build settings and phases, Tuist provides a beautiful API that encourages its adoption. - -Designing the API to be explicit allows Tuist to run some checks on the projects that otherwise wouldn’t be possible. Moreover, it enables features like “tuist graph”, which exports a representation of the dependency graph, or “tuist cache”, which caches all the frameworks as `.xcframeworks`. - -We should treat each request to port features from Xcode as an opportunity to simplify concepts with simple and explicit APIs. - -## 4. Keep it simple - -One of the main challenges when scaling Xcode projects comes from the fact that Xcode exposes a lot of complexity to the users. Due to that, teams have a high bus factor and only a few people in the team understand the project and the errors that the build system throws. That’s a bad situation to be in because the team relies on a few people. - -Xcode is a great tool, but so many years of improvements, new platforms, and programming languages, are reflected on their surface, which struggled to remain simple. - -Tuist should take the opportunity to keep things simple because working on simple things is fun and motivates us. No one wants to spend time trying to debug an error that happens at the very end of the compilation process, or understanding why they are not able to run the app on their devices. Xcode delegates the tasks to its underlying build system and in some cases it does a very poor job translating errors into actionable items. Have you ever got a “framework X not found” error and you didn’t know what to do? Imagine if we got a list of potential root causes for the bug. - -## 5. Start from the developer's experience - -Part of the reason why there is a lack of innovation around Xcode, or put differently, not as much as in other programming environments, is because **we often start analyzing problems from existing solutions.** As a consequence, most of the solutions that we find nowadays revolve around the same ideas and workflows. While it’s good to include existing solutions in the equations, we should not let them constrain our creativity. - -We like to think as [Tom Preston](https://tom.preston-werner.com/) puts it in [this podcast](https://tom.preston-werner.com/): _"Most things can be achieved, whatever you have in your head you can probably pull off with code as long as is possible within the constrains of the universe"_. If we imagine **how we'd like the developer experience to be**, it's just a matter of time to pull it off — by starting to analyze the problems from the developer experience gives us a unique point of view that will lead us to solutions that users will love to use. - -We might feel tempted to follow what everyone is doing, even if that means sticking with the inconveniences that everyone continues to complain about. Let's not do that. _How do I imagine archiving my app? How would I love code signing to be? What processes can I help streamline with Tuist?_ For example, adding support for [Fastlane](https://fastlane.tools) is a solution to a problem that we need to understand first. We can get to the root of the problem by asking "why" questions. Once we narrow down where the motivation comes from, we can think of how Tuist can help them best. Maybe the solution is integrating with Fastlane, but it's important we don't disregard other equally valid solutions that we can put on the table before making trade-offs. - -## 6. Errors can and will happen - -We, developers, have an inherent temptation to disregard that errors can happen. -As a result, we design and test software only considering the ideal scenario. - -Swift, its type system, and a well-architected code might help prevent some errors, but not all of them because some are out of our control. -We can’t assume the user will always have an internet connection, or that the system commands will return successfully. -The environments in which Tuist runs are not sandboxes that we control, and hence we need to make an effort to understand how they might change and impact Tuist. - -Poorly handled errors result in bad user experience, and users might lose trust in the project. -We want users to enjoy every single piece of Tuist, even the way we present errors to them. - -We should put ourselves in the shoes of users and imagine what we’d expect the error to tell us. -If the programming language is the communication channel through which errors propagate, and the users are the destination of the errors, they should be written in the same language that the target (users) speak. -They should include enough information to know what happened and hide the information that is not relevant. -Also, they should be actionable by telling users what steps they can take to recover from them. - -And last but not least, our test cases should contemplate failing scenarios. -Not only they ensure that we are handling errors as we are supposed to, but prevent future developers from breaking that logic. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/reporting-bugs.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/reporting-bugs.md deleted file mode 100644 index cf07b73dcc7..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/reporting-bugs.md +++ /dev/null @@ -1,30 +0,0 @@ -# Reporting Bugs - -This document contains simple guidelines that users can follow to report bugs found while using the tool. -One of the most important pieces of bug reports are reproducible cases because they help developers narrow down the issue for further investigation. - -## Reproducible cases - -### What is a reproducible test case? - -A reproducible test case is a small Tuist project to demonstrate a problem - often this problem is caused by a bug in Tuist. Your reproducible test case should contain the bare minimum features needed to clearly demonstrate the bug. - -### Why should you create a reproducible test case? - -A reproducible test case lets you isolate the cause of a problem, which is the first step towards fixing it! The most important part of any bug report is to describe the exact steps needed to reproduce the bug. - -A reproducible test case is a great way to share a specific environment that causes a bug. Your reproducible test case is the best way to help people that want to help you. - -### Steps to create a reproducible test case - -- Create a new git repository. -- Initialize a project using `tuist init` in the repository directory. -- Add the code needed to recreate the error you’ve seen. -- Publish the code _(your GitHub account is a good place to do this)_ and then link to it when creating an issue. - -### Benefits of reproducible test cases - -- **Smaller surface area:** By removing everything but the error, you don’t have to dig to find the bug. -- **No need to publish secret code:** You might not be able to publish your main site (for many reasons). Remaking a small part of it as a reproducible test case allows you to publicly demonstrate a problem without exposing any secret code. -- **Proof of the bug:** Sometimes a bug is caused by some combination of settings on your machine. A reproducible test case allows contributors to pull down your build and test it on their machines as well. This helps verify and narrow down the cause of a problem. -- **Get help with fixing your bug:** If someone else can reproduce your problem, they often have a good chance of fixing the problem. It’s almost impossible to fix a bug without first being able to reproduce it. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/config.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/config.md deleted file mode 100644 index c73c498cf2f..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/config.md +++ /dev/null @@ -1,45 +0,0 @@ -# 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 deleted file mode 100644 index c3cdc0df680..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/project.md +++ /dev/null @@ -1,38 +0,0 @@ -# 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 deleted file mode 100644 index 711c05b11a7..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/workspace.md +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index a249e44f571..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift +++ /dev/null @@ -1,21 +0,0 @@ -// 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/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 19cb9db09de..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Project.swift +++ /dev/null @@ -1,20 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "App", - organizationName: "tuist.io", - targets: [ - Target( - name: "App", - destinations: [.iPhone], - product: .app, - bundleId: "io.tuist.app", - deploymentTargets: .iOS("13.0"), - infoPlist: .default, - sources: ["Targets/App/Sources/**"], - dependencies: [ - .external(name: "Alamofire"), - ] - ), - ] -) 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 deleted file mode 100644 index 9cb332aa834..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-generate.sh +++ /dev/null @@ -1,6 +0,0 @@ -$ 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 deleted file mode 100644 index 723178c34ce..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-gitignore.txt +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 3535fc039a2..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/external-dependencies.tutorial +++ /dev/null @@ -1,66 +0,0 @@ -@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/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index e8a85ef20f1..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-edit.txt +++ /dev/null @@ -1,4 +0,0 @@ -% tuist edit -Generating workspace Manifests.xcworkspace -Generating project Manifests -Opening Xcode to edit the project. Press CTRL + C once you are done editing 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 deleted file mode 100644 index 7e52e2a0148..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-fetch.sh +++ /dev/null @@ -1,5 +0,0 @@ -$ tuist fetch - -Resolving and fetching dependencies. -Installing Swift Package Manager dependencies. -... diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/create-project.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/create-project.tutorial deleted file mode 100644 index 7b37520c01f..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/create-project.tutorial +++ /dev/null @@ -1,80 +0,0 @@ -@Tutorial(time: 5) { - @Intro(title: "Creating your First Project") { - Learn how to create a new Tuist project from scratch. - - @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") - } - - @Section(title: "Creating the project") { - @ContentAndMedia { - Creating a new project using Tuist is very simple as it provides us with templates which makes everything quite smooth. - } - - @Steps { - @Step { - Create a new directory for your project. - @Code(name: "console", file: "make-directory.txt", reset: true) - - > Note: The folder name would be used to name your project automatically. - } - - @Step { - Now, you can create a new project by running the `tuist init` command. The init command will bootstrap an iOS application, which includes the Info.plist files, an AppDelegate.swift, a tests file, and a Project.swift that contains the definition of the project. - @Code(name: "console", file: "tuist-init.txt") - - > Note: If you have used the Swift Package Manager before, the Project.swift file is the equivalent to the Package.swift. - } - - @Step { - To create a SwiftUI project, you can specify the SwiftUI template. - @Code(name: "console", file: "tuist-init-swiftui.txt", previousFile: "make-directory.txt") - } - } - } - - @Section(title: "Exploring project files") { - @ContentAndMedia { - Take a moment to check out the files generated and whether anything needs to be changed to suit your needs. - } - - @Steps { - @Step { - If you open the `Project.swift` file, you can see something like this. By default, tuist provides you a template with which you can have a modular app. - @Code(name: "Project.swift", file: "initial-project-file.swift", reset: true) - - > Tip: You can run `tuist edit` to generate a temporary Xcode project with all the project manifests and the project description helpers, so you will be able to edit the whole project configuration. - } - - @Step { - If you are curious, you can explore the generated files. The `Project+Templates.swift` file looks very much like the options you usually see in an Xcodeproj file. Many of these properties might be familiar to you, feel free to change things and if you would like to add something that's missing, you can check the reference for `ProjectDescription`. - @Code(name: "Project+Templates.swift", file: "inital-project-templates.swift", reset: true) - - > Tip: You can build the manifest project before ending your edits, to make sure you have not done any mistakes in project setup. Once you are happy with the project settings, you can close this window and press `^+C` to terminate the edit command. - } - } - } - - @Section(title: "Generating the Xcode project") { - @ContentAndMedia { - If you noticed from the previous section, we were missing one crucial file, the Xcode project. Without which Xcode cannot open our project, so let's see what we can do to fix that. - } - - @Steps { - @Step { - Tuist comes with a command to generate projects and workspaces from your manifest files. We can run this command in terminal and we'll get our MyApp.xcodeproj and MyApp.xcworkspace files. - @Code(name: "console", file: "tuist-generate.txt", reset: true) - } - - @Step { - Once, the project files have been generated, it would open Xcode automatically, and you should be able to run your project in the simulator successfully. You would get success messages like this in the console. - @Code(name: "console", file: "initial-run-output.txt", reset: true) - } - - @Step { - Last but not least, you might want to include a badge in your project's README to indicate that the project is defined using Tuist. - @Code(name: "README.md", file: "tuist-added-readme.txt", previousFile: "initial-readme.txt") - } - } - } -} - diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 1cd046a7089..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/inital-project-templates.swift +++ /dev/null @@ -1,87 +0,0 @@ -import ProjectDescription - -/// Project helpers are functions that simplify the way you define your project. -/// Share code to create targets, settings, dependencies, -/// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks" -/// See https://docs.tuist.io/guides/helpers/ - -extension Project { - /// Helper function to create the Project for this ExampleApp - public static func app(name: String, destinations: Destinations, additionalTargets: [String]) -> Project { - var targets = makeAppTargets( - name: name, - destinations: destinations, - dependencies: additionalTargets.map { TargetDependency.target(name: $0) } - ) - targets += additionalTargets.flatMap { - makeFrameworkTargets(name: $0, destinations: destinations) - } - return Project( - name: name, - organizationName: "tuist.io", - targets: targets - ) - } - - // MARK: - Private - - /// Helper function to create a framework target and an associated unit test target - private static func makeFrameworkTargets(name: String, destinations: Destinations) -> [Target] { - let sources = Target( - name: name, - destinations: destinations, - product: .framework, - bundleId: "io.tuist.\(name)", - infoPlist: .default, - sources: ["Targets/\(name)/Sources/**"], - resources: [], - dependencies: [] - ) - let tests = Target( - name: "\(name)Tests", - destinations: destinations, - product: .unitTests, - bundleId: "io.tuist.\(name)Tests", - infoPlist: .default, - sources: ["Targets/\(name)/Tests/**"], - resources: [], - dependencies: [.target(name: name)] - ) - return [sources, tests] - } - - /// Helper function to create the application target and the unit test target. - private static func makeAppTargets(name: String, destinations: Destinations, dependencies: [TargetDependency]) -> [Target] { - let platform: Platform = platform - let infoPlist: [String: InfoPlist.Value] = [ - "CFBundleShortVersionString": "1.0", - "CFBundleVersion": "1", - "UIMainStoryboardFile": "", - "UILaunchStoryboardName": "LaunchScreen", - ] - - let mainTarget = Target( - name: name, - destinations: destinations, - product: .app, - bundleId: "io.tuist.\(name)", - infoPlist: .extendingDefault(with: infoPlist), - sources: ["Targets/\(name)/Sources/**"], - resources: ["Targets/\(name)/Resources/**"], - dependencies: dependencies - ) - - let testTarget = Target( - name: "\(name)Tests", - destinations: destinations, - product: .unitTests, - bundleId: "io.tuist.\(name)Tests", - infoPlist: .default, - sources: ["Targets/\(name)/Tests/**"], - dependencies: [ - .target(name: "\(name)"), - ] - ) - return [mainTarget, testTarget] - } -} diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 37930a0d53b..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-project-file.swift +++ /dev/null @@ -1,31 +0,0 @@ -import MyPlugin -import ProjectDescription -import ProjectDescriptionHelpers - -/* - +-------------+ - | | - | App | Contains MyApp App target and MyApp unit-test target - | | - +------+-------------+-------+ - | depends on | - | | - +----v-----+ +-----v-----+ - | | | | - | Kit | | UI | Two independent frameworks to share code and start modularising your app - | | | | - +----------+ +-----------+ - - */ - -// MARK: - Project - -// Local plugin loaded -let localHelper = LocalHelper(name: "MyPlugin") - -// Creates our project using a helper function defined in ProjectDescriptionHelpers -let project = Project.app( - name: "MyApp", - destinations: [.iPhone, .iPad, .macWithiPadDesign], - additionalTargets: ["MyAppKit", "MyAppUI"] -) diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 475f9e7c8af..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-readme.txt +++ /dev/null @@ -1,3 +0,0 @@ -# MyApp - -This is the description of MyApp, which was awarded the best app of the year. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 4d844140a3d..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-run-output.txt +++ /dev/null @@ -1,2 +0,0 @@ -Hello, from your Kit framework -Hello, from your UI framework diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index ed198472677..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/make-directory.txt +++ /dev/null @@ -1,3 +0,0 @@ -% mkdir MyApp -% cd MyApp - diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 1e006ff728c..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-added-readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -# MyApp - -[![Tuist badge](https://img.shields.io/badge/Powered%20by-Tuist-blue)](https://tuist.io) -This is the description of MyApp, which was awarded the best app of the year. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 664cf24f503..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-generate.txt +++ /dev/null @@ -1,6 +0,0 @@ -% tuist generate -Resolved cache profile 'Development' from Tuist's defaults -Generating workspace MyApp.xcworkspace -Generating project MyApp -Project generated. -Total time taken: 4.787s diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index a91ec3c035b..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init-swiftui.txt +++ /dev/null @@ -1,5 +0,0 @@ -% mkdir MyApp -% cd MyApp - -% tuist init --platform ios --template swiftui -Project generated at path ~/Desktop/MyApp. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/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 deleted file mode 100644 index 2a3ae2e8110..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init.txt +++ /dev/null @@ -1,5 +0,0 @@ -% mkdir MyApp -% cd MyApp - -% tuist init --platform ios -Project generated at path ~/Desktop/MyApp. 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 deleted file mode 100644 index da63097c1d3..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install-rtx.sh +++ /dev/null @@ -1,2 +0,0 @@ -$ 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 deleted file mode 100644 index 45bc42bb5d2..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install.tutorial +++ /dev/null @@ -1,41 +0,0 @@ -@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 deleted file mode 100644 index fbe8311124e..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-install-tuist.sh +++ /dev/null @@ -1,13 +0,0 @@ -$ 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 deleted file mode 100644 index 8d0a24b039b..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-shell.sh +++ /dev/null @@ -1,11 +0,0 @@ -$ 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/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-help.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-help.sh deleted file mode 100644 index 97adc5c14e6..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-help.sh +++ /dev/null @@ -1,55 +0,0 @@ -$ 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. - -$ tuist -OVERVIEW: Generate, build and test your Xcode projects. - -USAGE: tuist [--help-env] - -OPTIONS: - --help-env Display subcommands to manage the environment tuist - versions. - -h, --help Show help information. - -SUBCOMMANDS: - build Builds a project - cache A set of utilities related to the caching of targets. - clean Clean all the artifacts stored locally - dump Outputs the manifest as a JSON - edit Generates a temporary project to edit the project in - the current directory - fetch Fetches any remote content necessary to interact with - the project. - generate Generates an Xcode workspace to start working on the - project. - graph Generates a graph from the workspace or project in - the current directory - init Bootstraps a project - cloud A set of commands to interact with the cloud. - migration A set of utilities to assist in the migration of - Xcode projects to Tuist. - plugin A set of commands for plugin's management. - run Runs a scheme or target in the project - scaffold Generates new project based on a template - signing A set of commands for signing-related operations - test Tests a project - version Outputs the current version of tuist - - See 'tuist help ' for detailed help. - 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 deleted file mode 100644 index 352f2fd2912..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-version.sh +++ /dev/null @@ -1,18 +0,0 @@ -$ 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/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/tuist-tutorials.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/tuist-tutorials.tutorial deleted file mode 100644 index 6b85fd21349..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/tuist-tutorials.tutorial +++ /dev/null @@ -1,15 +0,0 @@ -@Tutorials(name: "Tuist") { - @Intro(title: "Tutorials") { - Tuist is a command line tool (CLI) that aims to facilitate the generation, maintenance, and interaction with Xcode projects. These tutorials are written to walk you through from the basics to advanced concepts of tuist right from setting up a simple project to building at massive scales. - } - - @Chapter(name: "Get Started with Tuist") { - Get started right away on starting up a fresh project using the powerful templates inbuilt in tuist. - - @Image(source: "Logo.png", alt: "An illustration that shows the Tuist logo.") - - @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 deleted file mode 100644 index 2a05209b1d8..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/command-line-interface.md +++ /dev/null @@ -1,81 +0,0 @@ -# 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 deleted file mode 100644 index dce1a9a2260..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/dependencies.md +++ /dev/null @@ -1,119 +0,0 @@ -# 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 deleted file mode 100644 index 3bced922a7e..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/extensions.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 4620b363403..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/installation.md +++ /dev/null @@ -1,158 +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: [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 deleted file mode 100644 index 6c9cae852b1..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/migration-guidelines.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index cee74942091..00000000000 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/project-structure.md +++ /dev/null @@ -1,41 +0,0 @@ -# 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. - - + +## 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 [caching](/guides/develop/build/cache) and [smart test runs](/guides/develop/test/smart-runner). + +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. + +To add external dependencies, you'll have to create a `Package.swift` either under `Tuist/` or at the root of the project. + +::: code-group +```swift [Tuist/Package.swift] +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST + import ProjectDescription + import ProjectDescriptionHelpers + + let packageSettings = PackageSettings( + productTypes: [ + "Alamofire": .framework, // default is .staticFramework + ] + ) + +#endif + +let package = Package( + name: "PackageName", + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), + + ] +) +``` +::: + +> [!TIP] PACKAGE SETTINGS +> The `PackageSettings` instance wrapped in a compiler directive allows you to configure how packages are integrated. For example, in the example above it's used to override the default product type used for packages. By default, you shouldn't need it. + +The `Package.swift` file is just an interface to declare external dependencies, nothing else. That's why you don't define any targets or products in the package. Once you have the dependencies defined, you can run the following command to resolve and pull the dependencies into the `Tuist/Dependencies` directory: + +```bash +tuist install +# Resolving and fetching dependencies. +# Installing Swift Package Manager dependencies. +``` + +As you might have noticed, we take an approach similar to [CocoaPods](https://cocoapods.org)', where the resolution of dependencies is its own command. This gives control to the users over when they'd like dependencies to be resolved and updated, and allows opening the Xcode in project and have it ready to compile. This is an area where we believe the developer experience provided by Apple's integration with the Swift Package Manager degrates over time as the project grows. + +From your project targets you can then reference those dependencies using the `TargetDependency.external` dependency type: + +::: code-group +```swift [Project.swift] +import ProjectDescription + +let project = Project( + name: "App", + organizationName: "tuist.io", + targets: [ + .target( + name: "App", + destinations: [.iPhone], + product: .app, + bundleId: "io.tuist.app", + deploymentTargets: .iOS("13.0"), + infoPlist: .default, + sources: ["Targets/App/Sources/**"], + dependencies: [ + .external(name: "Alamofire"), // [!code ++] + ] + ), + ] +) +``` +::: + +> [!NOTE] NO SCHEMES GENERATED FOR EXTERNAL PACKAGES +> The **schemes** are not automatically created for Swift Package projects to keep the schemes list clean. You can create them via Xcode's UI. + +#### 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. + +> [!WARNING] SPM Build Tool Plugins +> SPM build tool plugins must be declared using [Xcode's default integration](#xcode-s-default-integration) mechanism, even when using Tuist's [XcodeProj-based integration](#tuist-s-xcodeproj-based-integration) for your project dependencies. + +A practical application of an SPM build tool plugin is performing code linting during Xcode's "Run Build Tool Plug-ins" build phase. In a package manifest this is defined as follows: + +```swift +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "Framework", + products: [ + .library(name: "Framework", targets: ["Framework"]), + ], + dependencies: [ + .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", .upToNextMajor(from: "0.56.1")), + ], + targets: [ + .target( + name: "Framework", + plugins: [ + .plugin(name: "SwiftLint", package: "SwiftLintPlugin"), + ] + ), + ] +) +``` + +To generate an Xcode project with the build tool plugin intact, you must declare the package in the project manifest's `packages` array, and then include a package with type `.plugin` in a target's dependencies. + +```swift +import ProjectDescription + +let project = Project( + name: "Framework", + packages: [ + .remote(url: "https://github.com/SimplyDanny/SwiftLintPlugins", requirement: .upToNextMajor(from: "0.56.1")), + ], + targets: [ + .target( + name: "Framework", + dependencies: [ + .package(product: "SwiftLintBuildToolPlugin", type: .plugin), + ] + ), + ] +) +``` + +### 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] BUILD AND TEST +> If you build and test your project through `tuist build` and `tuist test`, you will similarly need to ensure that the Carthage-resolved dependencies are present by running the `carthage update` command before `tuist build` or `tuist test` are run. + +### 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. + +## Static or dynamic + +Frameworks and libraries can be linked either statically or dynamically, **a choice that has significant implications for aspects like app size and boot time**. Despite its importance, this decision is often made without much consideration. + +The **general rule of thumb** is that you want as many things as possible to be statically linked in release builds to achieve fast boot times, and as many things as possible to be dynamically linked in debug builds to achieve fast iteration times. + +The challenge with changing between static and dynamic linking in a project graph is that is not trivial in Xcode because a change has cascading effect on the entire graph (e.g. libraries can't contain resources, static frameworks don't need to be embedded). Apple tried to solve the problem with compile time solutions like Swift Package Manager's automatic decision between static and dynamic linking, or [Mergeable Libraries](https://developer.apple.com/documentation/xcode/configuring-your-project-to-use-mergeable-libraries). However, this adds new dynamic variables to the compilation graph, adding new sources of non-determinism, and potentially causing some features like Swift Previews that rely on the compilation graph to become unreliable. + +Luckily, Tuist conceptually compresses the complexity associated with changing between static and dynamic and synthesizes [bundle accessors](/guides/develop/projects/synthesized-files#bundle-accessors) that are standard across linking types. In combination with [dynamic configurations via environment variables](/guides/develop/projects/dynamic-configuration), you can pass the linking type at invocation time, and use the value in your manifests to set the product type of your targets. + +```swift +// Use the value returned by this function to set the product type of your targets. +func productType() -> Product { + if case let .string(linking) = Environment.linking { + return linking == "static" ? .staticFramework : .framework + } else { + return .framework + } +} +``` + +Note that Tuist [does not default to convenience through implicit configuration due to its costs](/guides/develop/projects/cost-of-convenience). What this means is that we rely on you setting the linking type and any additional build settings that are sometimes required, like the [`-ObjC` linker flag](https://github.com/pointfreeco/swift-composable-architecture/discussions/1657#discussioncomment-4119184), to ensure the resulting binaries are correct. Therefore, the stance that we take is providing you with the resources, usually in the shape of documentation, to make the right decisions. + +> [!TIP] EXAMPLE: COMPOSABLE ARCHITECTURE +> A Swift Package that many projects integrate is [Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture). As described [here](https://github.com/pointfreeco/swift-composable-architecture/discussions/1657#discussioncomment-4119184) and the [troubleshooting section](#troubleshooting), you'll need to set the `OTHER_LDFLAGS` build setting to `$(inherited) -ObjC` when linking the packages statically, which is Tuist's default linking type. Alternatively, you can override the product type for the package to be dynamic. + +### Scenarios + +There are some scenarios where setting the linking entirely to static or dynamic is not feasible or a good idea. The following is a non-exhaustive list of scenarios where you might need to mix static and dynamic linking: + +- **Apps with extensions:** Since apps and their extensions need to share code, you might need to make those targets dynamic. Otherwise, you'll end up with the same code duplicated in both the app and the extension, causing the binary size to increase. +- **Pre-compiled external dependencies:** Sometimes you are provided with pre-compiled binaries that are either static or dynamic. Static can binaries can be wrapped in dynamic frameworks or libraries to be linked dynamically. + +When making changes to the graph, Tuist will analyze it and display a warning if it detects a "static side effect". This warning is meant to help you identify issues that might arise from linking a target statically that depends transitively on a static target through dynamic targets. These side effects often manifest as increased binary size or, in the worst cases, runtime crashes. + +## Troubleshooting + +### Objective-C Dependencies + +When integrating Objective-C dependencies, the inclusion of certain flags on the consuming target may be necessary to avoid runtime crashes as detailed in [Apple Technical Q&A QA1490](https://developer.apple.com/library/archive/qa/qa1490/_index.html). + +Since the build system and Tuist have no way of inferring whether the flag is necessary or not, and since the flag comes with potentially undesirable side effects, Tuist will not automatically apply any of these flags, and because Swift Package Manager considers `-ObjC` to be included via an `.unsafeFlag` most packages cannot include it as part of their default linking settings when required. + +Consumers of Objective-C dependencies (or internal Objective-C targets) should apply `-ObjC` or `-force_load` flags when required by setting `OTHER_LDFLAGS` on consuming targets. + +### Firebase & Other Google Libraries + +Google's open source libraries — while powerful — can be difficult to integrate within Tuist as they often use non-standard architecture and techniques in how they are built. + +Here are a few tips that may be necessary to follow to integrate Firebase and Google's other Apple-platform libraries: + +#### Ensure `-ObjC` is added to `OTHER_LDFLAGS` + +Many of Google's libraries are written in Objective-C. Because of this, any consuming target will need to include the `-ObjC` tag in its `OTHER_LDFLAGS` build setting. This can either be set in an `.xcconfig` file or manually specified in the target's settings within your Tuist manifests. An example: + +```swift +Target.target( + ... + settings: .settings( + base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"] + ) + ... +) +``` + +Refer to the [Objective-C Dependencies](#objective-c-dependencies) section above for more details. + +#### Set the product type for `FBLPromises` to dynamic framework + +Certain Google libraries depend on `FBLPromises`, another of Google's libraries. You may encounter a crash that mentions `FBLPromises`, looking something like this: + +``` +NSInvalidArgumentException. Reason: -[FBLPromise HTTPBody]: unrecognized selector sent to instance 0x600000cb2640. +``` + +Explicitly setting the product type of `FBLPromises` to `.framework` in your `Package.swift` file should fix the issue: + +```swift [Tuist/Package.swift] +// swift-tools-version: 5.10 + +import PackageDescription + +#if TUIST +import ProjectDescription +import ProjectDescriptionHelpers + +let packageSettings = PackageSettings( + productTypes: [ + "FPLPromises": .framework, + ] +) +#endif + +let package = Package( +... +``` + +### Transitive static dependencies leaking through `.swiftmodule` + +When a dynamic framework or library depends on static ones through `import StaticSwiftModule`, the symbols are included in the `.swiftmodule` of the dynamic framework or library, potentially [causing the compilation to fail](https://forums.swift.org/t/compiling-a-dynamic-framework-with-a-statically-linked-library-creates-dependencies-in-swiftmodule-file/22708/1). To prevent that, you'll have to import the static dependency using [`@_implementationOnly`](https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_implementationonly): + +```swift +@_implementationOnly import StaticModule +``` diff --git a/docs/docs/guides/develop/projects/directory-structure.md b/docs/docs/guides/develop/projects/directory-structure.md new file mode 100644 index 00000000000..66bc674acd1 --- /dev/null +++ b/docs/docs/guides/develop/projects/directory-structure.md @@ -0,0 +1,45 @@ +--- +title: Directory structure +description: Learn about the structure of Tuist projects and how to organize them. +--- + +# Directory structure + +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 SPM packages, templates, plugins, and tasks. This document describes the structure of Tuist projects and how to organize them. In later sections, we'll cover how to define templates, plugins, and tasks. + +## Standard Tuist projects + +Tuist 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: + +```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:**](/guides/develop/projects/manifests#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. + - **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. + - **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. Learn more [here](/guides/develop/projects/dependencies). + +- **Root directory**: The root directory of your project that also contains the `Tuist` directory. + - [**Workspace.swift:**](/guides/develop/projects/manifests#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:**](/guides/develop/projects/manifests#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, commands 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. + +## Swift Package + +Tuist also supports SPM package projects. If you are working on an SPM package, you shouldn't need to update anything. Tuist automatically picks up on your root `Package.swift` and all the features of Tuist work as if it was a `Project.swift` manifest. + +To get started, run `tuist install` and `tuist generate` in your SPM package. Your project should now have all the same schemes and files that you would see in the vanilla Xcode SPM integration. However, now you can also run [`tuist cache`](/guides/develop/build/cache) and have majority of your SPM dependencies and modules precompiled, making subsequent builds extremely fast. diff --git a/docs/docs/guides/develop/projects/dynamic-configuration.md b/docs/docs/guides/develop/projects/dynamic-configuration.md new file mode 100644 index 00000000000..141c2823360 --- /dev/null +++ b/docs/docs/guides/develop/projects/dynamic-configuration.md @@ -0,0 +1,55 @@ +--- +title: Dynamic configuration +description: Learn how how to use environment variables to dynamically configure your project. +--- + +# Dynamic configuration + +There are certain scenarios where you might need to dynamically configure your project at generation time. For example, you might want to change the name of the app, the bundle identifier, or the deployment target based on the environment where the project is being generated. Tuist supports that via environment variables, which can be accessed from the manifest files. + +## Configuration through environment variables + +Tuist allows passing configuration through environment variables that can be accessed from the manifest files. For example: + +```bash +TUIST_APP_NAME=MyApp tuist generate +``` + +If you want to pass multiple environment variables just separate them with a space. For example: + +```bash +TUIST_APP_NAME=MyApp TUIST_APP_LOCALE=pl tuist generate +``` + +## Reading the environment variables from manifests + +Variables can be accessed using the [`Environment`](/references/project-description/enums/environment) type. Any variables following the convention `TUIST_XXX` defined in the environment or passed to Tuist when running commands will be accessible using the `Environment` type. The following example shows how we access the `TUIST_APP_NAME` variable: + +```swift +func appName() -> String { + if case let .string(environmentAppName) = Environment.appName { + return environmentAppName + } else { + return "MyApp" + } +} +``` + +Accessing variables returns an instance of type `Environment.Value?` which can take any of the following values: + +| Case | Description | +| --- | --- | +| `.string(String)` | Used when the variable represents a string. | + +You can also retrieve the string or boolean `Environment` variable using either of the helper methods defined below, these methods require a default value to be passed to ensure the user gets consistent results each time. This avoids the need to define the function appName() defined above. + +::: code-group + +```swift [String] +Environment.appName.getString(default: "TuistServer") +``` + +```swift [Boolean] +Environment.isCI.getBoolean(default: false) +``` +::: \ No newline at end of file diff --git a/docs/docs/guides/develop/projects/editing.md b/docs/docs/guides/develop/projects/editing.md new file mode 100644 index 00000000000..8b941749eef --- /dev/null +++ b/docs/docs/guides/develop/projects/editing.md @@ -0,0 +1,41 @@ +--- +title: Editing +description: Learn how to use Tuist's edit workflow to declare your project leveraging Xcode's build system and editor capabilities. +--- + +# Editing + +Unlike traditional Xcode projects or Swift Packages, +where changes are done through Xcode's UI, +Tuist-managed projects are defined in Swift code contained in **manifest files**. +If you're familiar with Swift Packages and the `Package.swift` file, +the approach is very similar. + +You could edit these files using any text editor, +but we recommend to use Tuist-provided workflow for that, +`tuist edit`. +The workflow creates an Xcode project that contains all manifest files and allows you to edit and compile them. +Thanks to using Xcode, +you get all the benefits of **code completion, syntax highlighting, and error checking**. + +## Edit the project + +To edit your project, you can run the following command in a Tuist project directory or a sub-directory: + +```bash +tuist edit +``` + +The command creates an Xcode project in a global directory and opens it in Xcode. +The project includes a `Manifests` directory that you can build to ensure all your manifests are valid. + +> [!INFO] GLOB-RESOLVED MANIFESTS +> `tuist edit` resolves the manifests to be included by using the glob `**/{Manifest}.swift` from the project's root directory (the one containing the `/Tuist` directory). Make sure the `/Tuist` directory contains a valid `Config.swift` + +## Edit and generate workflow + +As you might have noticed, the editing can't be done from the generated Xcode project. +That's by design to prevent the generated project from having a dependency on Tuist, +ensuring you can move from Tuist in the future with little effort. + +When iterating on a project, we recommend running `tuist edit` from a terminal session to get an Xcode project to edit the project, and use another terminal session to run `tuist generate`. \ No newline at end of file diff --git a/docs/docs/guides/develop/projects/hashing.md b/docs/docs/guides/develop/projects/hashing.md new file mode 100644 index 00000000000..a3e5a1b66de --- /dev/null +++ b/docs/docs/guides/develop/projects/hashing.md @@ -0,0 +1,38 @@ +--- +title: Hashing +description: Learn about Tuist's hashing logic upon which features like binary caching and selective testing are built. +--- + +# Hashing + +Features like [caching](/guides/develop/build/cache) or smart test execution require a way to determine whether a target has changed. Tuist calculates a hash for each target in the dependency graph to determine if a target has changed. The hash is calculated based on the following attributes: + +- The target's attributes (e.g., name, platform, product, etc.) +- The target's files +- The hash of the target's dependencies + +### Cache attributes + +Additionally, when calculating the hash for [caching](/guides/develop/build/cache), we also hash the following attributes. + +#### Swift version + +We hash the Swift version obtained from running the command `/usr/bin/xcrun swift --version` to prevent compilation errors due to Swift version mismatches between the targets and the binaries. + +> [!NOTE] MODULE STABILITY +> Previous versions of binary caching relied on the `BUILD_LIBRARY_FOR_DISTRIBUTION` build setting to enable [module stability](https://www.swift.org/blog/library-evolution#enabling-library-evolution-support) and enable using binaries with any compiler version. However, it caused compilation issues in projects with targets that don't support module stability. Generated binaries are bound to the Swift version used to compile them, and the Swift version must match the one used to compile the project. + +#### Configuration + + +The idea behind this flag was to ensure debug binaries were not used in release builds and viceversa. However, we are still missing a mechanism to remove the other configurations from the projects to prevent them from being used. + +## Debugging + +If you notice non-deterministic behaviors when using the caching across environments or invocations, it might be related to differences across the environments or a bug in the hashing logic. We recommend following these steps to debug the issue: + +1. Ensure the same [configuration](#configuration) and [Swift version](#swift-version) is used across environments. +2. Check if there are differences between the Xcode projects generated by two consecutive invocations of `tuist generate` or across environments. You can use the `diff` command to compare the projects. The generated projects might include **absolute paths** causing the hashing logic to be non-deterministic. + +> [!NOTE] BETTER DEBUGGING EXPERIENCE PLANNED +> Improving our debugging experience is in our roadmap. The print-hashes command, which lacks the context to understand the differences, will be replaced by a more user-friendly command that uses a tree-like structure to show the differences between the hashes. \ No newline at end of file diff --git a/docs/docs/guides/develop/projects/manifests.md b/docs/docs/guides/develop/projects/manifests.md new file mode 100644 index 00000000000..1984d9f8060 --- /dev/null +++ b/docs/docs/guides/develop/projects/manifests.md @@ -0,0 +1,68 @@ +--- +title: Manifests +description: Learn about the manifest files that Tuist uses to define projects and workspaces and configure the generation process. +--- + +# Manifests + +Tuist defaults to Swift files as the primary way to define projects and workspaces and configure the generation process. These files are referred to as **manifest files** throughout the documentation. + +The decision of using Swift was inspired by the [Swift Package Manager](https://www.swift.org/documentation/package-manager/), which also uses Swift files to define packages. Thanks to the usage of Swift, we can leverage the compiler to validate the correctness of the content and reuse code across different manifest files, and Xcode to provide a first-class editing experience thanks to the syntax highlighting, auto-completion, and validation. + +> [!NOTE] CACHING +> Since manifest files are Swift files that need to be compiled, Tuist caches the compilation results to speed up the parsing process. Therefore, you'll notice that the first time you run Tuist, it might take a bit longer to generate the project. Subsequent runs will be faster. + +## Project.swift + +The [`Project.swift`](/references/project-description/structs/project) manifest declares an Xcode project. The project gets generated in the same directory where the manifest file is located with the name indicated in the `name` property. + +```swift +// Project.swift +let project = Project( + name: "App", + targets: [ + // .... + ] +) +``` + + +> [!WARNING] ROOT VARIABLES +> The only variable that should be at the root of the manifest is `let project = Project(...)`. If you need to reuse code across various parts of the manifest, you can use Swift functions. + +## Workspace.swift + +By default, Tuist generates an [Xcode Workspace](https://developer.apple.com/documentation/xcode/projects-and-workspaces) containing the project being generated and the projects of its dependencies. If for any reason you'd like to customize the workspace to add additional projects or include files and groups, you can do so by defining a [`Workspace.swift`](/references/project-description/structs/workspace) manifest. + +```swift +// Workspace.swift +import ProjectDescription + +let workspace = Workspace( + name: "App-Workspace", + projects: [ + "./App", // Path to directory containing the Project.swift file + ] +) +``` + +> [!NOTE] +> Tuist will resolve the dependency graph and include the projects of the dependencies in the workspace. You don't need to include them manually. This is necessary for the build system to resolve the dependencies correctly. + +### Multi or mono-project + +A question that often comes up is whether to use a single project or multiple projects in a workspace. In a world without Tuist where a mono-project setup would lead to frequent Git conflicts the usage of workspaces is encouraged. However, since we don't recommend including the Tuist-generated Xcode projects in the Git repository, Git conflicts are not an issue. Therefore, the decision of using a single project or multiple projects in a workspace is up to you. + +In the Tuist project we lean on mono-projects because the cold generation time is faster (fewer manifest files to compile) and we leverage [project description helpers](/guides/develop/projects/code-sharing) as a unit of encapsulation. However, you might want to use Xcode projects as a unit of encapsulation to represent different domains of your application, which aligns more closely with the Xcode's recommended project structure. + +## Config.swift + +Tuist provides [sensible defaults](/contributors/principles.html#default-to-conventions) to simplify project configuration. However, you can customize the configuration by defining a [`Config.swift`](/references/project-description/structs/config) manifest under the `Tuist` directory, which is used by Tuist to determine the root of the project. + +```swift +import ProjectDescription + +let config = Config( + generationOptions: .options(enforceExplicitDependencies: true) +) +``` \ No newline at end of file diff --git a/docs/docs/guides/develop/projects/plugins.md b/docs/docs/guides/develop/projects/plugins.md new file mode 100644 index 00000000000..da67fb35d64 --- /dev/null +++ b/docs/docs/guides/develop/projects/plugins.md @@ -0,0 +1,142 @@ +--- +title: Plugins +description: Learn how to create and use plugins in Tuist to extend its functionality. +--- + +# Plugins + +Plugins are a tool to share and reuse Tuist artifacts across multiple projects. The following artifacts are supported: + +- [Project description helpers](/guides/develop/projects/code-sharing) across multiple projects. +- [Templates](/guides/develop/projects/templates) across multiple projects. +- Tasks across multiple projects. +- [Resource accessor](/guides/develop/projects/synthesized-files) template across multiple projects + +Note that plugins are designed to be a simple way to extend Tuist's functionality. Therefore there are **some limitations to consider**: + +- A plugin cannot depend on another plugin. +- A plugin cannot depend on third-party Swift packages +- A plugin cannot use project description helpers from the project that uses the plugin. + +If you need more flexibility, consider suggesting a feature for the tool or building your own solution upon Tuist's generation framework, [`TuistGenerator`](https://github.com/tuist/tuist/tree/main/Sources/TuistGenerator). + +> [!WARNING] NON-ACTIVELY-MAINTAINED +> Our plugin infrastructure is not actively maintained. We are looking for contributors to help us improve it. If you are interested, please reach out to us on [Slack](https://slack.tuist.io/). + +## Plugin types + +### Project description helper plugin + +A project description helper plugin is represented by a directory containing a `Plugin.swift` manifest file that declares the plugin's name and a `ProjectDescriptionHelpers` directory containing the helper Swift files. + +::: code-group +```bash [Plugin.swift] +import ProjectDescription + +let plugin = Plugin(name: "MyPlugin") +``` +```bash [Directory structure] +. +├── ... +├── Plugin.swift +├── ProjectDescriptionHelpers +└── ... +``` +::: + +### Resource accessor templates plugin + +If you need to share [synthesized resource accessors](/guides/develop/projects/synthesized-files#resource-accessors) you can use +this type of plugin. The plugin is represented by a directory containing a `Plugin.swift` manifest file that declares the plugin's name and a `ResourceSynthesizers` directory containing the resource accessor template files. + + +::: code-group +```bash [Plugin.swift] +import ProjectDescription + +let plugin = Plugin(name: "MyPlugin") +``` +```bash [Directory structure] +. +├── ... +├── Plugin.swift +├── ResourceSynthesizers +├───── Strings.stencil +├───── Plists.stencil +├───── CustomTemplate.stencil +└── ... +``` +::: + +The name of the template is the [camel case](https://en.wikipedia.org/wiki/Camel_case) version of the resource type: + +| Resource type | Template file name | +| ------------- | ------------------ | +| Strings | Strings.stencil | +| Assets | Assets.stencil | +| Property Lists | Plists.stencil | +| Fonts | Fonts.stencil | +| Core Data | CoreData.stencil | +| Interface Builder | InterfaceBuilder.stencil | +| JSON | JSON.stencil | +| YAML | YAML.stencil | + +When defining the resource synthesizers in the project, you can specify the plugin name to use the templates from the plugin: + +```swift +let project = Project(resourceSynthesizers: [.strings(plugin: "MyPlugin")]) +``` + +### Task plugin + +Tasks are `$PATH`-exposed executables that are invocable through the `tuist` command if they follow the naming convention `tuist-`. In earlier versions, Tuist provided some weak conventions and tools under `tuist plugin` to `build`, `run`, `test` and `archive` tasks represented by executables in Swift Packages, but we have deprecated this feature since it increases the maintenance burden and complexity of the tool. + +If you were using Tuist for distributing tasks, we recommend building your +- You can continue using the `ProjectAutomation.xcframework` distributed with every Tuist release to have access to the project graph from your logic with `let graph = try Tuist.graph()`. The command uses sytem process to run the `tuist` command, and return the in-memory representation of the project graph. +- To distribute tasks, we recommend including the a fat binary that supports the `arm64` and `x86_64` in GitHub releases, and using [Mise](https://mise.jdx.dev) as an installation tool. To instruct Mise on how to install your tool, you'll need a plugin repository. You can use [Tuist's](https://github.com/asdf-community/asdf-tuist) as a reference. +- If you name your tool `tuist-{xxx}` and users can install it by running `mise install`, they can run it either invoking it directly, or through `tuist xxx`. + +> [!NOTE] THE FUTURE OF PROJECTAUTOMATION +> We plan to consolidate the models of `ProjectAutomation` and `XcodeGraph` into a single backward-compatible framework that exposes the entirity of the project graph to the user. Moreover, we'll extract the generation logic into a new layer, `XcodeGraph` that you can also use from your own CLI. Think of it as building your own Tuist. + +## Using plugins + +To use a plugin, you'll have to add it to your project's [`Config.swift`](/references/project-description/structs/config) manifest file: + +```swift +import ProjectDescription + + +let config = Config( + plugins: [ + .local(path: "/Plugins/MyPlugin") + ] +) +``` + +If you want to reuse a plugin across projects that live in different repositories, you can push your plugin to a Git repository and reference it in the `Config.swift` file: + +```swift +import ProjectDescription + + +let config = Config( + plugins: [ + .git(url: "https://url/to/plugin.git", tag: "1.0.0"), + .git(url: "https://url/to/plugin.git", sha: "e34c5ba") + ] +) +``` + +After adding the plugins, `tuist install` will fetch the plugins in a global cache directory. + +> [!NOTE] NO VERSION RESOLUTION +> As you might have noted, we don't provide version resolution for plugins. We recommend using Git tags or SHAs to ensure reproducibility. + +> [!TIP] PROJECT DESCRIPTION HELPERS PLUGINS +> When using a project description helpers plugin, the name of the module that contains the helpers is the name of the plugin +> ```swift +> import ProjectDescription +> import MyTuistPlugin +> let project = Project.app(name: "MyCoolApp", platform: .iOS) +> ``` \ No newline at end of file diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/synthesized-files.md b/docs/docs/guides/develop/projects/synthesized-files.md similarity index 79% rename from docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/synthesized-files.md rename to docs/docs/guides/develop/projects/synthesized-files.md index f0e73219ace..60695a592e7 100644 --- a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/synthesized-files.md +++ b/docs/docs/guides/develop/projects/synthesized-files.md @@ -1,10 +1,13 @@ -# Synthesized files +--- +title: Synthesized files +description: Learn about synthesized files in Tuist projects. +--- -Learn how Tuist synthesizes code and resources at generation-time +# Synthesized files -## Overview +Tuist can generate files and code at generation-time to bring some convenience to managing and working with Xcode projects. In this page you'll learn about this functionality, and how you can use it in your projects. -### Target resources +## Target resources Xcode projects support adding resources to targets. However, they present teams with a few challenges, specially when working with a modular project where sources and resources are often moved around: @@ -14,9 +17,10 @@ Xcode projects support adding resources to targets. However, they present teams Tuist solves the problems above by **synthesizing a unified interface to access bundles and resources** that abstracts away the implementation details. -> Important: Even though accessing resources through the Tuist-synthesized interface is not mandatory, we recommend it because it makes the code easier to reason about and the resources to move around. +> [!IMPORTANT] RECOMMENDED +> Even though accessing resources through the Tuist-synthesized interface is not mandatory, we recommend it because it makes the code easier to reason about and the resources to move around. -### Synthesized resources +## Resources Tuist provides interfaces to declare the content of files such as `Info.plist` or entitlements in Swift. This is useful to ensure consistency across targets and projects, @@ -26,13 +30,14 @@ You can also come up with your own abstractions to model the content and share i When your project is generated, Tuist will synthesize the content of those files and write them into the `Derived` directory relative to the directory containing the project that defines them. -> Note: We recommend adding the `Derived` directory to the `.gitignore` file of your project. +> [!TIP] GITIGNORE THE DERIVED DIRECTORY +> We recommend adding the `Derived` directory to the `.gitignore` file of your project. -### Synthesized bundle accessor +## Bundle accessors Tuist synthesizes an interface to access the bundle that contains the target resources. -##### Swift +### Swift The target will contain an extension of the `Bundle` type that exposes the bundle: @@ -40,7 +45,7 @@ The target will contain an extension of the `Bundle` type that exposes the bundl let bundle = Bundle.module ``` -##### Objective-C +### Objective-C In Objective-C, you'll get an interface `{Target}Resources` to access the bundle: @@ -48,9 +53,10 @@ In Objective-C, you'll get an interface `{Target}Resources` to access the bundle NSBundle *bundle = [MyFeatureResources bundle]; ``` -> Tip: If a target product, for example a library, doesn't support resources, Tuist will include the resources in a target of product type `bundle` ensuring that it ends up in the final product and that the interface points to the right bundle. +> [!TIP] SUPPORTING RESOURCES IN LIBRARIES THROUGH BUNDLES +> If a target product, for example a library, doesn't support resources, Tuist will include the resources in a target of product type `bundle` ensuring that it ends up in the final product and that the interface points to the right bundle. -### Synthesized resource accessors +## Resource accessors Resources are identified by their name and extension using strings. This is not ideal because it's not caught at compile time and might lead to crashes in release. To prevent that, Tuist integrates [SwiftGen](https://github.com/SwiftGen/SwiftGen) into the project generation process to synthesize an interface to access the resources. Thanks to that, you can confidently access the resources leveraging the compiler to catch any issues. @@ -92,6 +98,5 @@ you can use the `Project.resourceSynthesizers` property passing the list of reso let project = Project(resourceSynthesizers: [.string(), .fonts()]) ``` -> Note: You can check out [this fixture](https://github.com/tuist/tuist/tree/main/fixtures/ios_app_with_templates) to see an example of how to use custom templates to synthesize accessors to resources. - - \ No newline at end of file diff --git a/docs/docs/references/cli/[command].paths.js b/docs/docs/references/cli/[command].paths.js new file mode 100644 index 00000000000..8720154c4ea --- /dev/null +++ b/docs/docs/references/cli/[command].paths.js @@ -0,0 +1,12 @@ +import cliDataLoader from "./commands.data"; + +export default { + paths() { + return cliDataLoader.load().map((item) => { + return { + params: { command: item.command, title: item.title, description: item.description }, + content: item.content + } + }) + }, +}; diff --git a/docs/docs/references/cli/commands.data.js b/docs/docs/references/cli/commands.data.js new file mode 100644 index 00000000000..fbf6dfaa662 --- /dev/null +++ b/docs/docs/references/cli/commands.data.js @@ -0,0 +1,43 @@ +import * as path from "node:path"; +import fg from "fast-glob"; +import fs from "node:fs"; + +export default { + load() { + const generatedDirectory = path.join( + import.meta.dirname, + "../../../docs/generated/cli" + ); + const files = fg + .sync("**/*.md", { + cwd: generatedDirectory, + absolute: true + }) + + const validCategories = ["cloud", "plugin", "migration", "tuist"]; + + return files.map((file) => { + const relativePath = path.relative(generatedDirectory, file); + const fileName = path.basename(file, ".md"); + const content = fs.readFileSync(file, "utf-8"); + const description = content.split('\n')[1].trim(); + + const category = relativePath.split(path.sep)[0]; + const finalCategory = validCategories.includes(category) ? category : "tuist"; + + let titleParts = fileName.split("."); + let title = titleParts + .filter(part => !validCategories.includes(part)) + .join(" "); + + + return { + command: relativePath.replace(/\.md$/, ""), + title: title, + content: content, + path: relativePath.replace(/\.md$/, ""), + category: finalCategory, + }; + }) + }, +}; diff --git a/docs/docs/references/examples/[example].md b/docs/docs/references/examples/[example].md new file mode 100644 index 00000000000..f782f730688 --- /dev/null +++ b/docs/docs/references/examples/[example].md @@ -0,0 +1,16 @@ +--- +editLink: false +description: {{ $params.description }} +--- + + + + + +
Check out example diff --git a/docs/docs/references/examples/[example].paths.js b/docs/docs/references/examples/[example].paths.js new file mode 100644 index 00000000000..fe4e54d645f --- /dev/null +++ b/docs/docs/references/examples/[example].paths.js @@ -0,0 +1,17 @@ +import examplesDataLoader from "./examples.data"; + +export default { + paths() { + return examplesDataLoader.load().map((item) => { + return { + params: { + example: item.name, + title: item.title, + description: item.description, + url: item.url, + }, + content: item.content, + }; + }); + }, +}; diff --git a/docs/docs/references/examples/examples.data.js b/docs/docs/references/examples/examples.data.js new file mode 100644 index 00000000000..0d528f96d70 --- /dev/null +++ b/docs/docs/references/examples/examples.data.js @@ -0,0 +1,32 @@ +import * as path from "node:path"; +import fg from "fast-glob"; +import fs from "node:fs"; + +const glob = path.join(import.meta.dirname, "../../../../fixtures/*/README.md"); + +export default { + watch: [glob], + load(files) { + if (!files) { + files = fg + .sync(glob, { + absolute: true, + }) + .sort(); + } + return files.map((file) => { + const content = fs.readFileSync(file, "utf-8"); + const titleRegex = /^#\s*(.+)/m; + const titleMatch = content.match(titleRegex); + return { + title: titleMatch[1], + name: path.basename(path.dirname(file)).toLowerCase(), + description: "foo", + content: content, + url: `https://github.com/tuist/tuist/tree/main/fixtures/${path.basename( + path.dirname(file) + )}`, + }; + }); + }, +}; diff --git a/docs/docs/references/migrations/from-v3-to-v4.md b/docs/docs/references/migrations/from-v3-to-v4.md new file mode 100644 index 00000000000..3ef76f3e0b3 --- /dev/null +++ b/docs/docs/references/migrations/from-v3-to-v4.md @@ -0,0 +1,96 @@ +--- +title: From v3 to v4 +description: This page documents how to migrate the Tuist CLI from the version 3 to version 4. +--- + +# From Tuist v3 to v4 + +With the release of [Tuist 4](https://github.com/tuist/tuist/releases/tag/4.0.0), we took the opportunity to introduce some breaking changes to the project, which we believed would make the project easier to use and maintain in the long run. This document outlines the changes you will need to make to your project to upgrade from Tuist 3 to Tuist 4. + +### Dropped version management through `tuistenv` + +Prior to Tuist 4, the installation script installed a tool, `tuistenv`, that would get renamed to `tuist` at installation time. The tool would take care of installing and activating versions of Tuist ensuring determinism across environments. With the aim of reducing the feature surface of Tuist, we decided to drop `tuistenv` in favor of [Mise](https://mise.jdx.dev/), a tool that does the same job but is more flexible and can be used across different tools. If you were using `tuistenv`, you'll have to uninstall the current version of Tuist by running `curl -Ls https://uninstall.tuist.io | bash` and then install it using the installation method of your choice. We strongly recommend the usage of Mise because it's able to install and activate versions deterministically across environments. + +::: code-group + +```bash [Uninstall tuistenv] +curl -Ls https://uninstall.tuist.io | bash +``` +::: + +> [!IMPORTANT] MISE IN CI ENVIRONMENTS AND XCODE PROJECTS +> If you decide to embrace the determinism that Mise brings across the board, we recommend checking out the documentation for how to use Mise in [CI environments](https://mise.jdx.dev/continuous-integration.html) and [Xcode projects](https://mise.jdx.dev/ide-integration.html#xcode). + +> [!NOTE] HOMEBREW IS SUPPORTED +> Note that you can still install Tuist using Homebrew, which is a popular package manager for macOS. You can find the instructions on how to install Tuist using Homebrew in the [installation guide](/guides/quick-start/install-tuist#alternative-homebrew). + +### Dropped `init` constructors from `ProjectDescription` models + +With the aim of improving the readability and expressiveness of the APIs, we decided to remove the `init` constructors from all the `ProjectDescription` models. Every model now provides a static constructor that you can use to create instances of the models. If you were using the `init` constructors, you'll have to update your project to use the static constructors instead. + +> [!TIP] NAMING CONVENTION +> The naming convention that we follow is to use the name of the model as the name of the static constructor. For example, the static constructor for the `Target` model is `Target.target`. + +### Renamed `--no-cache` to `--no-binary-cache` + +Because the `--no-cache` flag was ambiguous, we decided to rename it to `--no-binary-cache` to make it clear that it refers to the binary cache. If you were using the `--no-cache` flag, you'll have to update your project to use the `--no-binary-cache` flag instead. + +### Renamed `tuist fetch` to `tuist install` + +We renamed the `tuist fetch` command to `tuist install` to align with the industry convention. If you were using the `tuist fetch` command, you'll have to update your project to use the `tuist install` command instead. + +### [Adopt `Package.swift` as the DSL for dependencies](https://github.com/tuist/tuist/pull/5862) + +Before Tuist 4, you could define dependencies in a `Dependencies.swift` file. This proprietary format broke the support in tools like [Dependabot](https://github.com/dependabot) or [Renovatebot](https://github.com/renovatebot/renovate) to automatically update dependencies. Moreover, it introduced unnecessary indirections for users. Therefore, we decided to embrace `Package.swift` as the only way to define dependencies in Tuist. If you were using the `Dependencies.swift` file, you'll have to move the content from your `Tuist/Dependencies.swift` to a `Package.swift` at the root, and use the `#if TUIST` directive to configure the integration. You can read more about how to integrate Swift Package dependencies [here](/guides/develop/projects/dependencies#swift-packages) + +### Renamed `tuist cache warm` to `tuist cache` + +For brevity, we decided to rename the `tuist cache warm` command to `tuist cache`. If you were using the `tuist cache warm` command, you'll have to update your project to use the `tuist cache` command instead. + + +### Renamed `tuist cache print-hashes` to `tuist cache --print-hashes` + +We decided to rename the `tuist cache print-hashes` command to `tuist cache --print-hashes` to make it clear that it's a flag of the `tuist cache` command. If you were using the `tuist cache print-hashes` command, you'll have to update your project to use the `tuist cache --print-hashes` flag instead. + +### Removed caching profiles + +Before Tuist 4, you could define caching profiles in `Tuist/Config.swift` which contained a configuration for the cache. We decided to remove this feature because it could lead to confusion when using it in the generation process with a profile other than the one that was used to generate the project. Moreover, it could lead to users using a debug profile to build a release version of the app, which could lead to unexpected results. In its place, we introduced the `--configuration` option, which you can use to specify the configuration you want to use when generating the project. If you were using caching profiles, you'll have to update your project to use the `--configuration` option instead. + +### Removed `--skip-cache` in favor of arguments + +We removed the flag `--skip-cache` from the `generate` command in favor of controlling for which targets the binary cache should be skipped by using the arguments. If you were using the `--skip-cache` flag, you'll have to update your project to use the arguments instead. + +::: code-group + +```bash [Before] +tuist generate --skip-cache Foo +``` + +```bash [After] +tuist generate Foo +``` +::: + +### [Dropped signing capabilities](https://github.com/tuist/tuist/pull/5716) + +Signing is already solved by community tooling like [Fastlane](https://fastlane.tools/) and Xcode itself, which do a much better job at that. We felt that signing was an stretch goal for Tuist, and that it was better to focus on the core features of the project. If you were using Tuist signing capabilities, which consisted of encrypting the certificates and profiles in the repository and installing them in the right places at generation time, you might want to replicate that logic in your own scripts that run before project generation. In particular: + - A script that decrypts the certificates and profiles using a key either stored in the file-system or in an environment variable, and installs certificates in the keychain, and the provisioning profiles in the directory `~/Library/MobileDevice/Provisioning\ Profiles`. + - A script that can take an existing profiles and certificates and encrypt them. + +> [!TIP] SIGNING REQUIREMENTS +> Signing requires the right certificates to be present in the keychain and the provisioning profiles to be present in the directory `~/Library/MobileDevice/Provisioning\ Profiles`. You can use the `security` command-line tool to install certificates in the keychain and the `cp` command to copy the provisioning profiles to the right directory. + +### Dropped Carthage integration via `Dependencies.swift` + +Before Tuist 4, Carthage dependencies could be defined in a `Dependencies.swift` file, which users could then fetch by running `tuist fetch`. We also felt that this was a stretch goal for Tuist, specially considering a future where Swift Package Manager would be the preferred way to manage dependencies. If you were using Carthage dependencies, you'll have to use `Carthage` directly to pull the pre-compiled frameworks and XCFrameworks into Carthage's standard directory, and then reference those binaries from your tagets using the `TargetDependency.xcframework` and `TargetDependency.framework` cases. + +> [!NOTE] CARTHAGE IS STILL SUPPORTED +> Some users understood that we dropped Carthage support. We didn't. The contract between Tuist and Carthage's output is to system-stored frameworks and XCFrameworks. The only thing that changed is who is responsible for fetching the dependencies. It used to be Tuist through Carthage, now it's Carthage. + +### Dropped the `TargetDependency.packagePlugin` API + +Before Tuist 4, you could define a package plugin dependency using the `TargetDependency.packagePlugin` case. After seeing the Swift Package Manager introducing new package types, we decided to iterate on the API towards something that would be more flexible and future-proof. If you were using `TargetDependency.packagePlugin`, you'll have to use `TargetDependency.package` instead, and pass the type of package you want to use as an argument. + +### [Dropped deprecated APIs](https://github.com/tuist/tuist/pull/5560) + +We removed the APIs that were marked as deprecated in Tuist 3. If you were using any of the deprecated APIs, you'll have to update your project to use the new APIs. diff --git a/docs/docs/references/project-description/[identifier].md b/docs/docs/references/project-description/[identifier].md new file mode 100644 index 00000000000..9635e3ce98e --- /dev/null +++ b/docs/docs/references/project-description/[identifier].md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/docs/references/project-description/[identifier].paths.js b/docs/docs/references/project-description/[identifier].paths.js new file mode 100644 index 00000000000..684c3174bd8 --- /dev/null +++ b/docs/docs/references/project-description/[identifier].paths.js @@ -0,0 +1,12 @@ +import typesDataLoader from "./types.data"; + +export default { + paths() { + return typesDataLoader.load().map((item) => { + return { + params: { type: item.name, title: item.title, description: item.description, identifier: item.identifier }, + content: item.content + } + }) + }, +}; diff --git a/docs/docs/references/project-description/types.data.js b/docs/docs/references/project-description/types.data.js new file mode 100644 index 00000000000..508597b3d15 --- /dev/null +++ b/docs/docs/references/project-description/types.data.js @@ -0,0 +1,31 @@ +import * as path from "node:path"; +import fg from "fast-glob"; +import fs from "node:fs"; + +export default { + load() { + const generatedDirectory = path.join( + import.meta.dirname, + "../../../docs/generated/manifest" + ); + const files = fg + .sync("**/*.md", { + cwd: generatedDirectory, + absolute: true, + ignore: ["**/README.md"], + }) + .sort(); + return files.map((file) => { + const category = path.basename(path.dirname(file)) + const fileName = path.basename(file).replace(".md", "") + return { + category: category, + title: fileName, + name: fileName.toLowerCase(), + identifier: category + "/" + fileName.toLowerCase(), + description: "", + content: fs.readFileSync(file, "utf-8"), + }; + }); + }, +}; diff --git a/docs/docs/tuxie.png b/docs/docs/tuxie.png new file mode 100644 index 00000000000..1291be1c6b4 Binary files /dev/null and b/docs/docs/tuxie.png differ diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000000..4bfd55e1c7a --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@tuist/docs", + "private": true, + "description": "Tuist's documentation website", + "license": "MIT", + "devDependencies": { + "execa": "^8.0.1", + "fast-glob": "^3.3.2", + "vitepress": "^1.3.1", + "vue": "^3.4.31", + "wrangler": "^3.64.0" + }, + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview", + "deploy": "vitepress build && wrangler pages deploy .vitepress/dist --project-name tuist-docs-next --branch main" + } +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 00000000000..56b507fc734 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,2100 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + execa: + specifier: ^8.0.1 + version: 8.0.1 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + vitepress: + specifier: ^1.3.1 + version: 1.3.1(@algolia/client-search@4.24.0)(search-insights@2.15.0) + vue: + specifier: ^3.4.31 + version: 3.4.31 + wrangler: + specifier: ^3.64.0 + version: 3.64.0 + +packages: + + /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0): + resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0) + '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + dev: true + + /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0): + resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==} + peerDependencies: + search-insights: '>= 1 < 3' + dependencies: + '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) + search-insights: 2.15.0 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + dev: true + + /@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0): + resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + dependencies: + '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) + '@algolia/client-search': 4.24.0 + algoliasearch: 4.24.0 + dev: true + + /@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0): + resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + dependencies: + '@algolia/client-search': 4.24.0 + algoliasearch: 4.24.0 + dev: true + + /@algolia/cache-browser-local-storage@4.24.0: + resolution: {integrity: sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==} + dependencies: + '@algolia/cache-common': 4.24.0 + dev: true + + /@algolia/cache-common@4.24.0: + resolution: {integrity: sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==} + dev: true + + /@algolia/cache-in-memory@4.24.0: + resolution: {integrity: sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==} + dependencies: + '@algolia/cache-common': 4.24.0 + dev: true + + /@algolia/client-account@4.24.0: + resolution: {integrity: sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==} + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /@algolia/client-analytics@4.24.0: + resolution: {integrity: sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==} + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /@algolia/client-common@4.24.0: + resolution: {integrity: sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==} + dependencies: + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /@algolia/client-personalization@4.24.0: + resolution: {integrity: sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==} + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /@algolia/client-search@4.24.0: + resolution: {integrity: sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==} + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /@algolia/logger-common@4.24.0: + resolution: {integrity: sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==} + dev: true + + /@algolia/logger-console@4.24.0: + resolution: {integrity: sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==} + dependencies: + '@algolia/logger-common': 4.24.0 + dev: true + + /@algolia/recommend@4.24.0: + resolution: {integrity: sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==} + dependencies: + '@algolia/cache-browser-local-storage': 4.24.0 + '@algolia/cache-common': 4.24.0 + '@algolia/cache-in-memory': 4.24.0 + '@algolia/client-common': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/logger-common': 4.24.0 + '@algolia/logger-console': 4.24.0 + '@algolia/requester-browser-xhr': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/requester-node-http': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /@algolia/requester-browser-xhr@4.24.0: + resolution: {integrity: sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==} + dependencies: + '@algolia/requester-common': 4.24.0 + dev: true + + /@algolia/requester-common@4.24.0: + resolution: {integrity: sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==} + dev: true + + /@algolia/requester-node-http@4.24.0: + resolution: {integrity: sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==} + dependencies: + '@algolia/requester-common': 4.24.0 + dev: true + + /@algolia/transporter@4.24.0: + resolution: {integrity: sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==} + dependencies: + '@algolia/cache-common': 4.24.0 + '@algolia/logger-common': 4.24.0 + '@algolia/requester-common': 4.24.0 + dev: true + + /@babel/helper-string-parser@7.24.8: + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/parser@7.24.8: + resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.9 + dev: true + + /@babel/types@7.24.9: + resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: true + + /@cloudflare/kv-asset-handler@0.3.4: + resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} + engines: {node: '>=16.13'} + dependencies: + mime: 3.0.0 + dev: true + + /@cloudflare/workerd-darwin-64@1.20240701.0: + resolution: {integrity: sha512-XAZa4ZP+qyTn6JQQACCPH09hGZXP2lTnWKkmg5mPwT8EyRzCKLkczAf98vPP5bq7JZD/zORdFWRY0dOTap8zTQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-darwin-arm64@1.20240701.0: + resolution: {integrity: sha512-w80ZVAgfH4UwTz7fXZtk7KmS2FzlXniuQm4ku4+cIgRTilBAuKqjpOjwUCbx5g13Gqcm9NuiHce+IDGtobRTIQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-linux-64@1.20240701.0: + resolution: {integrity: sha512-UWLr/Anxwwe/25nGv451MNd2jhREmPt/ws17DJJqTLAx6JxwGWA15MeitAIzl0dbxRFAJa+0+R8ag2WR3F/D6g==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-linux-arm64@1.20240701.0: + resolution: {integrity: sha512-3kCnF9kYgov1ggpuWbgpXt4stPOIYtVmPCa7MO2xhhA0TWP6JDUHRUOsnmIgKrvDjXuXqlK16cdg3v+EWsaPJg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-windows-64@1.20240701.0: + resolution: {integrity: sha512-6IPGITRAeS67j3BH1rN4iwYWDt47SqJG7KlZJ5bB4UaNAia4mvMBSy/p2p4vA89bbXoDRjMtEvRu7Robu6O7hQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@docsearch/css@3.6.0: + resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==} + dev: true + + /@docsearch/js@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0): + resolution: {integrity: sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==} + dependencies: + '@docsearch/react': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0) + preact: 10.22.1 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + dev: true + + /@docsearch/react@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0): + resolution: {integrity: sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + dependencies: + '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.15.0) + '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0) + '@docsearch/css': 3.6.0 + algoliasearch: 4.24.0 + search-insights: 2.15.0 + transitivePeerDependencies: + - '@algolia/client-search' + dev: true + + /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19): + resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} + peerDependencies: + esbuild: '*' + dependencies: + esbuild: 0.17.19 + dev: true + + /@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19): + resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} + peerDependencies: + esbuild: '*' + dependencies: + esbuild: 0.17.19 + escape-string-regexp: 4.0.0 + rollup-plugin-node-polyfills: 0.2.1 + dev: true + + /@esbuild/aix-ppc64@0.21.5: + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.21.5: + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.21.5: + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.21.5: + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.21.5: + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.21.5: + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.21.5: + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.21.5: + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.21.5: + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.21.5: + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.21.5: + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.21.5: + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.21.5: + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.21.5: + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.21.5: + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.21.5: + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.21.5: + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.21.5: + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.21.5: + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.21.5: + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.21.5: + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.21.5: + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.21.5: + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@rollup/rollup-android-arm-eabi@4.18.1: + resolution: {integrity: sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.18.1: + resolution: {integrity: sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.18.1: + resolution: {integrity: sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.18.1: + resolution: {integrity: sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.18.1: + resolution: {integrity: sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-musleabihf@4.18.1: + resolution: {integrity: sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.18.1: + resolution: {integrity: sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.18.1: + resolution: {integrity: sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-powerpc64le-gnu@4.18.1: + resolution: {integrity: sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.18.1: + resolution: {integrity: sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.18.1: + resolution: {integrity: sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.18.1: + resolution: {integrity: sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.18.1: + resolution: {integrity: sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.18.1: + resolution: {integrity: sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.18.1: + resolution: {integrity: sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.18.1: + resolution: {integrity: sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@shikijs/core@1.10.3: + resolution: {integrity: sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==} + dependencies: + '@types/hast': 3.0.4 + dev: true + + /@shikijs/transformers@1.10.3: + resolution: {integrity: sha512-MNjsyye2WHVdxfZUSr5frS97sLGe6G1T+1P41QjyBFJehZphMcr4aBlRLmq6OSPBslYe9byQPVvt/LJCOfxw8Q==} + dependencies: + shiki: 1.10.3 + dev: true + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/hast@3.0.4: + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + dependencies: + '@types/unist': 3.0.2 + dev: true + + /@types/linkify-it@5.0.0: + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + dev: true + + /@types/markdown-it@14.1.1: + resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + dev: true + + /@types/mdurl@2.0.0: + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + dev: true + + /@types/node-forge@1.3.11: + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + dependencies: + '@types/node': 20.14.10 + dev: true + + /@types/node@20.14.10: + resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/unist@3.0.2: + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + dev: true + + /@types/web-bluetooth@0.0.20: + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + dev: true + + /@vitejs/plugin-vue@5.0.5(vite@5.3.4)(vue@3.4.31): + resolution: {integrity: sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + dependencies: + vite: 5.3.4 + vue: 3.4.31 + dev: true + + /@vue/compiler-core@3.4.31: + resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} + dependencies: + '@babel/parser': 7.24.8 + '@vue/shared': 3.4.31 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 + dev: true + + /@vue/compiler-dom@3.4.31: + resolution: {integrity: sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==} + dependencies: + '@vue/compiler-core': 3.4.31 + '@vue/shared': 3.4.31 + dev: true + + /@vue/compiler-sfc@3.4.31: + resolution: {integrity: sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==} + dependencies: + '@babel/parser': 7.24.8 + '@vue/compiler-core': 3.4.31 + '@vue/compiler-dom': 3.4.31 + '@vue/compiler-ssr': 3.4.31 + '@vue/shared': 3.4.31 + estree-walker: 2.0.2 + magic-string: 0.30.10 + postcss: 8.4.39 + source-map-js: 1.2.0 + dev: true + + /@vue/compiler-ssr@3.4.31: + resolution: {integrity: sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==} + dependencies: + '@vue/compiler-dom': 3.4.31 + '@vue/shared': 3.4.31 + dev: true + + /@vue/devtools-api@7.3.6: + resolution: {integrity: sha512-z6cKyxdXrIGgA++eyGBfquj6dCplRdgjt+I18fJx8hjWTXDTIyeQvryyEBMchnfZVyvUTjK3QjGjDpLCnJxPjw==} + dependencies: + '@vue/devtools-kit': 7.3.6 + dev: true + + /@vue/devtools-kit@7.3.6: + resolution: {integrity: sha512-5Ym9V3fkJenEoptqKoo+cgY5RTVwrSssFdzRsuyIgaeiskCT+rRJeQdwoo81tyrQ1mfS7Er1rYZlSzr3Y3L/ew==} + dependencies: + '@vue/devtools-shared': 7.3.6 + birpc: 0.2.17 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.1 + dev: true + + /@vue/devtools-shared@7.3.6: + resolution: {integrity: sha512-R/FOmdJV+hhuwcNoxp6e87RRkEeDMVhWH+nOsnHUrwjjsyeXJ2W1475Ozmw+cbZhejWQzftkHVKO28Fuo1yqCw==} + dependencies: + rfdc: 1.4.1 + dev: true + + /@vue/reactivity@3.4.31: + resolution: {integrity: sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==} + dependencies: + '@vue/shared': 3.4.31 + dev: true + + /@vue/runtime-core@3.4.31: + resolution: {integrity: sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==} + dependencies: + '@vue/reactivity': 3.4.31 + '@vue/shared': 3.4.31 + dev: true + + /@vue/runtime-dom@3.4.31: + resolution: {integrity: sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==} + dependencies: + '@vue/reactivity': 3.4.31 + '@vue/runtime-core': 3.4.31 + '@vue/shared': 3.4.31 + csstype: 3.1.3 + dev: true + + /@vue/server-renderer@3.4.31(vue@3.4.31): + resolution: {integrity: sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==} + peerDependencies: + vue: 3.4.31 + dependencies: + '@vue/compiler-ssr': 3.4.31 + '@vue/shared': 3.4.31 + vue: 3.4.31 + dev: true + + /@vue/shared@3.4.31: + resolution: {integrity: sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==} + dev: true + + /@vueuse/core@10.11.0(vue@3.4.31): + resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==} + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.0 + '@vueuse/shared': 10.11.0(vue@3.4.31) + vue-demi: 0.14.8(vue@3.4.31) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /@vueuse/integrations@10.11.0(focus-trap@7.5.4)(vue@3.4.31): + resolution: {integrity: sha512-Pp6MtWEIr+NDOccWd8j59Kpjy5YDXogXI61Kb1JxvSfVBO8NzFQkmrKmSZz47i+ZqHnIzxaT38L358yDHTncZg==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^4 + drauu: ^0.3 + focus-trap: ^7 + fuse.js: ^6 + idb-keyval: ^6 + jwt-decode: ^3 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^6 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + dependencies: + '@vueuse/core': 10.11.0(vue@3.4.31) + '@vueuse/shared': 10.11.0(vue@3.4.31) + focus-trap: 7.5.4 + vue-demi: 0.14.8(vue@3.4.31) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /@vueuse/metadata@10.11.0: + resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==} + dev: true + + /@vueuse/shared@10.11.0(vue@3.4.31): + resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==} + dependencies: + vue-demi: 0.14.8(vue@3.4.31) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /acorn-walk@8.3.3: + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.12.1 + dev: true + + /acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /algoliasearch@4.24.0: + resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} + dependencies: + '@algolia/cache-browser-local-storage': 4.24.0 + '@algolia/cache-common': 4.24.0 + '@algolia/cache-in-memory': 4.24.0 + '@algolia/client-account': 4.24.0 + '@algolia/client-analytics': 4.24.0 + '@algolia/client-common': 4.24.0 + '@algolia/client-personalization': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/logger-common': 4.24.0 + '@algolia/logger-console': 4.24.0 + '@algolia/recommend': 4.24.0 + '@algolia/requester-browser-xhr': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/requester-node-http': 4.24.0 + '@algolia/transporter': 4.24.0 + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + dependencies: + printable-characters: 1.0.42 + dev: true + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /birpc@0.2.17: + resolution: {integrity: sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==} + dev: true + + /blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /capnp-ts@0.7.0: + resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} + dependencies: + debug: 4.3.5 + tslib: 2.6.3 + transitivePeerDependencies: + - supports-color + dev: true + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + dev: true + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + + /copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + dependencies: + is-what: 4.1.16 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: true + + /data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + dev: true + + /date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dev: true + + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + + /esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + + /exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /focus-trap@7.5.4: + resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} + dependencies: + tabbable: 6.2.0 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + dev: true + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + + /mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: true + + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /miniflare@3.20240701.0: + resolution: {integrity: sha512-m9+I+7JNyqDGftCMKp9cK9pCZkK72hAL2mM9IWwhct+ZmucLBA8Uu6+rHQqA5iod86cpwOkrB2PrPA3wx9YNgw==} + engines: {node: '>=16.13'} + hasBin: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.12.1 + acorn-walk: 8.3.3 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.4 + workerd: 1.20240701.0 + ws: 8.18.0 + youch: 3.3.3 + zod: 3.23.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /minisearch@7.0.1: + resolution: {integrity: sha512-xLeX/AwTJLzgBF2/bdUI7MEePwXtzaLExkRwu8YFGfLDwSe06KYkplqPodLANsqvfc5Ks/r5ItFUSjIp7+9xtw==} + dev: true + + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + dev: true + + /node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + dev: true + + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + dev: true + + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /postcss@8.4.39: + resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: true + + /preact@10.22.1: + resolution: {integrity: sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==} + dev: true + + /printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.14.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + dev: true + + /rollup-plugin-inject@3.0.2: + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. + dependencies: + estree-walker: 0.6.1 + magic-string: 0.25.9 + rollup-pluginutils: 2.8.2 + dev: true + + /rollup-plugin-node-polyfills@0.2.1: + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} + dependencies: + rollup-plugin-inject: 3.0.2 + dev: true + + /rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: true + + /rollup@4.18.1: + resolution: {integrity: sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.18.1 + '@rollup/rollup-android-arm64': 4.18.1 + '@rollup/rollup-darwin-arm64': 4.18.1 + '@rollup/rollup-darwin-x64': 4.18.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.18.1 + '@rollup/rollup-linux-arm-musleabihf': 4.18.1 + '@rollup/rollup-linux-arm64-gnu': 4.18.1 + '@rollup/rollup-linux-arm64-musl': 4.18.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.18.1 + '@rollup/rollup-linux-riscv64-gnu': 4.18.1 + '@rollup/rollup-linux-s390x-gnu': 4.18.1 + '@rollup/rollup-linux-x64-gnu': 4.18.1 + '@rollup/rollup-linux-x64-musl': 4.18.1 + '@rollup/rollup-win32-arm64-msvc': 4.18.1 + '@rollup/rollup-win32-ia32-msvc': 4.18.1 + '@rollup/rollup-win32-x64-msvc': 4.18.1 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /search-insights@2.15.0: + resolution: {integrity: sha512-ch2sPCUDD4sbPQdknVl9ALSi9H7VyoeVbsxznYz6QV55jJ8CI3EtwpO1i84keN4+hF5IeHWIeGvc08530JkVXQ==} + dev: true + + /selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + dependencies: + '@types/node-forge': 1.3.11 + node-forge: 1.3.1 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /shiki@1.10.3: + resolution: {integrity: sha512-eneCLncGuvPdTutJuLyUGS8QNPAVFO5Trvld2wgEq1e002mwctAhJKeMGWtWVXOIEzmlcLRqcgPSorR6AVzOmQ==} + dependencies: + '@shikijs/core': 1.10.3 + '@types/hast': 3.0.4 + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + dev: true + + /speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + dev: true + + /stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + dev: true + + /stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /superjson@2.2.1: + resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} + engines: {node: '>=16'} + dependencies: + copy-anything: 3.0.5 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + dev: true + + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: true + + /unenv-nightly@1.10.0-1717606461.a117952: + resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==} + dependencies: + consola: 3.2.3 + defu: 6.1.4 + mime: 3.0.0 + node-fetch-native: 1.6.4 + pathe: 1.1.2 + ufo: 1.5.3 + dev: true + + /vite@5.3.4: + resolution: {integrity: sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.21.5 + postcss: 8.4.39 + rollup: 4.18.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitepress@1.3.1(@algolia/client-search@4.24.0)(search-insights@2.15.0): + resolution: {integrity: sha512-soZDpg2rRVJNIM/IYMNDPPr+zTHDA5RbLDHAxacRu+Q9iZ2GwSR0QSUlLs+aEZTkG0SOX1dc8RmUYwyuxK8dfQ==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + dependencies: + '@docsearch/css': 3.6.0 + '@docsearch/js': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.15.0) + '@shikijs/core': 1.10.3 + '@shikijs/transformers': 1.10.3 + '@types/markdown-it': 14.1.1 + '@vitejs/plugin-vue': 5.0.5(vite@5.3.4)(vue@3.4.31) + '@vue/devtools-api': 7.3.6 + '@vue/shared': 3.4.31 + '@vueuse/core': 10.11.0(vue@3.4.31) + '@vueuse/integrations': 10.11.0(focus-trap@7.5.4)(vue@3.4.31) + focus-trap: 7.5.4 + mark.js: 8.11.1 + minisearch: 7.0.1 + shiki: 1.10.3 + vite: 5.3.4 + vue: 3.4.31 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - '@vue/composition-api' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + dev: true + + /vue-demi@0.14.8(vue@3.4.31): + resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.4.31 + dev: true + + /vue@3.4.31: + resolution: {integrity: sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.4.31 + '@vue/compiler-sfc': 3.4.31 + '@vue/runtime-dom': 3.4.31 + '@vue/server-renderer': 3.4.31(vue@3.4.31) + '@vue/shared': 3.4.31 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /workerd@1.20240701.0: + resolution: {integrity: sha512-qSgNVqauqzNCij9MaJLF2c2ko3AnFioVSIxMSryGbRK+LvtGr9BKBt6JOxCb24DoJASoJDx3pe3DJHBVydUiBg==} + engines: {node: '>=16'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20240701.0 + '@cloudflare/workerd-darwin-arm64': 1.20240701.0 + '@cloudflare/workerd-linux-64': 1.20240701.0 + '@cloudflare/workerd-linux-arm64': 1.20240701.0 + '@cloudflare/workerd-windows-64': 1.20240701.0 + dev: true + + /wrangler@3.64.0: + resolution: {integrity: sha512-q2VQADJXzuOkXs9KIfPSx7UCZHBoxsqSNbJDLkc2pHpGmsyNQXsJRqjMoTg/Kls7O3K9A7EGnzGr7+Io2vE6AQ==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20240620.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + chokidar: 3.6.0 + date-fns: 3.6.0 + esbuild: 0.17.19 + miniflare: 3.20240701.0 + nanoid: 3.3.7 + path-to-regexp: 6.2.2 + resolve: 1.22.8 + resolve.exports: 2.0.2 + selfsigned: 2.4.1 + source-map: 0.6.1 + unenv: /unenv-nightly@1.10.0-1717606461.a117952 + xxhash-wasm: 1.0.2 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xxhash-wasm@1.0.2: + resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} + dev: true + + /youch@3.3.3: + resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} + dependencies: + cookie: 0.5.0 + mustache: 4.2.0 + stacktracey: 2.1.8 + dev: true + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: true diff --git a/docs/scripts/generate-cli-docs.mjs b/docs/scripts/generate-cli-docs.mjs new file mode 100755 index 00000000000..ad5aad76a60 --- /dev/null +++ b/docs/scripts/generate-cli-docs.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +import { execa } from "execa"; +import path from "node:path"; +import fs from "node:fs"; +import fg from "fast-glob"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDirectory = path.join(__dirname, "../.."); +const docsDirectory = path.join(__dirname, "../docs/generated/cli"); +const manDirectory = path.join( + rootDirectory, + ".build/plugins/GenerateManual/outputs/tuist" +); + +const generateManPages = async () => { + await execa( + "swift", + ["package", "plugin", "generate-manual", "--multi-page"], + { + cwd: rootDirectory, + stdio: "inherit", + } + ); +}; + +const runManCommand = async (filePath) => { + const { stdout } = await execa("sh", ["-c", `man ${filePath} | col -b`]); + return stdout; +}; + +const parseManPage = async (filePath) => { + const manContent = await runManCommand(filePath); + + // Remove the first line containing `TUIST.BUILD(1) General Commands Manual TUIST.BUILD(1)` + const lines = manContent.split("\n").slice(1); + + // Remove the last line + lines.pop(); + + // Convert all caps sections to subheadings + const formattedContent = lines + .map((line) => { + if (/^[A-Z ]+$/.test(line.trim())) { + return `### ${line.trim()}`; + } + return line; + }) + .join("\n"); + + return `# ${path.basename(filePath, ".1")} + +${formattedContent}`; +}; + +const generateDocs = async () => { + if (!fs.existsSync(docsDirectory)) { + fs.mkdirSync(docsDirectory, { recursive: true }); + } + + const manFiles = fg.sync(path.join(manDirectory, "*.1")); + + for (const file of manFiles) { + const commandName = path.basename(file, ".1"); + const docContent = await parseManPage(file); + + // Create directory structure based on the path + const docPathParts = commandName.split(".").slice(1); // Remove "tuist" + const docDirParts = docPathParts.slice(0, -1); // All but the last part + const docDir = path.join(docsDirectory, ...docDirParts); + const docPath = path.join(docDir, `${commandName}.md`); + + if (!fs.existsSync(docDir)) { + fs.mkdirSync(docDir, { recursive: true }); + } + + fs.writeFileSync(docPath, docContent, "utf-8"); + } + + console.log("All command docs generated successfully."); +}; + +const main = async () => { + await generateManPages(); + await generateDocs(); +}; + +main(); diff --git a/docs/scripts/generate-manifest-docs.mjs b/docs/scripts/generate-manifest-docs.mjs new file mode 100755 index 00000000000..e33cc54ab61 --- /dev/null +++ b/docs/scripts/generate-manifest-docs.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import { execa } from "execa"; +import path from "node:path"; +import fs from "node:fs"; +import fg from "fast-glob"; + +const rootDirectory = path.join(import.meta.dirname, "../.."); +const docsDirectory = path.join(import.meta.dirname, ".."); + +await execa("tuist", ["install"], { + cwd: rootDirectory, + stdio: "inherit", +}); + +await execa("tuist", ["generate", "--no-open", "--no-binary-cache"], { + cwd: rootDirectory, + stdio: "inherit", +}); + +await execa( + "sourcedocs", + [ + "generate", + "-o", + path.join(docsDirectory, "docs/generated/manifest"), + "--clean", + "--table-of-contents", + "--module-name", + "ProjectDescription", + "--", + "-scheme", + "Tuist-Workspace", + "-workspace", + path.join(rootDirectory, "Tuist.xcworkspace"), + ], + { cwd: rootDirectory, stdio: "inherit" } +); + +fs.rmSync(path.join(docsDirectory, "docs/generated/manifest/README.md")); + +fg.sync(path.join(docsDirectory, "docs/generated/manifest/**/*.md")).forEach((file) => { + const renamedPath = file.replace(/\[(.*?)\]/g, "Array<$1>"); + fs.renameSync(file, renamedPath); +}); \ No newline at end of file diff --git a/fixtures/README.md b/fixtures/README.md deleted file mode 100644 index e257dbb281f..00000000000 --- a/fixtures/README.md +++ /dev/null @@ -1,310 +0,0 @@ -# Fixtures - -This folder contains sample projects we use in the integration and acceptance tests. -Please keep this list in alphabetical order. - -## app_with_organization_name_project - -An iOS app where the organization name is defined at the `Project` level. - -## framework_with_environment_variables - -A framework project that leverages environment variables to change the name of the framework. - -## invalid_manifest - -A project with an invalid manifest. - -## invalid_workspace_manifest_name - -Contains a single file `Workspac.swift`, incorrectly named workspace manifest file. - -## ios_app_with_actions - -An iOS app with a target that has pre and post actions. - -## ios_app_with_build_variables - -An iOS app with a Xcode build variables defined in pre action. - -## ios_app_with_coredata - -A simple iOS app with a Core Data model and Mapping Model (.xcmappingmodel). - -## ios_app_with_custom_workspace - -Contains a few projects and a `Workspace.swift` manifest file. - -The workspace manifest defines: - -- glob patterns to list projects -- glob patterns to include documentation files -- folder reference to directory with html files - -The App's project manifest leverages `additionalFiles` that: - -- defines glob patterns to include documentation files -- includes a Swift `Danger.swift` file that shouldn't get included in any build phase -- defines folder references to a directory with json files - -## ios_app_with_extensions - -Sample application with extension targets. - -## ios_app_with_framework_and_resources - -A workspace with an application that includes resources. - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - Framework1: - - Framework1 (dynamic iOS framework) - - StaticFramework - - StaticFramework (static iOS framework) - - StaticFrameworkResources (iOS bundle) -``` - -Dependencies: - -- App -> Framework1 -- App -> StaticFramework -- App -> StaticFrameworkResources - -## ios_app_with_framework_linking_static_framework - -An example project demonstrating an iOS application linking a dynamic framework which itself depends on a static framework with transitive static dependencies. - -Only `Framework1.framework` should be linked and included into App, everything else should be statically linked into the Framework1 executable. - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - Framework1: - - Framework1 (dynamic iOS framework) - - Framework1Tests (iOS unit tests) - - Framework2: - - Framework2 (static iOS framework) - - Framework2Tests (iOS unit tests) - - Framework3: - - Framework3 (static iOS framework) - - Framework3Tests (iOS unit tests) - - Framework4: - - Framework4 (static iOS framework) - - Framework4Tests (iOS unit tests) -``` - -Dependencies: - -- App -> Framework1 -- Framework1 -> Framework2 -- Framework1 -> Framework3 -- Framework3 -> Framework4 - -## ios_app_with_frameworks - -Slightly more complicated project consists of an iOS app and few frameworks. - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - Framework1: - - Framework1 (dynamic iOS framework) - - Framework1Tests (iOS unit tests) - - Framework2: - - Framework2 (dynamic iOS framework) - - Framework2Tests (iOS unit tests) - - Framework3: - - Framework3 (dynamic iOS framework) - - Framework4: - - Framework4 (dynamic iOS framework) - - Framework5: - - Framework5 (dynamic iOS framework) -``` - -Dependencies: - -- App -> Framework1 -- App -> Framework2 -- Framework1 -> Framework2 -- Framework2 -> Framework3 -- Framework3 -> Framework4 -- Framework4 -> Framework5 - -## ios_app_with_helpers - -A basic iOS app that has some manifest bits extracted into helpers. - -## ios_app_with_incompatible_dependencies - -An iOS app that has a dependency with a dependency with a framework for macOS. - -## ios_app_with_incompatible_xcode - -An iOS app whose Config file requires an Xcode version that is not available in the system. - -## ios_app_with_local_swift_package - -An iOS application with local Swift package. - -## ios_app_with_multi_configs - -An workspace that contains an application and frameworks that leverage multiple configurations (Debug, Beta and Release) each of which also has an associated xcconfig file within `ConfigurationFiles`. - -## ios_app_with_remote_swift_package - -An iOS application with remote Swift package. - -## ios_app_with_sdk - -An application that contains an application target that depends on system libraries and frameworks (`.framework` and `.tbd`). - -One of the dependencies is declared as `.optional` i.e. will be linked weakly. - -## ios_app_with_static_frameworks - -This fixture contains an application that depends on static frameworks, both directly and transitively. - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - Modules - - A: - - A (static framework iOS) - - ATests (iOS unit tests) - - B: - - B (static framework iOS) - - BTests (iOS unit tests) - - C: - - C (static framework iOS) - - CTests (iOS unit tests) - - D: - - D (dynamic framework iOS) -``` - -A standalone `Prebuilt` project is used to generate a prebuilt static framework: - -``` -- Prebuilt - - PrebuiltStaticFramework (static framework iOS) -``` - -Dependencies: - -- App -> A -- App -> C -- App -> PrebuiltStaticFramework -- A -> B -- A -> C -- C -> D - -Note: to re-create `PrebuiltStaticFramework.framework` run `ios_app_with_static_frameworks/Prebuilt//build.sh` - -## ios_app_with_static_libraries - -This application provides a top level application with two static library dependencies. The first static library dependency has another static library dependency so that we are able to test how Tuist handles the transitiveness of the static libraries in the linked frameworks of the main app. - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - A: - - A (static library iOS) - - ATests (iOS unit tests) - - B: - - B (static library iOS) - - BTests (iOS unit tests) -``` - -A standalone C project is used to generate a prebuilt static library: - -``` - - C: - - C (static library iOS) - - CTests (iOS unit tests) -``` - -Dependencies: - -- App -> A -- A -> B -- A -> prebuild C (libC.a) - -Note: to re-create `libC.a` run `ios_app_with_static_libraries/Modules/C/build.sh` - -## ios_app_with_static_library_and_package - -An iOS application that depends on static library that depends on Swift package where static library is defined first. - -Note: to re-create `PrebuiltStaticFramework.framework` run `ios_app_with_static_library_and_package/Prebuilt/build.sh` - -## ios_app_with_tests - -Simple app with tests, which includes a setup manifest and uses `.notGrouped` autogenerated schemes. - -## ios_app_with_transitive_framework - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - Framework1: - - Framework1 (dynamic iOS framework) - - Framework1Tests (iOS unit tests) -``` - -A standalone Framework2 project is used to generate a prebuilt dynamic framework: - -``` - - Framework2: - - Framework2 (dynamic iOS framework) -``` - -Dependencies: - -- App -> Framework1 -- Framework1 -> Framework2 (prebuilt) - -Note: to re-create `Framework2.framework` run `ios_app_with_transitive_framework/Framework2/build.sh` - -## ios_app_with_xcframeworks - -``` -Workspace: - - App: - - MainApp (iOS app) - - MainAppTests (iOS unit tests) - - MyFramework: - - MyFramework (dynamic iOS framework) - - MyStaticFramework: - - MyStaticFramework (static iOS framework) - - MyStaticLibirary: - - MyStaticLibrary (static iOS libraries) -``` - -An example of an application which depends on prebuilt `.xcframework`s. - -The `.xcframework` can be obtained by running the `build.sh` script within the each of the xcframework directories -e.g. `ios_app_with_xcframeworks/XCFrameworks/MyFramework/build.sh`. - -## ios_workspace_with_dependency_cycle - -An example of a workspace that has a dependency cycle between targets in different projects. - -## macos_app_with_extensions - -The project contains a macOS app with various types of extensions. - -## manifest_with_logs - -A project that contains logs to verify that the logs are forwarded by Tuist. diff --git a/fixtures/app_with_build_rules/Project.swift b/fixtures/app_with_build_rules/Project.swift index a7a9e6919d9..a0c7bd78d60 100644 --- a/fixtures/app_with_build_rules/Project.swift +++ b/fixtures/app_with_build_rules/Project.swift @@ -4,14 +4,14 @@ let project = Project( name: "App", organizationName: "Tuist", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", sources: ["App/*"], buildRules: [ - .init( + .buildRule( name: "Process_InfoPlist.strings", fileType: .sourceFilesWithNamesMatching, filePatterns: "*/InfoPlist.strings", diff --git a/fixtures/app_with_build_rules/README.md b/fixtures/app_with_build_rules/README.md new file mode 100644 index 00000000000..7cb7e4be3d8 --- /dev/null +++ b/fixtures/app_with_build_rules/README.md @@ -0,0 +1,3 @@ +# Application with build rules + +This example contains an example that uses [build rules](https://developer.apple.com/documentation/xcode/creating-build-rules-for-custom-file-types), which instruct Xcode on how to compile a particular file. \ No newline at end of file diff --git a/fixtures/app_with_custom_default_configuration/.gitignore b/fixtures/app_with_custom_default_configuration/.gitignore new file mode 100644 index 00000000000..24b244f9c45 --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/.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/.build \ No newline at end of file diff --git a/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..9221b9bb1a3 --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/App/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/app_with_custom_default_configuration/App/Resources/Assets.xcassets/Contents.json b/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/App/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Templates/LaunchScreen+iOS.stencil b/fixtures/app_with_custom_default_configuration/App/Resources/LaunchScreen.storyboard similarity index 100% rename from Templates/LaunchScreen+iOS.stencil rename to fixtures/app_with_custom_default_configuration/App/Resources/LaunchScreen.storyboard diff --git a/fixtures/app_with_custom_default_configuration/App/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/fixtures/app_with_custom_default_configuration/App/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/App/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/app_with_custom_default_configuration/App/Sources/AppApp.swift b/fixtures/app_with_custom_default_configuration/App/Sources/AppApp.swift new file mode 100644 index 00000000000..1295f66aa3e --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/App/Sources/AppApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct AppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/app_with_custom_default_configuration/App/Sources/ContentView.swift b/fixtures/app_with_custom_default_configuration/App/Sources/ContentView.swift new file mode 100644 index 00000000000..48f62ebea83 --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/App/Sources/ContentView.swift @@ -0,0 +1,16 @@ +import SwiftUI + +public struct ContentView: View { + public init() {} + + public var body: some View { + Text("Hello, World!") + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/fixtures/app_with_custom_default_configuration/App/Tests/AppTests.swift b/fixtures/app_with_custom_default_configuration/App/Tests/AppTests.swift new file mode 100644 index 00000000000..70c453ba3f3 --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/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/app_with_custom_default_configuration/Framework/Sources/Framework.swift b/fixtures/app_with_custom_default_configuration/Framework/Sources/Framework.swift new file mode 100644 index 00000000000..19a008d12ea --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/Framework/Sources/Framework.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum Framework { + public static func hello() -> String { + "Hello from Framework!" + } +} diff --git a/fixtures/app_with_custom_default_configuration/Project.swift b/fixtures/app_with_custom_default_configuration/Project.swift new file mode 100644 index 00000000000..65012836f5b --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/Project.swift @@ -0,0 +1,40 @@ +import ProjectDescription + +let project = Project( + name: "App", + targets: [ + .target( + name: "App", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.App", + infoPlist: .extendingDefault( + with: [ + "UILaunchStoryboardName": "LaunchScreen.storyboard", + ] + ), + sources: ["App/Sources/**"], + resources: ["App/Resources/**"], + dependencies: [ + .target(name: "Framework"), + ] + ), + .target( + name: "AppTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.AppTests", + infoPlist: .default, + sources: ["App/Tests/**"], + resources: [], + dependencies: [.target(name: "App")] + ), + .target( + name: "Framework", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.framework", + sources: ["Framework/Sources/**"] + ), + ] +) diff --git a/fixtures/app_with_custom_default_configuration/README.md b/fixtures/app_with_custom_default_configuration/README.md new file mode 100644 index 00000000000..ed4987f6b5d --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/README.md @@ -0,0 +1,3 @@ +# App with a custom default configuration + +App that showcases a custom default configuration generationo option. \ No newline at end of file diff --git a/fixtures/app_with_custom_default_configuration/Tuist/Config.swift b/fixtures/app_with_custom_default_configuration/Tuist/Config.swift new file mode 100644 index 00000000000..c38ad99b85c --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/Tuist/Config.swift @@ -0,0 +1,7 @@ +import ProjectDescription + +let config = Config( + generationOptions: .options( + defaultConfiguration: "Release" + ) +) diff --git a/fixtures/app_with_custom_default_configuration/Tuist/Package.swift b/fixtures/app_with_custom_default_configuration/Tuist/Package.swift new file mode 100644 index 00000000000..c22106db8fe --- /dev/null +++ b/fixtures/app_with_custom_default_configuration/Tuist/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + // Customize the product types for specific package product + // Default is .staticFramework + // productTypes: ["Alamofire": .framework,] + productTypes: [:] + ) +#endif + +let package = Package( + name: "App", + dependencies: [ + // Add your own dependencies here: + // .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), + // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies + ] +) diff --git a/fixtures/app_with_framework_and_tests/Project.swift b/fixtures/app_with_framework_and_tests/Project.swift index 789701b0103..aa3492ed550 100644 --- a/fixtures/app_with_framework_and_tests/Project.swift +++ b/fixtures/app_with_framework_and_tests/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", @@ -14,9 +14,9 @@ let project = Project( .target(name: "Framework"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.appTests", infoPlist: "Info.plist", @@ -25,9 +25,9 @@ let project = Project( .target(name: "App"), ] ), - Target( + .target( name: "Framework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.framework", infoPlist: "Info.plist", @@ -35,9 +35,9 @@ let project = Project( dependencies: [ ] ), - Target( + .target( name: "FrameworkTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.frameworkTests", infoPlist: "Info.plist", @@ -48,7 +48,7 @@ let project = Project( ), ], schemes: [ - Scheme( + .scheme( name: "AppCustomScheme", buildAction: .buildAction(targets: [TargetReference("App")]) ), diff --git a/fixtures/app_with_google_maps/.gitignore b/fixtures/app_with_google_maps/.gitignore new file mode 100644 index 00000000000..24b244f9c45 --- /dev/null +++ b/fixtures/app_with_google_maps/.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/.build \ No newline at end of file diff --git a/fixtures/app_with_google_maps/App/Sources/AppDelegate.swift b/fixtures/app_with_google_maps/App/Sources/AppDelegate.swift new file mode 100644 index 00000000000..64d05f188cb --- /dev/null +++ b/fixtures/app_with_google_maps/App/Sources/AppDelegate.swift @@ -0,0 +1,24 @@ +import DynamicFramework +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) + + Mapper.provide(key: "key_not_need_to_see_bundle_missing_crash") + + let viewController = UIViewController() + viewController.view.backgroundColor = .white + viewController.view.addSubview(Mapper(frame: viewController.view.bounds)) + window?.rootViewController = viewController + window?.makeKeyAndVisible() + + return true + } +} diff --git a/fixtures/app_with_google_maps/DynamicFramework/Sources/DynamicFramework.swift b/fixtures/app_with_google_maps/DynamicFramework/Sources/DynamicFramework.swift new file mode 100644 index 00000000000..a6094d24140 --- /dev/null +++ b/fixtures/app_with_google_maps/DynamicFramework/Sources/DynamicFramework.swift @@ -0,0 +1,26 @@ +import Foundation +import GoogleMaps +import UIKit + +public class Mapper: UIView { + public static func provide(key: String) { + GMSServices.provideAPIKey(key) + } + + override public init(frame: CGRect) { + super.init(frame: frame) + + let mapView = GMSMapView() + mapView.delegate = self + addSubview(mapView) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension Mapper: GMSMapViewDelegate { + public func mapView(_: GMSMapView, didChange _: GMSCameraPosition) {} +} diff --git a/fixtures/app_with_google_maps/Project.swift b/fixtures/app_with_google_maps/Project.swift new file mode 100644 index 00000000000..31083c89377 --- /dev/null +++ b/fixtures/app_with_google_maps/Project.swift @@ -0,0 +1,29 @@ +import ProjectDescription + +let project = Project( + name: "App", + targets: [ + .target( + name: "App", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.App", + sources: ["App/Sources/**"], + dependencies: [ + .target(name: "DynamicFramework"), + ] + ), + .target( + name: "DynamicFramework", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.DynamicFramework", + sources: ["DynamicFramework/Sources/**"], + dependencies: [ + .external(name: "GoogleMaps"), + .external(name: "GoogleMapsBase"), + .external(name: "GoogleMapsCore"), + ] + ), + ] +) diff --git a/fixtures/app_with_google_maps/README.md b/fixtures/app_with_google_maps/README.md new file mode 100644 index 00000000000..e2b39119880 --- /dev/null +++ b/fixtures/app_with_google_maps/README.md @@ -0,0 +1,3 @@ +# App with Google Maps + +An example of integrating an app with Google Maps. The `GoogleMaps` dependency is imported into the App through a dynamic framework. \ No newline at end of file diff --git a/fixtures/app_with_google_maps/Tuist/Config.swift b/fixtures/app_with_google_maps/Tuist/Config.swift new file mode 100644 index 00000000000..37390c94e35 --- /dev/null +++ b/fixtures/app_with_google_maps/Tuist/Config.swift @@ -0,0 +1,9 @@ +import ProjectDescription + +let config = Config( + // Create an account with "tuist auth" and a project with "tuist project create" +// then uncomment the section below and set the project full-handle. +// * Read more: https://docs.tuist.io/guides/quick-start/gather-insights +// +// fullHandle: "{account_handle}/{project_handle}", +) diff --git a/fixtures/app_with_google_maps/Tuist/Package.resolved b/fixtures/app_with_google_maps/Tuist/Package.resolved new file mode 100644 index 00000000000..f018ee54bd1 --- /dev/null +++ b/fixtures/app_with_google_maps/Tuist/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "ios-maps-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googlemaps/ios-maps-sdk", + "state" : { + "revision" : "bd392d7d844b49ec6795871a3c25edd01ab839f2", + "version" : "8.4.0" + } + } + ], + "version" : 2 +} diff --git a/fixtures/app_with_google_maps/Tuist/Package.swift b/fixtures/app_with_google_maps/Tuist/Package.swift new file mode 100644 index 00000000000..8b3eb7cc3f9 --- /dev/null +++ b/fixtures/app_with_google_maps/Tuist/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + // Customize the product types for specific package product + // Default is .staticFramework + // productTypes: ["Alamofire": .framework,] + productTypes: [:] + ) +#endif + +let package = Package( + name: "App", + dependencies: [ + .package(url: "https://github.com/googlemaps/ios-maps-sdk", exact: "8.4.0"), + ] +) diff --git a/fixtures/app_with_organization_name_project/Project.swift b/fixtures/app_with_organization_name_project/Project.swift index 0ee8146ee68..8f56f6ce597 100644 --- a/fixtures/app_with_organization_name_project/Project.swift +++ b/fixtures/app_with_organization_name_project/Project.swift @@ -4,9 +4,9 @@ let project = Project( name: "App", organizationName: "Tuist", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", diff --git a/fixtures/app_with_organization_name_project/README.md b/fixtures/app_with_organization_name_project/README.md new file mode 100644 index 00000000000..46ec1205b1f --- /dev/null +++ b/fixtures/app_with_organization_name_project/README.md @@ -0,0 +1,3 @@ +# App with organization name defined in `Project` + +An iOS app where the organization name is defined at the `Project` level. \ No newline at end of file diff --git a/fixtures/app_with_plugins/InspectGraph/Package.resolved b/fixtures/app_with_plugins/InspectGraph/Package.resolved new file mode 100644 index 00000000000..a3abb118378 --- /dev/null +++ b/fixtures/app_with_plugins/InspectGraph/Package.resolved @@ -0,0 +1,257 @@ +{ + "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version" : "4.6.1" + } + }, + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + }, + { + "identity" : "colorizer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getGuaka/Colorizer.git", + "state" : { + "revision" : "2ccc99bf1715e73c4139e8d40b6e6b30be975586", + "version" : "0.2.1" + } + }, + { + "identity" : "combineext", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CombineCommunity/CombineExt", + "state" : { + "revision" : "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", + "version" : "1.8.1" + } + }, + { + "identity" : "difference", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzysztofzablocki/Difference.git", + "state" : { + "revision" : "f627d00718033c3d7888acd5f4e3524a843db1cf", + "version" : "1.0.2" + } + }, + { + "identity" : "graphviz", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftDocOrg/GraphViz", + "state" : { + "revision" : "70bebcf4597b9ce33e19816d6bbd4ba9b7bdf038", + "version" : "0.2.0" + } + }, + { + "identity" : "kanna", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tid-kijyun/Kanna.git", + "state" : { + "revision" : "41c3d28ea0eac07e4551b28def9de1ede702e739", + "version" : "5.3.0" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "komondor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/Komondor.git", + "state" : { + "revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60", + "version" : "1.1.3" + } + }, + { + "identity" : "mockable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kolos65/Mockable.git", + "state" : { + "revision" : "7c35a588f6c6a79ce6e0a719dc31e49a6fec069b", + "version" : "0.0.4" + } + }, + { + "identity" : "packageconfig", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/PackageConfig.git", + "state" : { + "revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb", + "version" : "1.1.3" + } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, + { + "identity" : "queuer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FabrizioBrancati/Queuer", + "state" : { + "revision" : "52515108d0ac4616d9e15ffcc7ad986e300d31ff", + "version" : "2.1.1" + } + }, + { + "identity" : "shellout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/ShellOut.git", + "state" : { + "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version" : "2.3.0" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" + } + }, + { + "identity" : "stencil", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stencilproject/Stencil", + "state" : { + "revision" : "4f222ac85d673f35df29962fc4c36ccfdaf9da5b", + "version" : "0.15.1" + } + }, + { + "identity" : "stencilswiftkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftGen/StencilSwiftKit", + "state" : { + "revision" : "20e2de5322c83df005939d9d9300fab130b49f97", + "version" : "2.10.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core", + "state" : { + "revision" : "3b13e439a341bbbfe0f710c7d1be37221745ef1a", + "version" : "0.6.1" + } + }, + { + "identity" : "swiftgen", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftGen/SwiftGen", + "state" : { + "revision" : "1cf6d7eebd70c2157f69d5a991bc57c1ef182ed1", + "version" : "6.6.2" + } + }, + { + "identity" : "xcbeautify", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cpisciotta/xcbeautify", + "state" : { + "revision" : "3140aa2d58063dbe30426cce118f8ba8feb37e60", + "version" : "2.0.1" + } + }, + { + "identity" : "xcodeproj", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeProj", + "state" : { + "revision" : "75e787fb3eb5a8397c3d06d5a71e667ac2d20ac1", + "version" : "8.19.0" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MaxDesiatov/XMLCoder.git", + "state" : { + "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", + "version" : "0.17.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "8a835d918245ca22f36663dd3862138805d7f707", + "version" : "5.1.0" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation", + "state" : { + "revision" : "b979e8b52c7ae7f3f39fa0182e738e9e7257eb78", + "version" : "0.9.18" + } + } + ], + "version" : 2 +} diff --git a/fixtures/app_with_plugins/InspectGraph/Package.swift b/fixtures/app_with_plugins/InspectGraph/Package.swift new file mode 100644 index 00000000000..537ce7e7f79 --- /dev/null +++ b/fixtures/app_with_plugins/InspectGraph/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "InspectGraph", + platforms: [.macOS(.v12)], + products: [ + .executable( + name: "inspect-graph", + targets: ["InspectGraph"] + ), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(path: "../../../"), + ], + targets: [ + .target( + name: "InspectGraph", + dependencies: [ + .product(name: "ProjectAutomation", package: "tuist"), + ] + ), + ] +) diff --git a/fixtures/app_with_plugins/InspectGraph/Sources/InspectGraph/main.swift b/fixtures/app_with_plugins/InspectGraph/Sources/InspectGraph/main.swift new file mode 100644 index 00000000000..716bba8e1bf --- /dev/null +++ b/fixtures/app_with_plugins/InspectGraph/Sources/InspectGraph/main.swift @@ -0,0 +1,14 @@ +import Foundation +import ProjectAutomation + +let graph: Graph +if CommandLine.arguments.contains("--path"), + let path = CommandLine.arguments.last +{ + graph = try Tuist.graph(at: path) +} else { + graph = try Tuist.graph() +} + +let targets = graph.projects.values.flatMap(\.targets) +print("The current graph has the following targets: \(targets.map(\.name).joined(separator: " "))") diff --git a/fixtures/app_with_plugins/Package.swift b/fixtures/app_with_plugins/Package.swift new file mode 100644 index 00000000000..60be1a37250 --- /dev/null +++ b/fixtures/app_with_plugins/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version: 5.8 +import PackageDescription + +#if TUIST + import ExampleTuistPlugin + import LocalPlugin + import ProjectDescription + + // Note: Testing importing of plugins in local helpers + let localPlugin = LocalHelper(name: "local") + let remotePlugin = RemoteHelper(name: "remote") + + let packageSettings = PackageSettings() +#endif + +let package = Package( + name: "PackageName", + dependencies: [] +) diff --git a/fixtures/app_with_plugins/Project.swift b/fixtures/app_with_plugins/Project.swift index a6e250ad453..ec19286bbf7 100644 --- a/fixtures/app_with_plugins/Project.swift +++ b/fixtures/app_with_plugins/Project.swift @@ -10,6 +10,6 @@ let remoteHelper = RemoteHelper(name: "RemotePlugin") let project = Project.app( name: "TuistPluginTest", - platform: .iOS, + destinations: .iOS, additionalTargets: ["TuistPluginTestKit", "TuistPluginTestUI"] ) diff --git a/fixtures/app_with_plugins/Tuist/Config.swift b/fixtures/app_with_plugins/Tuist/Config.swift index 3584c3af636..3698eeb3a31 100644 --- a/fixtures/app_with_plugins/Tuist/Config.swift +++ b/fixtures/app_with_plugins/Tuist/Config.swift @@ -3,6 +3,6 @@ import ProjectDescription let config = Config( plugins: [ .local(path: .relativeToManifest("../../LocalPlugin")), - .git(url: "https://github.com/tuist/ExampleTuistPlugin", tag: "3.1.0"), + .git(url: "https://github.com/tuist/ExampleTuistPlugin", tag: "3.2.0"), ] ) diff --git a/fixtures/app_with_plugins/Tuist/Dependencies.swift b/fixtures/app_with_plugins/Tuist/Dependencies.swift deleted file mode 100644 index c8c92490384..00000000000 --- a/fixtures/app_with_plugins/Tuist/Dependencies.swift +++ /dev/null @@ -1,14 +0,0 @@ -import ExampleTuistPlugin -import LocalPlugin -import ProjectDescription - -// Note: Testing importing of plugins in local helpers -let localPlugin = LocalHelper(name: "local") -let remotePlugin = RemoteHelper(name: "remote") - -let dependencies = Dependencies( - swiftPackageManager: .init( - targetSettings: [:] - ), - platforms: [.iOS, .macOS] -) diff --git a/fixtures/app_with_plugins/Tuist/Package.swift b/fixtures/app_with_plugins/Tuist/Package.swift deleted file mode 100644 index 2eee767569e..00000000000 --- a/fixtures/app_with_plugins/Tuist/Package.swift +++ /dev/null @@ -1,7 +0,0 @@ -// swift-tools-version: 5.8 -import PackageDescription - -let package = Package( - name: "PackageName", - dependencies: [] -) diff --git a/fixtures/app_with_plugins/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/fixtures/app_with_plugins/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 39ceb9800e5..a4fe5a96e8e 100644 --- a/fixtures/app_with_plugins/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/fixtures/app_with_plugins/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -4,14 +4,14 @@ import ProjectDescription extension Project { /// Helper function to create the Project for this ExampleApp - public static func app(name: String, platform: Platform, additionalTargets _: [String]) -> Project { + public static func app(name: String, destinations: Destinations, additionalTargets _: [String]) -> Project { // Note: Testing importing of plugins in local helpers _ = LocalHelper(name: "local") _ = RemoteHelper(name: "remote") - let mainTarget = Target( + let mainTarget: Target = .target( name: name, - platform: platform, + destinations: destinations, product: .app, bundleId: "io.tuist.\(name)", infoPlist: .default, diff --git a/fixtures/app_with_previews/PreviewsFramework/Sources/TestView.swift b/fixtures/app_with_previews/PreviewsFramework/Sources/TestView.swift new file mode 100644 index 00000000000..deb9f5ac3d7 --- /dev/null +++ b/fixtures/app_with_previews/PreviewsFramework/Sources/TestView.swift @@ -0,0 +1,19 @@ +import ResourcesFramework +import SwiftUI + +struct TestView: View { + var body: some View { + VStack { + Button("Click to read file from bundle") { + text = readFileFromBundle() + } + Text(text) + } + } + + @State var text = "-" +} + +#Preview { + TestView() +} diff --git a/fixtures/app_with_previews/Project.swift b/fixtures/app_with_previews/Project.swift new file mode 100644 index 00000000000..065218163ce --- /dev/null +++ b/fixtures/app_with_previews/Project.swift @@ -0,0 +1,17 @@ +import ProjectDescription + +let project = Project( + name: "PreviewsFramework", + targets: [ + .target( + name: "PreviewsFramework", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.previewsframework", + sources: "PreviewsFramework/Sources/**", + dependencies: [ + .external(name: "ResourcesFramework"), + ] + ), + ] +) diff --git a/fixtures/app_with_previews/ResourcesFramework/Package.swift b/fixtures/app_with_previews/ResourcesFramework/Package.swift new file mode 100644 index 00000000000..e13b3d219ca --- /dev/null +++ b/fixtures/app_with_previews/ResourcesFramework/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "ResourcesFramework", + platforms: [.iOS(.v15), .macOS(.v14)], + products: [ + .library( + name: "ResourcesFramework", + type: .static, + targets: [ + "ResourcesFramework", + ] + ), + ], + targets: [ + .target( + name: "ResourcesFramework", + resources: [ + .process("Resources"), + ] + ), + ] +) diff --git a/fixtures/app_with_previews/ResourcesFramework/Sources/ResourcesFramework/BundleReader.swift b/fixtures/app_with_previews/ResourcesFramework/Sources/ResourcesFramework/BundleReader.swift new file mode 100644 index 00000000000..4ff0daa67cf --- /dev/null +++ b/fixtures/app_with_previews/ResourcesFramework/Sources/ResourcesFramework/BundleReader.swift @@ -0,0 +1,6 @@ +import Foundation + +public func readFileFromBundle() -> String { + let path = Bundle.module.url(forResource: "file", withExtension: "txt")! + return try! String(contentsOf: path) +} diff --git a/fixtures/app_with_previews/ResourcesFramework/Sources/ResourcesFramework/Resources/file.txt b/fixtures/app_with_previews/ResourcesFramework/Sources/ResourcesFramework/Resources/file.txt new file mode 100644 index 00000000000..ce013625030 --- /dev/null +++ b/fixtures/app_with_previews/ResourcesFramework/Sources/ResourcesFramework/Resources/file.txt @@ -0,0 +1 @@ +hello diff --git a/fixtures/app_with_previews/Tuist/Package.swift b/fixtures/app_with_previews/Tuist/Package.swift new file mode 100644 index 00000000000..2f00c24619f --- /dev/null +++ b/fixtures/app_with_previews/Tuist/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "project_with_previews_crash", + dependencies: [ + .package(path: "../ResourcesFramework"), + ] +) diff --git a/fixtures/app_with_spm_dependencies/App/Project.swift b/fixtures/app_with_spm_dependencies/App/Project.swift index 99f21142c04..13a1ace9a55 100644 --- a/fixtures/app_with_spm_dependencies/App/Project.swift +++ b/fixtures/app_with_spm_dependencies/App/Project.swift @@ -5,9 +5,9 @@ let project = Project( name: "App", settings: .projectSettings, targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: .default, @@ -16,12 +16,14 @@ let project = Project( .target(name: "AppKit"), .project(target: "FeatureOneFramework_iOS", path: .relativeToRoot("Features/FeatureOne")), .external(name: "Styles"), + .external(name: "BrazeKit"), + .external(name: "BrazeUI"), ], settings: .targetSettings ), - Target( + .target( name: "AppKit", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.app.kit", infoPlist: .default, @@ -30,27 +32,67 @@ let project = Project( .sdk(name: "c++", type: .library, status: .required), .external(name: "Alamofire"), .external(name: "ComposableArchitecture"), + .external(name: "ZipArchive"), + .external(name: "Yams"), + .external(name: "GoogleSignIn"), + .external(name: "Sentry"), + .external(name: "RealmSwift"), + .external(name: "CocoaLumberjackSwift"), + .external(name: "AppCenterAnalytics"), + .external(name: "AppCenterCrashes"), + .external(name: "libzstd"), + .external(name: "NYTPhotoViewer"), + .external(name: "SVProgressHUD"), + .external(name: "AirshipPreferenceCenter"), + .external(name: "MarkdownUI"), + .external(name: "GoogleMobileAds"), + .external(name: "LookinServer"), ], settings: .targetSettings ), - Target( + .target( + name: "AppKitTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.app.kit", + infoPlist: .default, + sources: "Tests/AppKit/**", + dependencies: [ + .target(name: "AppKit"), + .external(name: "Nimble"), + .external(name: "Testing"), + .external(name: "Cuckoo"), + ], + settings: .targetSettings + ), + .target( + name: "VisionOSApp", + destinations: [.appleVision], + product: .app, + bundleId: "io.tuist.app.applevision", + sources: ["Sources/VisionOS/App/**"], + dependencies: [ + .external(name: "Alamofire"), + ] + ), + .target( name: "WatchApp", - platform: .watchOS, + destinations: [.appleWatch], product: .watch2App, bundleId: "io.tuist.app.watchapp", infoPlist: .extendingDefault( with: [ "WKCompanionAppBundleIdentifier": "io.tuist.app", ] - ), - sources: ["Sources/Watch/App/**"], + ), sources: ["Sources/Watch/App/**"], + dependencies: [ .target(name: "WatchExtension"), ] ), - Target( + .target( name: "WatchExtension", - platform: .watchOS, + destinations: [.appleWatch], product: .watch2Extension, bundleId: "io.tuist.app.watchapp.extension", sources: ["Sources/Watch/Extension/**"], diff --git a/fixtures/app_with_spm_dependencies/App/Sources/App/AppDelegate.swift b/fixtures/app_with_spm_dependencies/App/Sources/App/AppDelegate.swift index 53774a9ffcb..7706415911f 100644 --- a/fixtures/app_with_spm_dependencies/App/Sources/App/AppDelegate.swift +++ b/fixtures/app_with_spm_dependencies/App/Sources/App/AppDelegate.swift @@ -1,4 +1,5 @@ import AppKit +import BrazeUI import Styles import UIKit @@ -36,6 +37,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fatalError("singleFile is missing") } + let brazeUILocalizedString = BrazeUIResources.bundle?.localizedString( + forKey: "braze.in-app-message.close-button.title", + value: nil, + table: "InAppMessageLocalizable" + ) + precondition(brazeUILocalizedString == "Close", "Failed to fetch localized resource from BrazeUI") + AppKit.start() return true diff --git a/fixtures/app_with_spm_dependencies/App/Sources/AppKit/AppKit.swift b/fixtures/app_with_spm_dependencies/App/Sources/AppKit/AppKit.swift index 475b7236772..0494c355205 100644 --- a/fixtures/app_with_spm_dependencies/App/Sources/AppKit/AppKit.swift +++ b/fixtures/app_with_spm_dependencies/App/Sources/AppKit/AppKit.swift @@ -1,5 +1,21 @@ import Alamofire +import AppCenter +import AppCenterAnalytics +import AppCenterCrashes +import CocoaLumberjackSwift import ComposableArchitecture +import CrashReporter +import GoogleMobileAds +import GoogleSignIn +import libzstd +import MarkdownUI +import NYTPhotoViewer +import Realm +import RealmSwift +import Sentry +import SVProgressHUD +import Yams +import ZipArchive public enum AppKit { public static func start() { @@ -8,6 +24,39 @@ public enum AppKit { // Use ComposableArchitecture to make sure it links fine _ = EmptyReducer() + + // Use ZipArchive + _ = SSZipArchive.createZipFile(atPath: #file + "/ss.zip", withFilesAtPaths: []) + + // Use Yams + _ = YAMLEncoder() + + // Use GoogleSignIn + _ = GIDSignIn.sharedInstance.hasPreviousSignIn() + + // Use Sentry + SentrySDK.startSession() + + // Use CocoaLumberjack + _ = DDOSLogger.sharedInstance + + // Use Realm + _ = Realm.Configuration() + + // Use AppCenter + AppCenter.start(withAppSecret: "{Your App Secret}", services: [Analytics.self, Crashes.self]) + + // Use libzstd + _ = ZDICT_isError(0) + + // Use NYTPhotoViewer + _ = NYTPhotosOverlayView() + + // Use SVProgressHUD + SVProgressHUD.show() + + // Use MarkdownUI + _ = BulletedList(of: [""]) } } diff --git a/fixtures/app_with_spm_dependencies/App/Sources/VisionOS/App/VisionOSApp.swift b/fixtures/app_with_spm_dependencies/App/Sources/VisionOS/App/VisionOSApp.swift new file mode 100644 index 00000000000..407db871a00 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/App/Sources/VisionOS/App/VisionOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct VisionOSTestApp: App { + var body: some Scene { + WindowGroup { + Text("Hello, world!") + } + } +} diff --git a/fixtures/app_with_spm_dependencies/App/Tests/AppKit/AppKitTests.swift b/fixtures/app_with_spm_dependencies/App/Tests/AppKit/AppKitTests.swift new file mode 100644 index 00000000000..85cf7b5bb37 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/App/Tests/AppKit/AppKitTests.swift @@ -0,0 +1,9 @@ +import Cuckoo +import Nimble +import Testing + +struct AppKitTestingTests { + @Test func example() { + expect(1).to(equal(1)) + } +} diff --git a/fixtures/app_with_spm_dependencies/Features/FeatureOne/Project.swift b/fixtures/app_with_spm_dependencies/Features/FeatureOne/Project.swift index 78fc4e97174..d391680ec4e 100644 --- a/fixtures/app_with_spm_dependencies/Features/FeatureOne/Project.swift +++ b/fixtures/app_with_spm_dependencies/Features/FeatureOne/Project.swift @@ -5,20 +5,22 @@ let project = Project( name: "FeatureOne", settings: .projectSettings, targets: [ - Target( + .target( name: "FeatureOneFramework_iOS", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.featureOne", - sources: ["Sources/**"], + sources: ["Sources/*.{swift,m}"], + headers: .headers(public: "Sources/*.h"), dependencies: [ .external(name: "Alamofire"), + .external(name: "UICKeyChainStore"), ], settings: .targetSettings ), - Target( + .target( name: "FeatureOneFramework_watchOS", - platform: .watchOS, + destinations: [.appleWatch], product: .framework, bundleId: "io.tuist.featureOne", sources: ["Sources/**"], diff --git a/fixtures/app_with_spm_dependencies/Features/FeatureOne/Sources/ObjectiveFeature.h b/fixtures/app_with_spm_dependencies/Features/FeatureOne/Sources/ObjectiveFeature.h new file mode 100644 index 00000000000..3b71c380df2 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/Features/FeatureOne/Sources/ObjectiveFeature.h @@ -0,0 +1,16 @@ +// +// ObjectiveFeature.h +// FeatureOne +// +// Created by Shai Mishali on 23/03/2024. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ObjectiveFeature : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/fixtures/app_with_spm_dependencies/Features/FeatureOne/Sources/ObjectiveFeature.m b/fixtures/app_with_spm_dependencies/Features/FeatureOne/Sources/ObjectiveFeature.m new file mode 100644 index 00000000000..ce0c0949d81 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/Features/FeatureOne/Sources/ObjectiveFeature.m @@ -0,0 +1,13 @@ +// +// ObjectiveFeature.m +// FeatureOne +// +// Created by Shai Mishali on 23/03/2024. +// + +#import "ObjectiveFeature.h" +@import UICKeyChainStore; + +@implementation ObjectiveFeature + +@end diff --git a/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Package.swift b/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Package.swift index dbe2c97515c..37298664a9c 100644 --- a/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Package.swift +++ b/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Package.swift @@ -15,7 +15,7 @@ let package = Package( .target( name: "Styles", dependencies: [ - .product(name: "LibraryA", package: "LocalSwiftPackageB", condition: .when(platforms: [.macOS])), + .product(name: "LibraryA", package: "LocalSwiftPackageB"), ], resources: [ .process("Resources/Fonts"), diff --git a/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Sources/Styles/StylesResources.swift b/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Sources/Styles/StylesResources.swift new file mode 100644 index 00000000000..c2a22774a23 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/LocalSwiftPackage/Sources/Styles/StylesResources.swift @@ -0,0 +1,10 @@ +import Foundation + +// Public Accessors + +@objc +public class StylesResources: NSObject { + @objc public class var bundle: Bundle { + return .module + } +} diff --git a/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Package.swift b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Package.swift index 6e0a5f75736..449874f7c08 100644 --- a/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Package.swift +++ b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Package.swift @@ -8,6 +8,10 @@ let package = Package( defaultLocalization: "en", products: [.library(name: "LibraryA", targets: ["LibraryA"])], targets: [ - .target(name: "LibraryA"), - ] + .target(name: "LibraryA", dependencies: ["LibraryAProxy"]), + .target( + name: "LibraryAProxy" + ), + ], + cxxLanguageStandard: .cxx17 ) diff --git a/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryA/MyStruct.swift b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryA/MyStruct.swift index f214625efe7..44a0d843c16 100644 --- a/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryA/MyStruct.swift +++ b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryA/MyStruct.swift @@ -1,3 +1,3 @@ -import AppKit +import LibraryAProxy public struct MyStruct {} diff --git a/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryAProxy/Test.mm b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryAProxy/Test.mm new file mode 100644 index 00000000000..202415b8d08 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryAProxy/Test.mm @@ -0,0 +1,5 @@ +#import "Test.h" + +@implementation Test { } + +@end diff --git a/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryAProxy/include/Test.h b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryAProxy/include/Test.h new file mode 100644 index 00000000000..1a061ba95d5 --- /dev/null +++ b/fixtures/app_with_spm_dependencies/LocalSwiftPackageB/Sources/LibraryAProxy/include/Test.h @@ -0,0 +1,5 @@ +#import + +@interface Test : NSObject + +@end diff --git a/fixtures/app_with_spm_dependencies/README.md b/fixtures/app_with_spm_dependencies/README.md new file mode 100644 index 00000000000..b98f319091e --- /dev/null +++ b/fixtures/app_with_spm_dependencies/README.md @@ -0,0 +1,5 @@ +# Application with Swift Package Manager Dependencies + +This example contains an example that uses Swift Package Manager dependencies. + +It also contains a static Objective-C dependency that relies on the `-ObjC` linker flag to be added to the consuming application. \ No newline at end of file diff --git a/fixtures/app_with_spm_dependencies/StringifyMacro/Package.swift b/fixtures/app_with_spm_dependencies/StringifyMacro/Package.swift index 1e5277f8859..7609713af3e 100644 --- a/fixtures/app_with_spm_dependencies/StringifyMacro/Package.swift +++ b/fixtures/app_with_spm_dependencies/StringifyMacro/Package.swift @@ -20,7 +20,7 @@ let package = Package( ], dependencies: [ // Depend on the latest Swift 5.9 prerelease of SwiftSyntax - .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-07-10-a"), + .package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/fixtures/app_with_spm_dependencies/Tuist/Package.resolved b/fixtures/app_with_spm_dependencies/Tuist/Package.resolved index e3f66daf7c1..41320f78ca3 100644 --- a/fixtures/app_with_spm_dependencies/Tuist/Package.resolved +++ b/fixtures/app_with_spm_dependencies/Tuist/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "a18b44e37498cdc083fe1514666ea3bbc090f84d03b3b5888bf4d09314e4a1db", "pins" : [ { "identity" : "alamofire", @@ -9,6 +10,42 @@ "version" : "5.8.0" } }, + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "71cde449f13d453227e687458144bde372d30fc7", + "version" : "1.6.2" + } + }, + { + "identity" : "appcenter-sdk-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/microsoft/appcenter-sdk-apple", + "state" : { + "revision" : "1120c26835925f8314d035127c580bc71689c620", + "version" : "5.0.4" + } + }, + { + "identity" : "braze-swift-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/braze-inc/braze-swift-sdk.git", + "state" : { + "revision" : "f893978502a08b02124d2d15c6117eb1a06b7f6f", + "version" : "8.4.0" + } + }, + { + "identity" : "cocoalumberjack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack", + "state" : { + "revision" : "af4973721172dd7850a42a58a9ffcec0ec82e5a9", + "version" : "3.8.4" + } + }, { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", @@ -18,13 +55,175 @@ "version" : "1.0.0" } }, + { + "identity" : "cuckoo", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Brightify/Cuckoo.git", + "state" : { + "revision" : "0cfbb729d0aced6494c19c58c0c31ea16d1cbf66", + "version" : "1.10.4" + } + }, + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", + "version" : "2.1.2" + } + }, + { + "identity" : "cwlpreconditiontesting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state" : { + "revision" : "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", + "version" : "2.2.0" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS", + "state" : { + "revision" : "7932d33686c1dc4d7df7a919aae47361d1cdfda4", + "version" : "7.0.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", + "version" : "3.3.1" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "cee3c709307912d040bd1e06ca919875a92339c6", + "version" : "2.0.0" + } + }, + { + "identity" : "ios-library", + "kind" : "remoteSourceControl", + "location" : "https://github.com/urbanairship/ios-library.git", + "state" : { + "revision" : "53040c77617a2acc5d9a7b69cf5bbbf36f94ad4b", + "version" : "17.7.3" + } + }, + { + "identity" : "lookinserver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QMUI/LookinServer", + "state" : { + "revision" : "e553d1b689d147817dc54ad5c28fcff71e860101", + "version" : "1.2.8" + } + }, + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble", + "state" : { + "revision" : "c1f3dd66222d5e7a1a20afc237f7e7bc432c564f", + "version" : "13.2.0" + } + }, + { + "identity" : "nytphotoviewer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/NYTPhotoViewer", + "state" : { + "branch" : "develop", + "revision" : "e4acd0988c43e3df0908fc463104e567098f9dfc" + } + }, + { + "identity" : "plcrashreporter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/microsoft/PLCrashReporter.git", + "state" : { + "revision" : "1aed8f7dc79ce8e674c61e430ef51ca3db18cea9", + "version" : "1.11.1" + } + }, + { + "identity" : "quick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Quick", + "state" : { + "revision" : "a9e6c24033a25e158384d523a556c0b96c44a839", + "version" : "7.4.0" + } + }, + { + "identity" : "realm-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/realm/realm-core.git", + "state" : { + "revision" : "a5e87a39cffdcc591f3203c11cfca68100d0b9a6", + "version" : "13.26.0" + } + }, + { + "identity" : "realm-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/realm/realm-swift", + "state" : { + "revision" : "eafdd3720a8cc750bdd38bf776082d2c8cf743fc", + "version" : "10.46.0" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "f6afa0132961d593f07970d84e2d8b588c29ea04", + "version" : "5.19.1" + } + }, + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa", + "state" : { + "revision" : "5421f94cc859eb65f5ae3866165a053aa634431e", + "version" : "8.32.0" + } + }, + { + "identity" : "svprogresshud", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SVProgressHUD/SVProgressHUD", + "state" : { + "revision" : "c33f7c775ba7feea6047a1fc3257f2e5863b44f7", + "version" : "2.3.1" + } + }, { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "a5521dde99570789d8cb7c43e51418d7cd1a87ca", - "version" : "1.1.2" + "revision" : "e593aba2c6222daad7c4f2732a431eed2c09bb07", + "version" : "1.3.0" } }, { @@ -32,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", - "version" : "1.0.0" + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" } }, { @@ -41,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", - "version" : "1.0.5" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -50,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "dcde72151de8a60eecaa1673ed3c3d110549069a", - "version" : "1.5.1" + "revision" : "115fe5af41d333b6156d4924d7c7058bc77fd580", + "version" : "1.9.2" } }, { @@ -68,8 +267,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "65fc9e2b62727cacfab9fc60d580c284a4b9308c", - "version" : "1.1.1" + "revision" : "6ea3b1b6a4957806d72030a32360d4bcb155a0d2", + "version" : "1.2.0" } }, { @@ -77,8 +276,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "9783b58167f7618cb86011156e741cbc6f4cc864", - "version" : "1.1.2" + "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", + "version" : "1.2.2" } }, { @@ -90,13 +289,67 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui", + "state" : { + "revision" : "ae799d015a5374708f7b4c85f3294c05f2a564e2", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-package-manager-google-mobile-ads", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads", + "state" : { + "revision" : "ef8d21fc890eed1727195632d63a9333b9949253", + "version" : "11.1.0" + } + }, + { + "identity" : "swift-package-manager-google-user-messaging-platform", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git", + "state" : { + "revision" : "668673ea23b3e71b9f2540d8fb9464c4dc32ecc0", + "version" : "2.2.0" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "83fc3d89676b33f1c3d49e9268e76ef62430339f", + "version" : "1.1.3" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" + } + }, + { + "identity" : "swift-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-testing", + "state" : { + "revision" : "8097dd8bcf7f2ed85f8aa8883635ce413012f53b", + "version" : "0.6.0" } }, { @@ -104,8 +357,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", - "version" : "1.2.0" + "revision" : "d9e72f3083c08375794afa216fb2f89c0114f303", + "version" : "1.2.1" + } + }, + { + "identity" : "uickeychainstore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/UICKeyChainStore", + "state" : { + "revision" : "8220ac38124613fb709508426f75fbac6921e261", + "version" : "2.2.1" } }, { @@ -113,10 +375,37 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", - "version" : "1.0.2" + "revision" : "b58e6627149808b40634c4552fcf2f44d0b3ca87", + "version" : "1.1.0" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" + } + }, + { + "identity" : "ziparchive", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ZipArchive/ZipArchive", + "state" : { + "revision" : "79d4dc9729096c6ad83dd3cee2b9f354d1b4ab7b", + "version" : "2.5.5" + } + }, + { + "identity" : "zstd", + "kind" : "remoteSourceControl", + "location" : "https://github.com/facebook/zstd", + "state" : { + "revision" : "63779c798237346c2b245c546c40b72a5a5913fe", + "version" : "1.5.5" } } ], - "version" : 2 + "version" : 3 } diff --git a/fixtures/app_with_spm_dependencies/Tuist/Package.swift b/fixtures/app_with_spm_dependencies/Tuist/Package.swift index 2eea13cbc7a..1f05b908ea2 100644 --- a/fixtures/app_with_spm_dependencies/Tuist/Package.swift +++ b/fixtures/app_with_spm_dependencies/Tuist/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 5.10 import PackageDescription #if TUIST @@ -9,8 +9,7 @@ import PackageDescription baseSettings: .targetSettings, projectOptions: [ "LocalSwiftPackage": .options(disableSynthesizedResourceAccessors: false), - ], - platforms: [.iOS, .watchOS] + ] ) #endif @@ -19,8 +18,32 @@ let package = Package( name: "PackageName", dependencies: [ .package(url: "https://github.com/Alamofire/Alamofire", exact: "5.8.0"), - .package(url: "https://github.com/pointfreeco/swift-composable-architecture", .upToNextMinor(from: "1.5.0")), - .package(path: "../../../LocalSwiftPackage"), - .package(path: "../../../StringifyMacro"), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture", .upToNextMinor(from: "1.9.2")), + .package(url: "https://github.com/ZipArchive/ZipArchive", .upToNextMajor(from: "2.5.5")), + .package(url: "https://github.com/jpsim/Yams", .upToNextMajor(from: "5.0.6")), + .package(url: "https://github.com/google/GoogleSignIn-iOS", .upToNextMajor(from: "7.0.0")), + .package(url: "https://github.com/getsentry/sentry-cocoa", .upToNextMajor(from: "8.32.0")), + .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.46.0")), + .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.8.4")), + .package(url: "https://github.com/facebook/zstd", exact: "1.5.5"), + .package(url: "https://github.com/microsoft/appcenter-sdk-apple", .upToNextMajor(from: "5.0.4")), + // Has SWIFTPM_MODULE_BUNDLE + .package(url: "https://github.com/tuist/NYTPhotoViewer", branch: "develop"), + .package(url: "https://github.com/Quick/Quick", exact: "7.4.0"), + .package(url: "https://github.com/Quick/Nimble", exact: "13.2.0"), + .package(url: "https://github.com/SVProgressHUD/SVProgressHUD", exact: "2.3.1"), + // Has missing resources and its own resource bundle accessors + .package(url: "https://github.com/urbanairship/ios-library.git", exact: "17.7.3"), + .package(url: "https://github.com/braze-inc/braze-swift-sdk.git", exact: "8.4.0"), + // Has an umbrella header where moduleName must be sanitized + .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.2.0"), + .package(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", from: "11.1.0"), + .package(url: "https://github.com/apple/swift-testing", .upToNextMajor(from: "0.6.0")), + .package(path: "../LocalSwiftPackage"), + .package(path: "../StringifyMacro"), + .package(url: "https://github.com/kishikawakatsumi/UICKeyChainStore", exact: "2.2.1"), + .package(url: "https://github.com/QMUI/LookinServer", from: "1.2.8"), + // Has XCTest API in a non-test target. Tuist will add Test Search path to support it + .package(url: "https://github.com/Brightify/Cuckoo.git", exact: "1.10.4"), ] ) diff --git a/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift b/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift index 30c868338ac..1514659f5b1 100644 --- a/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift +++ b/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift @@ -52,28 +52,4 @@ extension Scheme { .map { scheme(for: $0, target: target, executable: executable) } } } - - public static func scheme( - name: String, - shared: Bool = true, - hidden: Bool = false, - buildAction: ProjectDescription.BuildAction? = nil, - testAction: ProjectDescription.TestAction? = nil, - runAction: ProjectDescription.RunAction? = nil, - archiveAction: ProjectDescription.ArchiveAction? = nil, - profileAction: ProjectDescription.ProfileAction? = nil, - analyzeAction: ProjectDescription.AnalyzeAction? = nil - ) -> Scheme { - Scheme( - name: name, - shared: shared, - hidden: hidden, - buildAction: buildAction, - testAction: testAction, - runAction: runAction, - archiveAction: archiveAction, - profileAction: profileAction, - analyzeAction: analyzeAction - ) - } } diff --git a/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Settings+Templates.swift b/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Settings+Templates.swift index f0d57112bfb..5db66ebcd62 100644 --- a/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Settings+Templates.swift +++ b/fixtures/app_with_spm_dependencies/Tuist/ProjectDescriptionHelpers/Settings+Templates.swift @@ -10,6 +10,10 @@ extension ProjectDescription.Settings { public static var targetSettings: Self { .settings( + base: [ + "SOME_BASE_FLAG": .string("VALUE"), + "OTHER_LDFLAGS": .string("-ObjC"), + ].otherSwiftFlags("-enable-actor-data-race-checks"), configurations: BuildEnvironment.allCases.map(\.targetConfiguration) ) } diff --git a/fixtures/app_with_tasks/Project.swift b/fixtures/app_with_tasks/Project.swift index 71c6a391fdb..cd890817d70 100644 --- a/fixtures/app_with_tasks/Project.swift +++ b/fixtures/app_with_tasks/Project.swift @@ -22,6 +22,6 @@ import ProjectDescriptionHelpers // Creates our project using a helper function defined in ProjectDescriptionHelpers let project = Project.app( name: "App", - platform: .iOS, + destinations: .iOS, additionalTargets: ["AppKit", "AppUI"] ) diff --git a/fixtures/app_with_tasks/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/fixtures/app_with_tasks/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 783f3223cb5..3b67c6db491 100644 --- a/fixtures/app_with_tasks/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/fixtures/app_with_tasks/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -25,7 +25,7 @@ extension Project { /// Helper function to create a framework target and an associated unit test target private static func makeFrameworkTargets(name: String, platform: Platform) -> [Target] { - let sources = Target( + let sources = .target( name: name, platform: platform, product: .framework, @@ -35,7 +35,7 @@ extension Project { resources: [], dependencies: [] ) - let tests = Target( + let tests = .target( name: "\(name)Tests", platform: platform, product: .unitTests, @@ -62,7 +62,7 @@ extension Project { "aps-environment": "development", ] - let mainTarget = Target( + let mainTarget: Target = .target( name: name, platform: platform, product: .app, @@ -74,7 +74,7 @@ extension Project { dependencies: dependencies ) - let testTarget = Target( + let testTarget: Target = .target( name: "\(name)Tests", platform: platform, product: .unitTests, diff --git a/fixtures/app_with_test_plan/Project.swift b/fixtures/app_with_test_plan/Project.swift index 31986e95f89..c210c12c83b 100644 --- a/fixtures/app_with_test_plan/Project.swift +++ b/fixtures/app_with_test_plan/Project.swift @@ -4,18 +4,18 @@ let project = Project( name: "App", organizationName: "tuist.io", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: [.iPhone], product: .app, bundleId: "io.tuist.app", - deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone), + deploymentTargets: .iOS("13.0"), infoPlist: .default, sources: ["Targets/App/Sources/**"] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: .default, @@ -24,12 +24,12 @@ let project = Project( .target(name: "App"), ] ), - Target( + .target( name: "MacFramework", - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "io.tuist.MacFramework", - deploymentTarget: .macOS(targetVersion: "10.15"), + deploymentTargets: .macOS("10.15"), infoPlist: .default, sources: "Targets/MacFramework/Sources/**", settings: .settings( @@ -39,12 +39,12 @@ let project = Project( ] ) ), - Target( + .target( name: "MacFrameworkTests", - platform: .macOS, + destinations: [.mac], product: .unitTests, bundleId: "io.tuist.MacFrameworkTests", - deploymentTarget: .macOS(targetVersion: "10.15"), + deploymentTargets: .macOS("10.15"), infoPlist: .default, sources: "Targets/MacFramework/Tests/**", dependencies: [ @@ -59,9 +59,9 @@ let project = Project( ), ], schemes: [ - Scheme( + .scheme( name: "App", - buildAction: BuildAction(targets: ["App"]), + buildAction: .buildAction(targets: ["App"]), testAction: .testPlans([.relativeToManifest("All.xctestplan")]), runAction: .runAction( configuration: .debug, diff --git a/fixtures/app_with_tests/Project.swift b/fixtures/app_with_tests/Project.swift index cd6972937a6..f4818b685b2 100644 --- a/fixtures/app_with_tests/Project.swift +++ b/fixtures/app_with_tests/Project.swift @@ -4,18 +4,18 @@ let project = Project( name: "App", organizationName: "tuist.io", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: [.iPhone], product: .app, bundleId: "io.tuist.app", - deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone), + deploymentTargets: .iOS("13.0"), infoPlist: .default, sources: ["Targets/App/Sources/**"] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: .default, @@ -24,17 +24,17 @@ let project = Project( .target(name: "App"), ] ), - Target( + .target( name: "tvOSFramework", - platform: .tvOS, + destinations: [.appleTv], product: .framework, bundleId: "io.tuist.tvOSFramework", infoPlist: .default, sources: "Targets/tvOSFramework/Sources/**" ), - Target( + .target( name: "tvOSFrameworkTests", - platform: .tvOS, + destinations: [.appleTv], product: .unitTests, bundleId: "io.tuist.tvOSFrameworkTests", infoPlist: .default, @@ -43,12 +43,12 @@ let project = Project( .target(name: "tvOSFramework"), ] ), - Target( + .target( name: "MacFramework", - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "io.tuist.MacFramework", - deploymentTarget: .macOS(targetVersion: "10.15"), + deploymentTargets: .macOS("10.15"), infoPlist: .default, sources: "Targets/MacFramework/Sources/**", settings: .settings( @@ -58,12 +58,12 @@ let project = Project( ] ) ), - Target( + .target( name: "MacFrameworkTests", - platform: .macOS, + destinations: [.mac], product: .unitTests, bundleId: "io.tuist.MacFrameworkTests", - deploymentTarget: .macOS(targetVersion: "10.15"), + deploymentTargets: .macOS("10.15"), infoPlist: .default, sources: "Targets/MacFramework/Tests/**", dependencies: [ diff --git a/fixtures/command_line_tool_basic/Project.swift b/fixtures/command_line_tool_basic/Project.swift index 22b5c1d33b6..38485d883b5 100644 --- a/fixtures/command_line_tool_basic/Project.swift +++ b/fixtures/command_line_tool_basic/Project.swift @@ -3,13 +3,16 @@ import ProjectDescription let project = Project( name: "CommandLineTool", targets: [ - Target( + .target( name: "CommandLineTool", - platform: .macOS, + destinations: [.mac], product: .commandLineTool, bundleId: "com.example.commandlinetool", infoPlist: .default, - sources: ["main.swift"] + sources: ["main.swift"], + dependencies: [ + .external(name: "SystemPackage"), + ] ), ] ) diff --git a/fixtures/command_line_tool_basic/Tuist/Package.resolved b/fixtures/command_line_tool_basic/Tuist/Package.resolved new file mode 100644 index 00000000000..23bc833b3cb --- /dev/null +++ b/fixtures/command_line_tool_basic/Tuist/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "1d9037060b1f5bba948714e32c2f1311d4e0412b80099aa57b7e530425cb4cea", + "pins" : [ + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "f9266c85189c2751589a50ea5aec72799797e471", + "version" : "1.3.0" + } + } + ], + "version" : 3 +} diff --git a/fixtures/command_line_tool_basic/Tuist/Package.swift b/fixtures/command_line_tool_basic/Tuist/Package.swift new file mode 100644 index 00000000000..183c2a5487b --- /dev/null +++ b/fixtures/command_line_tool_basic/Tuist/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version:5.10 + +import PackageDescription + +let package = Package( + name: "workspace-dependencies", + dependencies: [ + .package( + url: "https://github.com/apple/swift-system.git", + from: "1.3.0" + ), + ] +) diff --git a/fixtures/command_line_tool_with_dynamic_framework/Project.swift b/fixtures/command_line_tool_with_dynamic_framework/Project.swift index 7671bef9ccb..137199a92e1 100644 --- a/fixtures/command_line_tool_with_dynamic_framework/Project.swift +++ b/fixtures/command_line_tool_with_dynamic_framework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "CommandLineTool", targets: [ - Target( + .target( name: "CommandLineTool", - platform: .macOS, + destinations: [.mac], product: .commandLineTool, bundleId: "com.example.commandlinetool", infoPlist: .default, @@ -14,9 +14,9 @@ let project = Project( .target(name: "DynamicFramework"), ] ), - Target( + .target( name: "DynamicFramework", - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "com.example.dynamicframework", infoPlist: .default, diff --git a/fixtures/command_line_tool_with_dynamic_library/Project.swift b/fixtures/command_line_tool_with_dynamic_library/Project.swift index 016925ebfd6..6a831bc49a4 100644 --- a/fixtures/command_line_tool_with_dynamic_library/Project.swift +++ b/fixtures/command_line_tool_with_dynamic_library/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "CommandLineTool", targets: [ - Target( + .target( name: "CommandLineTool", - platform: .macOS, + destinations: [.mac], product: .commandLineTool, bundleId: "com.example.commandlinetool", infoPlist: .default, @@ -14,9 +14,9 @@ let project = Project( .target(name: "DynamicLib"), ] ), - Target( + .target( name: "DynamicLib", - platform: .macOS, + destinations: [.mac], product: .dynamicLibrary, bundleId: "com.example.dynamiclib", infoPlist: .default, diff --git a/fixtures/command_line_tool_with_static_library/Project.swift b/fixtures/command_line_tool_with_static_library/Project.swift index f4d6b1cf8ef..046ed26911c 100644 --- a/fixtures/command_line_tool_with_static_library/Project.swift +++ b/fixtures/command_line_tool_with_static_library/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "CommandLineTool", targets: [ - Target( + .target( name: "CommandLineTool", - platform: .macOS, + destinations: [.mac], product: .commandLineTool, bundleId: "com.example.commandlinetool", infoPlist: .default, @@ -14,9 +14,9 @@ let project = Project( .target(name: "StaticLib"), ] ), - Target( + .target( name: "StaticLib", - platform: .macOS, + destinations: [.mac], product: .staticLibrary, bundleId: "com.example.staticlib", infoPlist: .default, diff --git a/fixtures/framework_with_environment_variables/Project.swift b/fixtures/framework_with_environment_variables/Project.swift index b0de99fb456..20e7775c999 100644 --- a/fixtures/framework_with_environment_variables/Project.swift +++ b/fixtures/framework_with_environment_variables/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework", targets: [ - Target( + .target( name: Environment.frameworkName.getString(default: "Framework"), - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "io.tuist.App", infoPlist: .default, diff --git a/fixtures/framework_with_environment_variables/README.md b/fixtures/framework_with_environment_variables/README.md new file mode 100644 index 00000000000..900f4677285 --- /dev/null +++ b/fixtures/framework_with_environment_variables/README.md @@ -0,0 +1,3 @@ +# Framework with environment variables + +A framework project that leverages environment variables to change the name of the framework. \ No newline at end of file diff --git a/fixtures/framework_with_macro_and_plugin_packages/.package.resolved b/fixtures/framework_with_macro_and_plugin_packages/.package.resolved new file mode 100644 index 00000000000..ca6162eecea --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/.package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "f23bfcf9a9ba15fad7000bb7c533c80d700daf4b8147c24567a6b7275c64803a", + "pins" : [ + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "a0f7b12c7be228592d924f29f654ebbd924ac9c5", + "version" : "0.55.1" + } + } + ], + "version" : 3 +} diff --git a/fixtures/framework_with_macro_and_plugin_packages/.swiftlint.yml b/fixtures/framework_with_macro_and_plugin_packages/.swiftlint.yml new file mode 100644 index 00000000000..505f7abc696 --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/.swiftlint.yml @@ -0,0 +1,5 @@ +opt_in_rules: + - missing_docs + +included: + - Sources diff --git a/fixtures/framework_with_macro_and_plugin_packages/Package.resolved b/fixtures/framework_with_macro_and_plugin_packages/Package.resolved new file mode 100644 index 00000000000..7d9f8d6e7d8 --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/Package.resolved @@ -0,0 +1,140 @@ +{ + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" + } + }, + { + "identity" : "structbuildermacro", + "kind" : "remoteSourceControl", + "location" : "https://github.com/alschmut/StructBuilderMacro", + "state" : { + "revision" : "e281b8e34cb5663e7ae060a51cf7669725c91a27", + "version" : "0.5.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "433a23118f739078644ebeb4009e23d307af694a", + "version" : "1.10.4" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "350e1e119babe8525f9bd155b76640a5de270184", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "d533cd18b0b456b106694a9899f917ee595f2666", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "8e8ca36c02abc7775a3ab81f9468c0df5fe22c2c", + "version" : "1.1.7" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "a0f7b12c7be228592d924f29f654ebbd924ac9c5", + "version" : "0.55.1" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "2ec6c3a15293efff6083966b38439a4004f25565", + "version" : "1.3.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" + } + } + ], + "version" : 2 +} diff --git a/fixtures/framework_with_macro_and_plugin_packages/Package.swift b/fixtures/framework_with_macro_and_plugin_packages/Package.swift new file mode 100644 index 00000000000..b09776c87c2 --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "Framework", + platforms: [ + .iOS(.v13), + .macOS(.v13), + ], + products: [ + .library( + name: "Framework", + targets: ["Framework"] + ), + ], + dependencies: [ + .package(url: "https://github.com/alschmut/StructBuilderMacro", from: "0.5.0"), + .package(url: "https://github.com/lukepistrol/SwiftLintPlugin", from: "0.2.2"), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.10.0"), + ], + targets: [ + .target( + name: "Framework", + dependencies: [ + .product(name: "Buildable", package: "StructBuilderMacro"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ], + plugins: [ + .plugin(name: "SwiftLint", package: "SwiftLintPlugin"), + ] + ), + ] +) diff --git a/fixtures/framework_with_macro_and_plugin_packages/Project.swift b/fixtures/framework_with_macro_and_plugin_packages/Project.swift new file mode 100644 index 00000000000..5d53245b46f --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/Project.swift @@ -0,0 +1,24 @@ +import ProjectDescription + +let project = Project( + name: "Framework", + packages: [ + .package(url: "https://github.com/lukepistrol/SwiftLintPlugin", from: "0.55.0"), + ], + targets: [ + .target( + name: "Framework", + destinations: [ + .mac, + ], + product: .framework, + bundleId: "io.tuist.Framework", + sources: ["Sources/**/*"], + dependencies: [ + .external(name: "Buildable"), + .external(name: "ComposableArchitecture"), + .package(product: "SwiftLint", type: .plugin), + ] + ), + ] +) diff --git a/fixtures/framework_with_macro_and_plugin_packages/README.md b/fixtures/framework_with_macro_and_plugin_packages/README.md new file mode 100644 index 00000000000..b7416d059f5 --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/README.md @@ -0,0 +1,6 @@ +# Framework with native Swift Macro and Plugin packages + +This project represents a project with a framework that integrates the following Swift Packages: + +- A [build tool plugin](https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md) using Xcode's native integration. +- A Swift Macro using Tuist's integration based on XcodeProj primitives. \ No newline at end of file diff --git a/fixtures/framework_with_macro_and_plugin_packages/Sources/Framework.swift b/fixtures/framework_with_macro_and_plugin_packages/Sources/Framework.swift new file mode 100644 index 00000000000..b587958bed9 --- /dev/null +++ b/fixtures/framework_with_macro_and_plugin_packages/Sources/Framework.swift @@ -0,0 +1,27 @@ +import Buildable +import ComposableArchitecture +import Foundation + +@Buildable +public struct Person { + let name: String + let age: Int + let hobby: String? + + var likesReading: Bool { + hobby == "Reading" + } + + static let minimumAge = 21 +} + +@Reducer +struct Feature { + struct State {} + + enum Action {} + + var body: some ReducerOf { + EmptyReducer() + } +} diff --git a/docs/Sources/tuist/main.swift b/fixtures/framework_with_macro_and_plugin_packages/Tuist/.gitkeep similarity index 100% rename from docs/Sources/tuist/main.swift rename to fixtures/framework_with_macro_and_plugin_packages/Tuist/.gitkeep diff --git a/fixtures/framework_with_native_swift_macro/Tuist/Package.resolved b/fixtures/framework_with_native_swift_macro/Package.resolved similarity index 100% rename from fixtures/framework_with_native_swift_macro/Tuist/Package.resolved rename to fixtures/framework_with_native_swift_macro/Package.resolved diff --git a/fixtures/framework_with_native_swift_macro/Tuist/Package.swift b/fixtures/framework_with_native_swift_macro/Package.swift similarity index 100% rename from fixtures/framework_with_native_swift_macro/Tuist/Package.swift rename to fixtures/framework_with_native_swift_macro/Package.swift diff --git a/fixtures/framework_with_native_swift_macro/Project.swift b/fixtures/framework_with_native_swift_macro/Project.swift index 8605240763d..fde74208d84 100644 --- a/fixtures/framework_with_native_swift_macro/Project.swift +++ b/fixtures/framework_with_native_swift_macro/Project.swift @@ -3,7 +3,7 @@ import ProjectDescription let project = Project( name: "FrameworkWithSwiftMacro", targets: [ - Target( + .target( name: "Framework", destinations: [.iPhone, .mac], product: .staticLibrary, diff --git a/fixtures/framework_with_native_swift_macro/Tuist/.gitkeep b/fixtures/framework_with_native_swift_macro/Tuist/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/framework_with_native_swift_macro/Tuist/Dependencies.swift b/fixtures/framework_with_native_swift_macro/Tuist/Dependencies.swift deleted file mode 100644 index d4564a8262b..00000000000 --- a/fixtures/framework_with_native_swift_macro/Tuist/Dependencies.swift +++ /dev/null @@ -1,8 +0,0 @@ -import ProjectDescription - -let dependencies = Dependencies( - swiftPackageManager: .init( - targetSettings: [:] - ), - platforms: [.macOS, .iOS] -) diff --git a/fixtures/framework_with_swift_macro/Project.swift b/fixtures/framework_with_swift_macro/Project.swift index a34684acdc4..48cdedcd94c 100644 --- a/fixtures/framework_with_swift_macro/Project.swift +++ b/fixtures/framework_with_swift_macro/Project.swift @@ -7,9 +7,9 @@ let project = Project( .remote(url: "https://github.com/pointfreeco/swift-composable-architecture", requirement: .exact("1.4.0")), ], targets: [ - Target( + .target( name: "Framework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.FrameworkWithSwiftMacro", sources: ["Sources/**/*"], diff --git a/fixtures/ios_app_large/Project.swift b/fixtures/ios_app_large/Project.swift index 5d58e3bbc56..225feb0e96b 100644 --- a/fixtures/ios_app_large/Project.swift +++ b/fixtures/ios_app_large/Project.swift @@ -1,9 +1,9 @@ import ProjectDescription func target(name: String) -> Target { - Target( + .target( name: name, - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.\(name)", infoPlist: .file(path: .relativeToManifest("Info.plist")), diff --git a/fixtures/ios_app_with_actions/App With Space/Project.swift b/fixtures/ios_app_with_actions/App With Space/Project.swift index 3a82817f263..9eadfe0f08c 100644 --- a/fixtures/ios_app_with_actions/App With Space/Project.swift +++ b/fixtures/ios_app_with_actions/App With Space/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "AppWithSpace", targets: [ - Target( + .target( name: "AppWithSpace", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app-with-space", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_actions/App/Project.swift b/fixtures/ios_app_with_actions/App/Project.swift index 665a45c7b82..e220a783b03 100644 --- a/fixtures/ios_app_with_actions/App/Project.swift +++ b/fixtures/ios_app_with_actions/App/Project.swift @@ -6,9 +6,9 @@ let project = Project( .remote(url: "https://github.com/realm/SwiftLint", requirement: .exact("0.52.4")), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_actions/README.md b/fixtures/ios_app_with_actions/README.md new file mode 100644 index 00000000000..3d6e0dd5cf9 --- /dev/null +++ b/fixtures/ios_app_with_actions/README.md @@ -0,0 +1,3 @@ +# iOS app with actions + +An iOS app with a target that has pre and post actions. \ No newline at end of file diff --git a/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..9221b9bb1a3 --- /dev/null +++ b/fixtures/ios_app_with_appclip/AppClip1Widgets/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/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/Contents.json b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/ios_app_with_appclip/AppClip1Widgets/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_appclip/AppClip1Widgets/Sources/Widget.swift b/fixtures/ios_app_with_appclip/AppClip1Widgets/Sources/Widget.swift new file mode 100644 index 00000000000..eac88f57911 --- /dev/null +++ b/fixtures/ios_app_with_appclip/AppClip1Widgets/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/ios_app_with_appclip/Project.swift b/fixtures/ios_app_with_appclip/Project.swift index dbca417e772..6d997ef2f74 100644 --- a/fixtures/ios_app_with_appclip/Project.swift +++ b/fixtures/ios_app_with_appclip/Project.swift @@ -4,9 +4,9 @@ let project = Project( name: "App", organizationName: "Tuist", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: .default, @@ -15,27 +15,27 @@ let project = Project( .target(name: "AppClip1"), ] ), - Target( + .target( name: "Framework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework", infoPlist: .default, sources: ["Framework/Sources/**"], dependencies: [] ), - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework", infoPlist: .default, sources: ["StaticFramework/Sources/**"], dependencies: [] ), - Target( + .target( name: "AppClip1", - platform: .iOS, + destinations: .iOS, product: .appClip, bundleId: "io.tuist.App.Clip", infoPlist: .default, @@ -44,11 +44,12 @@ let project = Project( dependencies: [ .target(name: "Framework"), .target(name: "StaticFramework"), + .target(name: "AppClip1Widgets"), ] ), - Target( + .target( name: "AppClip1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppClip1Tests", infoPlist: .default, @@ -58,9 +59,9 @@ let project = Project( .target(name: "StaticFramework"), ] ), - Target( + .target( name: "AppClip1UITests", - platform: .iOS, + destinations: .iOS, product: .uiTests, bundleId: "io.tuist.AppClip1UITests", infoPlist: .default, @@ -69,5 +70,22 @@ let project = Project( .target(name: "AppClip1"), ] ), + .target( + name: "AppClip1Widgets", + destinations: .iOS, + product: .appExtension, + bundleId: "io.tuist.App.Clip.Widgets", + infoPlist: .extendingDefault(with: [ + "CFBundleDisplayName": "$(PRODUCT_NAME)", + "NSExtension": [ + "NSExtensionPointIdentifier": "com.apple.widgetkit-extension", + ], + ]), + sources: "AppClip1Widgets/Sources/**", + resources: "AppClip1Widgets/Resources/**", + dependencies: [ + .target(name: "StaticFramework"), + ] + ), ] ) diff --git a/fixtures/ios_app_with_build_variables/App/Project.swift b/fixtures/ios_app_with_build_variables/App/Project.swift index 3acb146de1c..0c8be7f5bd4 100644 --- a/fixtures/ios_app_with_build_variables/App/Project.swift +++ b/fixtures/ios_app_with_build_variables/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_build_variables/README.md b/fixtures/ios_app_with_build_variables/README.md new file mode 100644 index 00000000000..c60d7ef797f --- /dev/null +++ b/fixtures/ios_app_with_build_variables/README.md @@ -0,0 +1,3 @@ +# iOS app with build variables + +An iOS app with a Xcode build variables defined in pre action. \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/CoreData/Unversioned.xcdatamodeld/Unversioned.xcdatamodel/contents b/fixtures/ios_app_with_coredata/CoreData/Unversioned.xcdatamodeld/Unversioned.xcdatamodel/contents index c02c3e68527..9c33e04760a 100644 --- a/fixtures/ios_app_with_coredata/CoreData/Unversioned.xcdatamodeld/Unversioned.xcdatamodel/contents +++ b/fixtures/ios_app_with_coredata/CoreData/Unversioned.xcdatamodeld/Unversioned.xcdatamodel/contents @@ -1,10 +1,7 @@ - - + + - - - \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/1.xcdatamodel/contents b/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/1.xcdatamodel/contents index 9e3da4939f4..836fb1cbc9a 100644 --- a/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/1.xcdatamodel/contents +++ b/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/1.xcdatamodel/contents @@ -1,10 +1,7 @@ - - + + - - - \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/2.xcdatamodel/contents b/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/2.xcdatamodel/contents index d21bb709bdd..21856a47e36 100644 --- a/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/2.xcdatamodel/contents +++ b/fixtures/ios_app_with_coredata/CoreData/Users.xcdatamodeld/2.xcdatamodel/contents @@ -1,10 +1,7 @@ - - + + - - - \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/1.xcdatamodel/contents b/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/1.xcdatamodel/contents index 59d8f899b83..971d919d12e 100644 --- a/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/1.xcdatamodel/contents +++ b/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/1.xcdatamodel/contents @@ -1,10 +1,7 @@ - - + + - - - \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.5.xcdatamodel/contents b/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.5.xcdatamodel/contents index d6255643ab1..971d919d12e 100644 --- a/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.5.xcdatamodel/contents +++ b/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.5.xcdatamodel/contents @@ -1,10 +1,7 @@ - - + + - - - \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.xcdatamodel/contents b/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.xcdatamodel/contents index b0480147863..3224638ff56 100644 --- a/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.xcdatamodel/contents +++ b/fixtures/ios_app_with_coredata/CoreData/UsersAutoDetect.xcdatamodeld/2.xcdatamodel/contents @@ -1,10 +1,7 @@ - - + + - - - \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/Project.swift b/fixtures/ios_app_with_coredata/Project.swift index 3adf3019601..c0ad2eafd15 100644 --- a/fixtures/ios_app_with_coredata/Project.swift +++ b/fixtures/ios_app_with_coredata/Project.swift @@ -3,18 +3,19 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", sources: ["Sources/**"], coreDataModels: [ - CoreDataModel("CoreData/Users.xcdatamodeld", currentVersion: "1"), - CoreDataModel("CoreData/UsersAutoDetect.xcdatamodeld"), - CoreDataModel("CoreData/Unversioned.xcdatamodeld"), + .coreDataModel("CoreData/Users.xcdatamodeld", currentVersion: "1"), + .coreDataModel("CoreData/UsersAutoDetect.xcdatamodeld"), + .coreDataModel("CoreData/Unversioned.xcdatamodeld"), ] ), - ] + ], + resourceSynthesizers: .default + [.coreData()] ) diff --git a/fixtures/ios_app_with_coredata/README.md b/fixtures/ios_app_with_coredata/README.md new file mode 100644 index 00000000000..f53aed3e279 --- /dev/null +++ b/fixtures/ios_app_with_coredata/README.md @@ -0,0 +1,3 @@ +# Application with CoreData models + +This example showcases a project that utilizes CoreData models and code generation for these models using resourceSynthesizer. \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata/Tuist/ResourceSynthesizers/CoreData.stencil b/fixtures/ios_app_with_coredata/Tuist/ResourceSynthesizers/CoreData.stencil new file mode 100644 index 00000000000..e8b5c21ac32 --- /dev/null +++ b/fixtures/ios_app_with_coredata/Tuist/ResourceSynthesizers/CoreData.stencil @@ -0,0 +1,10 @@ +import CoreData +import Foundation + +{% for model in models %} +{% for name, entity in model.entities %} +@objc({{ entity.className }}) +public final class {{ entity.className }}: NSManagedObject { +} +{% endfor %} +{% endfor %} \ No newline at end of file diff --git a/fixtures/ios_app_with_coredata_in_static_framework/App/Project.swift b/fixtures/ios_app_with_coredata_in_static_framework/App/Project.swift index c24063b9418..ee6bbd6d124 100644 --- a/fixtures/ios_app_with_coredata_in_static_framework/App/Project.swift +++ b/fixtures/ios_app_with_coredata_in_static_framework/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", diff --git a/fixtures/ios_app_with_coredata_in_static_framework/Framework/Project.swift b/fixtures/ios_app_with_coredata_in_static_framework/Framework/Project.swift index 5cf480665fd..45a839d8eab 100644 --- a/fixtures/ios_app_with_coredata_in_static_framework/Framework/Project.swift +++ b/fixtures/ios_app_with_coredata_in_static_framework/Framework/Project.swift @@ -3,15 +3,15 @@ import ProjectDescription let project = Project( name: "Framework", targets: [ - Target( + .target( name: "Framework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.framework", infoPlist: .default, sources: ["Sources/**"], coreDataModels: [ - CoreDataModel("CoreData/Users.xcdatamodeld"), + .coreDataModel("CoreData/Users.xcdatamodeld"), ] ), ] diff --git a/fixtures/ios_app_with_custom_configuration/Tuist/Config.swift b/fixtures/ios_app_with_custom_configuration/Tuist/Config.swift index 352d33d3d19..ff8e64fa9ee 100644 --- a/fixtures/ios_app_with_custom_configuration/Tuist/Config.swift +++ b/fixtures/ios_app_with_custom_configuration/Tuist/Config.swift @@ -1,8 +1,3 @@ import ProjectDescription -let config = Config( - cache: .cache( - profiles: [.profile(name: "Simulator", configuration: "debug", device: "iPhone 14 Pro")], - path: .relativeToRoot("TuistCloud") - ) -) +let config = Config() diff --git a/fixtures/ios_app_with_custom_configuration/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/fixtures/ios_app_with_custom_configuration/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 73ed202ebca..3966a95ab17 100644 --- a/fixtures/ios_app_with_custom_configuration/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/fixtures/ios_app_with_custom_configuration/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -62,12 +62,12 @@ extension Target { dependencies: [TargetDependency], resources: ResourceFileElements? = nil ) -> Target { - Target( + .target( name: name, - platform: .iOS, + destinations: [.iPhone], product: product, bundleId: "tuist.io.\(name)", - deploymentTarget: .deploymentTarget, + deploymentTargets: .iOS("11.0"), infoPlist: .default, sources: ["Sources/**"], resources: resources, @@ -78,9 +78,9 @@ extension Target { public static func test( name: String ) -> Target { - Target( + .target( name: "\(name)Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "tuist.io..\(name)Tests", infoPlist: .default, @@ -90,31 +90,20 @@ extension Target { } } -extension DeploymentTarget { - static var deploymentTarget: DeploymentTarget { - .iOS( - targetVersion: "11.0", - devices: [.iphone] - ) - } -} - extension Scheme { static func scheme( name: String, mainTargetName: String, testTargetName: String ) -> Scheme { - let main = TargetReference( - projectPath: nil, - target: mainTargetName + let main: TargetReference = .target( + mainTargetName ) - let test = TargetReference( - projectPath: nil, - target: testTargetName + let test: TargetReference = .target( + testTargetName ) - return Scheme( + return .scheme( name: name, shared: true, buildAction: .buildAction(targets: [ @@ -122,7 +111,7 @@ extension Scheme { ]), testAction: .targets( [ - TestableTarget(target: test), + .testableTarget(target: test), ], configuration: "debug" ), diff --git a/fixtures/ios_app_with_custom_development_region/Project.swift b/fixtures/ios_app_with_custom_development_region/Project.swift index 713eb7b167b..81907ed0e13 100644 --- a/fixtures/ios_app_with_custom_development_region/Project.swift +++ b/fixtures/ios_app_with_custom_development_region/Project.swift @@ -6,9 +6,9 @@ let project = Project( developmentRegion: "fr" ), targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: .default, diff --git a/fixtures/ios_app_with_custom_regions/Project.swift b/fixtures/ios_app_with_custom_regions/Project.swift index 323d7c71c8e..8147ab4025d 100644 --- a/fixtures/ios_app_with_custom_regions/Project.swift +++ b/fixtures/ios_app_with_custom_regions/Project.swift @@ -7,9 +7,9 @@ let project = Project( developmentRegion: "en-GB" ), targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: .default, diff --git a/fixtures/ios_app_with_custom_resource_parser_options/Project.swift b/fixtures/ios_app_with_custom_resource_parser_options/Project.swift index f42b38739be..1894ad82d5a 100644 --- a/fixtures/ios_app_with_custom_resource_parser_options/Project.swift +++ b/fixtures/ios_app_with_custom_resource_parser_options/Project.swift @@ -6,9 +6,9 @@ let project = Project( developmentRegion: "fr" ), targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: .default, diff --git a/fixtures/ios_app_with_custom_scheme/App/Project.swift b/fixtures/ios_app_with_custom_scheme/App/Project.swift index 637c650752e..12d3556014c 100644 --- a/fixtures/ios_app_with_custom_scheme/App/Project.swift +++ b/fixtures/ios_app_with_custom_scheme/App/Project.swift @@ -1,7 +1,7 @@ import ProjectDescription -let debugAction = ExecutionAction(scriptText: "echo Debug", target: "App") -let debugScheme = Scheme( +let debugAction: ExecutionAction = .executionAction(scriptText: "echo Debug", target: "App") +let debugScheme: Scheme = .scheme( name: "App-Debug", shared: true, buildAction: .buildAction( @@ -9,7 +9,14 @@ let debugScheme = Scheme( preActions: [debugAction], runPostActionsOnFailure: true ), - testAction: TestAction.targets(["AppTests"]), + testAction: .targets( + [ + .testableTarget( + target: "AppTests", + simulatedLocation: .rioDeJaneiro + ), + ] + ), runAction: .runAction( customLLDBInitFile: "../Scripts/lldb/_lldbinit", executable: "App", @@ -18,27 +25,34 @@ let debugScheme = Scheme( simulatedLocation: .johannesburg, enableGPUFrameCaptureMode: .metal ), - diagnosticsOptions: [.mainThreadChecker] + diagnosticsOptions: .options(mainThreadCheckerEnabled: true) ) ) -let releaseAction = ExecutionAction(scriptText: "echo Release", target: "App") -let releaseScheme = Scheme( +let releaseAction: ExecutionAction = .executionAction(scriptText: "echo Release", target: "App") +let releaseScheme: Scheme = .scheme( name: "App-Release", shared: true, buildAction: .buildAction(targets: ["App"], preActions: [releaseAction]), - testAction: TestAction.targets(["AppTests"]), + testAction: .targets( + [ + .testableTarget( + target: "AppTests", + simulatedLocation: .custom(gpxFile: "Resources/Grand Canyon.gpx") + ), + ] + ), runAction: .runAction( executable: "App", options: .options( simulatedLocation: .custom(gpxFile: "Resources/Grand Canyon.gpx"), enableGPUFrameCaptureMode: .disabled ), - diagnosticsOptions: [.mainThreadChecker] + diagnosticsOptions: .options(mainThreadCheckerEnabled: true) ) ) -let userScheme = Scheme( +let userScheme: Scheme = .scheme( name: "App-Local", shared: false, buildAction: .buildAction(targets: ["App"], preActions: [debugAction]), @@ -49,9 +63,9 @@ let userScheme = Scheme( let project = Project( name: "MainApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -61,9 +75,9 @@ let project = Project( .project(target: "Framework2", path: "../Frameworks/Framework2"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_custom_scheme/Frameworks/Framework1/Project.swift b/fixtures/ios_app_with_custom_scheme/Frameworks/Framework1/Project.swift index d8f394a4711..19801e38d68 100644 --- a/fixtures/ios_app_with_custom_scheme/Frameworks/Framework1/Project.swift +++ b/fixtures/ios_app_with_custom_scheme/Frameworks/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework1", infoPlist: "Config/Framework1-Info.plist", @@ -15,9 +15,9 @@ let project = Project( ] ), - Target( + .target( name: "Framework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Config/Framework1Tests-Info.plist", diff --git a/fixtures/ios_app_with_custom_scheme/Frameworks/Framework2/Project.swift b/fixtures/ios_app_with_custom_scheme/Frameworks/Framework2/Project.swift index 538f8d88e09..96a8727a82d 100644 --- a/fixtures/ios_app_with_custom_scheme/Frameworks/Framework2/Project.swift +++ b/fixtures/ios_app_with_custom_scheme/Frameworks/Framework2/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework2", targets: [ - Target( + .target( name: "Framework2", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework2", infoPlist: "Config/Framework2-Info.plist", @@ -13,9 +13,9 @@ let project = Project( dependencies: [] ), - Target( + .target( name: "Framework2Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework2Tests", infoPlist: "Config/Framework2Tests-Info.plist", diff --git a/fixtures/ios_app_with_custom_scheme/Workspace.swift b/fixtures/ios_app_with_custom_scheme/Workspace.swift index 5a18ee26a88..fb81b4fc9bd 100644 --- a/fixtures/ios_app_with_custom_scheme/Workspace.swift +++ b/fixtures/ios_app_with_custom_scheme/Workspace.swift @@ -1,6 +1,6 @@ import ProjectDescription -let customAppScheme = Scheme( +let customAppScheme: Scheme = .scheme( name: "Workspace-App", shared: true, buildAction: .buildAction( @@ -9,14 +9,14 @@ let customAppScheme = Scheme( .project(path: "Frameworks/Framework1", target: "Framework1"), ], preActions: [ - ExecutionAction( + .executionAction( scriptText: "echo \"pre-action in $SHELL\"", target: .project(path: "App", target: "App"), shellPath: "/bin/zsh" ), ], postActions: [ - ExecutionAction( + .executionAction( scriptText: "echo \"post-action in $SHELL\"", target: .project(path: "Frameworks/Framework1", target: "Framework1"), shellPath: "/bin/zsh" @@ -25,12 +25,12 @@ let customAppScheme = Scheme( ), testAction: TestAction.targets( [ - TestableTarget(target: .project(path: "App", target: "AppTests")), - TestableTarget(target: .project( + .testableTarget(target: .project(path: "App", target: "AppTests")), + .testableTarget(target: .project( path: "Frameworks/Framework1", target: "Framework1Tests" )), - TestableTarget(target: .project( + .testableTarget(target: .project( path: "Frameworks/Framework2", target: "Framework2Tests" )), @@ -39,7 +39,7 @@ let customAppScheme = Scheme( ), runAction: .runAction( executable: .project(path: "App", target: "App"), - arguments: Arguments(environment: ["path": "$(SRCROOT)"], launchArguments: []), + arguments: .arguments(environmentVariables: ["path": "$(SRCROOT)"], launchArguments: []), options: .options( storeKitConfigurationPath: "App/Config/ProjectStoreKitConfig.storekit" ), @@ -48,7 +48,7 @@ let customAppScheme = Scheme( archiveAction: .archiveAction(configuration: "Debug", customArchiveName: "Something2") ) -let customAppSchemeWithTestPlans = Scheme( +let customAppSchemeWithTestPlans: Scheme = .scheme( name: "Workspace-App-With-TestPlans", shared: true, buildAction: .buildAction( @@ -66,7 +66,7 @@ let customAppSchemeWithTestPlans = Scheme( archiveAction: .archiveAction(configuration: "Debug", customArchiveName: "Something2") ) -let customFrameworkScheme = Scheme( +let customFrameworkScheme: Scheme = .scheme( name: "Workspace-Framework", shared: true, buildAction: .buildAction( @@ -74,7 +74,7 @@ let customFrameworkScheme = Scheme( preActions: [] ), testAction: TestAction - .targets([TestableTarget(target: .project( + .targets([.testableTarget(target: .project( path: "Frameworks/Framework1", target: "Framework1Tests" ))]), diff --git a/fixtures/ios_app_with_custom_workspace/App/Project.swift b/fixtures/ios_app_with_custom_workspace/App/Project.swift index 7d29b135873..091a7a764f3 100644 --- a/fixtures/ios_app_with_custom_workspace/App/Project.swift +++ b/fixtures/ios_app_with_custom_workspace/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MainApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -15,9 +15,9 @@ let project = Project( .project(target: "Framework2", path: "../Frameworks/Framework2"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Project.swift b/fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Project.swift index d8f394a4711..19801e38d68 100644 --- a/fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Project.swift +++ b/fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework1", infoPlist: "Config/Framework1-Info.plist", @@ -15,9 +15,9 @@ let project = Project( ] ), - Target( + .target( name: "Framework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Config/Framework1Tests-Info.plist", diff --git a/fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Project.swift b/fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Project.swift index 538f8d88e09..96a8727a82d 100644 --- a/fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Project.swift +++ b/fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework2", targets: [ - Target( + .target( name: "Framework2", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework2", infoPlist: "Config/Framework2-Info.plist", @@ -13,9 +13,9 @@ let project = Project( dependencies: [] ), - Target( + .target( name: "Framework2Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework2Tests", infoPlist: "Config/Framework2Tests-Info.plist", diff --git a/fixtures/ios_app_with_custom_workspace/README.md b/fixtures/ios_app_with_custom_workspace/README.md new file mode 100644 index 00000000000..fd0e30983ca --- /dev/null +++ b/fixtures/ios_app_with_custom_workspace/README.md @@ -0,0 +1,15 @@ +# iOS app with a custom workspace + +iOS with a few projects and a `Workspace.swift` manifest file. + +The workspace manifest defines: + +- glob patterns to list projects +- glob patterns to include documentation files +- folder reference to directory with html files + +The App's project manifest leverages `additionalFiles` that: + +- defines glob patterns to include documentation files +- includes a Swift `Danger.swift` file that shouldn't get included in any build phase +- defines folder references to a directory with json files \ No newline at end of file diff --git a/fixtures/ios_app_with_device_only_xcframework/Lib/Sources/Lib.swift b/fixtures/ios_app_with_device_only_xcframework/Lib/Sources/Lib.swift deleted file mode 100644 index 3da90b9e597..00000000000 --- a/fixtures/ios_app_with_device_only_xcframework/Lib/Sources/Lib.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -public class Lib { - public init() {} - - public func run() { - print("Hello world!") - } -} diff --git a/fixtures/ios_app_with_device_only_xcframework/MyTarget/Sources/MyClass.swift b/fixtures/ios_app_with_device_only_xcframework/MyTarget/Sources/MyClass.swift deleted file mode 100644 index 5ca094c0d45..00000000000 --- a/fixtures/ios_app_with_device_only_xcframework/MyTarget/Sources/MyClass.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import Lib - -public class MyClass { - public init() {} - - public func hello() { - Lib().run() - } -} diff --git a/fixtures/ios_app_with_device_only_xcframework/Project.swift b/fixtures/ios_app_with_device_only_xcframework/Project.swift deleted file mode 100644 index ac5394991ee..00000000000 --- a/fixtures/ios_app_with_device_only_xcframework/Project.swift +++ /dev/null @@ -1,30 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "App", - targets: [ - Target( - name: "MyTarget", - platform: .iOS, - product: .framework, - bundleId: "io.tuist.MyTarget", - infoPlist: .default, - sources: [ - "MyTarget/Sources/**", - ], - dependencies: [ - .target(name: "Lib"), - ] - ), - Target( - name: "Lib", - platform: .iOS, - product: .framework, - bundleId: "io.tuist.Lib", - infoPlist: .default, - sources: [ - "Lib/Sources/**", - ] - ), - ] -) diff --git a/fixtures/ios_app_with_device_only_xcframework/Tuist/Config.swift b/fixtures/ios_app_with_device_only_xcframework/Tuist/Config.swift deleted file mode 100644 index 7d1fed08e4d..00000000000 --- a/fixtures/ios_app_with_device_only_xcframework/Tuist/Config.swift +++ /dev/null @@ -1,10 +0,0 @@ -import ProjectDescription - -let config = Config( - cache: .cache(profiles: [ - .profile( - name: "DeviceOnly", - configuration: "Debug" - ), - ]) -) diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/.gitignore b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/.gitignore new file mode 100644 index 00000000000..24b244f9c45 --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/.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/.build \ No newline at end of file diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Sources/App.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Sources/App.swift new file mode 100644 index 00000000000..356f48ca30e --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Sources/App.swift @@ -0,0 +1,13 @@ +import DynamicFrameworkA +import SwiftUI + +@main +struct MyApp: App { + private var dynamicFramework = DynamicFrameworkA() + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Sources/ContentView.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Sources/ContentView.swift new file mode 100644 index 00000000000..70fa229464f --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Sources/ContentView.swift @@ -0,0 +1,21 @@ +import DynamicFrameworkA +import SwiftUI + +public struct ContentView: View { + public init() {} + + public var body: some View { + let choice = Thing().choice + if choice.is(\.bluePill) { + Text("Blue") + } else { + Text("Red") + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Tests/AppTests.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/App/Tests/AppTests.swift new file mode 100644 index 00000000000..70c453ba3f3 --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/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_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkA/DynamicFrameworkA.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkA/DynamicFrameworkA.swift new file mode 100644 index 00000000000..b4688f7f3be --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkA/DynamicFrameworkA.swift @@ -0,0 +1,17 @@ +import GoogleMobileAds +import UIKit + +public class DynamicFrameworkA: NSObject { + let googleAds = GADExtras() + override public init() { + super.init() + } +} + +extension DynamicFrameworkA: GADAdSizeDelegate { + public func adView(_ bannerView: GADBannerView, willChangeAdSizeTo size: GADAdSize) { + var frame = bannerView.frame + frame.size.height = CGSizeFromGADAdSize(size).height + print("new frame \(frame)") + } +} diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkA/Thing.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkA/Thing.swift new file mode 100644 index 00000000000..7c2ba4a4fdd --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkA/Thing.swift @@ -0,0 +1,9 @@ +import DynamicFrameworkB + +public struct Thing { + public var choice: Choice + + public init() { + choice = .redPill + } +} diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkB/Choice.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkB/Choice.swift new file mode 100644 index 00000000000..e02411c278e --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/DynamicFrameworkB/Choice.swift @@ -0,0 +1,7 @@ +import CasePaths + +@CasePathable +public enum Choice { + case bluePill + case redPill +} diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Project.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Project.swift new file mode 100644 index 00000000000..d3096ee8388 --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Project.swift @@ -0,0 +1,57 @@ +import ProjectDescription + +let project = Project( + name: "App", + targets: [ + .target( + name: "App", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.App", + infoPlist: .extendingDefault( + with: [ + "UILaunchScreen": [ + "UIColorName": "", + "UIImageName": "", + ], + ] + ), + sources: ["App/Sources/**"], + resources: [], + dependencies: [ + .target(name: "DynamicFrameworkA"), + ] + ), + .target( + name: "AppTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.AppTests", + infoPlist: .default, + sources: ["App/Tests/**"], + resources: [], + dependencies: [.target(name: "App")] + ), + .target( + name: "DynamicFrameworkA", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.DynamicFrameworkA", + sources: ["DynamicFrameworkA/**"], + dependencies: [ + .external(name: "GoogleMobileAds"), + .target(name: "DynamicFrameworkB"), + ] + ), + .target( + name: "DynamicFrameworkB", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.DynamicFrameworkB", + sources: ["DynamicFrameworkB/**"], + dependencies: [ + .external(name: "CasePaths"), + ] + ), + ] +) diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Tuist/Package.resolved b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Tuist/Package.resolved new file mode 100644 index 00000000000..66cffdd4bd9 --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Tuist/Package.resolved @@ -0,0 +1,50 @@ +{ + "pins" : [ + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "b9ad2661b6e8fb411fef6a441c9955c3413afac0", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-package-manager-google-mobile-ads", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", + "state" : { + "revision" : "5ff977255c2ba5844e7e9da779f1f2a5a00e0028", + "version" : "11.2.0" + } + }, + { + "identity" : "swift-package-manager-google-user-messaging-platform", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git", + "state" : { + "revision" : "459949af1286c12487a6efd76f00f3e9d951c9ba", + "version" : "2.5.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" + } + } + ], + "version" : 2 +} diff --git a/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Tuist/Package.swift b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Tuist/Package.swift new file mode 100644 index 00000000000..763b4827456 --- /dev/null +++ b/fixtures/ios_app_with_dynamic_frameworks_linking_static_frameworks/Tuist/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + // Customize the product types for specific package product + // Default is .staticFramework + // productTypes: ["Alamofire": .framework,] + productTypes: [:] + ) +#endif + +let package = Package( + name: "App", + dependencies: [ + .package(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", exact: "11.2.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.4.2"), + ] +) diff --git a/fixtures/ios_app_with_env_suppressed/App With Space/Project.swift b/fixtures/ios_app_with_env_suppressed/App With Space/Project.swift index d1321dea216..d957d001f7e 100644 --- a/fixtures/ios_app_with_env_suppressed/App With Space/Project.swift +++ b/fixtures/ios_app_with_env_suppressed/App With Space/Project.swift @@ -4,9 +4,9 @@ let project = Project( name: "AppWithSpace", options: .options(disableShowEnvironmentVarsInScriptPhases: true), targets: [ - Target( + .target( name: "AppWithSpace", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app-with-space", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_env_suppressed/App/Project.swift b/fixtures/ios_app_with_env_suppressed/App/Project.swift index 03ffceaa072..ee4149e2f86 100644 --- a/fixtures/ios_app_with_env_suppressed/App/Project.swift +++ b/fixtures/ios_app_with_env_suppressed/App/Project.swift @@ -4,9 +4,9 @@ let project = Project( name: "App", options: .options(disableShowEnvironmentVarsInScriptPhases: true), targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..13613e3ee1a --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/Contents.json b/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/App/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_extension_and_tests/App/Sources/MainApp.swift b/fixtures/ios_app_with_extension_and_tests/App/Sources/MainApp.swift new file mode 100644 index 00000000000..50d60d1239a --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/App/Sources/MainApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MainApp: App { + var body: some Scene { + WindowGroup { + Text("App extension") + } + } +} diff --git a/fixtures/ios_app_with_extension_and_tests/AppExtension/ExtensionViewController.swift b/fixtures/ios_app_with_extension_and_tests/AppExtension/ExtensionViewController.swift new file mode 100644 index 00000000000..6b572f32cae --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/AppExtension/ExtensionViewController.swift @@ -0,0 +1,3 @@ +import UIKit + +final class ExtensionViewController: UIViewController {} diff --git a/fixtures/ios_app_with_extension_and_tests/AppExtensionTests/ExtensionTests.swift b/fixtures/ios_app_with_extension_and_tests/AppExtensionTests/ExtensionTests.swift new file mode 100644 index 00000000000..b4959a7e4d2 --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/AppExtensionTests/ExtensionTests.swift @@ -0,0 +1,9 @@ +import AppExtension +import Foundation +import XCTest + +final class ExtensionTests: XCTestCase { + func test_extension_is_working_fine() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/fixtures/ios_app_with_extension_and_tests/Project.swift b/fixtures/ios_app_with_extension_and_tests/Project.swift new file mode 100644 index 00000000000..8c62ca69d19 --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/Project.swift @@ -0,0 +1,47 @@ +import ProjectDescription + +let project = Project( + name: "App", + targets: [ + .target( + name: "App", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.App", + infoPlist: .extendingDefault( + with: [ + "UILaunchScreen": [ + "UIColorName": "", + "UIImageName": "", + ], + ] + ), + sources: ["App/Sources/**"], + resources: ["App/Resources/**"], + dependencies: [.target(name: "AppExtension")] + ), + .target( + name: "AppExtension", + destinations: .iOS, + product: .appExtension, + bundleId: "io.tuist.App.AppExtension", + infoPlist: .extendingDefault(with: [ + "CFBundleDisplayName": "$(PRODUCT_NAME)", + "NSExtension": [ + "NSExtensionPrincipalClass": "ExtensionViewController", + ], + ]), + sources: ["AppExtension/**"], + dependencies: [] + ), + .target( + name: "AppExtensionTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.App.AppExtensionTests", + infoPlist: .default, + sources: ["AppExtensionTests/**"], + dependencies: [.target(name: "AppExtension")] + ), + ] +) diff --git a/fixtures/ios_app_with_extension_and_tests/README.md b/fixtures/ios_app_with_extension_and_tests/README.md new file mode 100644 index 00000000000..83ef21fc30d --- /dev/null +++ b/fixtures/ios_app_with_extension_and_tests/README.md @@ -0,0 +1,3 @@ +# iOS app with extensions having unit test target + +Sample iOS application with extension target and a unit test target. diff --git a/fixtures/ios_app_with_extensions/Bundle/dummy.jpg b/fixtures/ios_app_with_extensions/Bundle/dummy.jpg new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/ios_app_with_extensions/Project.swift b/fixtures/ios_app_with_extensions/Project.swift index e28b6617fd5..a1d36014ffe 100644 --- a/fixtures/ios_app_with_extensions/Project.swift +++ b/fixtures/ios_app_with_extensions/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( // We need a separate app to test out Message Extensions // as having both stickers pack and message extensions in one app // doesn't seem to be supported. - Target( + .target( name: "AppWithMessagesExtension", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App2", infoPlist: "Info.plist", @@ -32,9 +32,9 @@ let project = Project( .target(name: "NotificationServiceExtension"), ] ), - Target( + .target( name: "StickersPackExtension", - platform: .iOS, + destinations: .iOS, product: .stickerPackExtension, bundleId: "io.tuist.App.StickersPackExtension", infoPlist: .extendingDefault(with: [ @@ -49,9 +49,9 @@ let project = Project( dependencies: [ ] ), - Target( + .target( name: "NotificationServiceExtension", - platform: .iOS, + destinations: .iOS, product: .appExtension, bundleId: "io.tuist.App.NotificationServiceExtension", infoPlist: .extendingDefault(with: [ @@ -65,9 +65,9 @@ let project = Project( dependencies: [ ] ), - Target( + .target( name: "MessageExtension", - platform: .iOS, + destinations: .iOS, product: .messagesExtension, bundleId: "io.tuist.App2.MessageExtension", infoPlist: .extendingDefault(with: [ @@ -82,9 +82,9 @@ let project = Project( dependencies: [ ] ), - Target( + .target( name: "WidgetExtension", - platform: .iOS, + destinations: .iOS, product: .appExtension, bundleId: "io.tuist.App.WidgetExtension", infoPlist: .extendingDefault(with: [ @@ -96,20 +96,21 @@ let project = Project( sources: "WidgetExtension/Sources/**", resources: "WidgetExtension/Resources/**", dependencies: [ + .target(name: "Bundle"), .target(name: "StaticFramework"), ] ), - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.App.StaticFramework", infoPlist: .default, sources: "StaticFramework/Sources/**" ), - Target( + .target( name: "AppIntentExtension", - platform: .iOS, + destinations: .iOS, product: .extensionKitExtension, bundleId: "io.tuist.App.AppIntentExtension", infoPlist: .extendingDefault(with: [ @@ -119,5 +120,12 @@ let project = Project( ]), sources: "AppIntentExtension/Sources/**" ), + .target( + name: "Bundle", + destinations: .iOS, + product: .bundle, + bundleId: "io.tuist.App.Bundle", + resources: "Bundle/**" + ), ] ) diff --git a/fixtures/ios_app_with_extensions/README.md b/fixtures/ios_app_with_extensions/README.md new file mode 100644 index 00000000000..ec28324ec44 --- /dev/null +++ b/fixtures/ios_app_with_extensions/README.md @@ -0,0 +1,3 @@ +# iOS app with extensions + +Sample iOS application with extension targets. \ No newline at end of file diff --git a/fixtures/ios_app_with_framework_and_disabled_resources/App/Project.swift b/fixtures/ios_app_with_framework_and_disabled_resources/App/Project.swift index 0d38967c7f0..454adb7611e 100644 --- a/fixtures/ios_app_with_framework_and_disabled_resources/App/Project.swift +++ b/fixtures/ios_app_with_framework_and_disabled_resources/App/Project.swift @@ -7,9 +7,9 @@ let project = Project( disableSynthesizedResourceAccessors: true ), targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", diff --git a/fixtures/ios_app_with_framework_and_disabled_resources/Framework1/Project.swift b/fixtures/ios_app_with_framework_and_disabled_resources/Framework1/Project.swift index 5f378dc0d28..87c7905d085 100644 --- a/fixtures/ios_app_with_framework_and_disabled_resources/Framework1/Project.swift +++ b/fixtures/ios_app_with_framework_and_disabled_resources/Framework1/Project.swift @@ -15,14 +15,14 @@ let project = Project( disableSynthesizedResourceAccessors: true ), targets: [ - Target( + .target( name: "Framework1", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework1", infoPlist: "Config/Framework1-Info.plist", sources: "Sources/**", - resources: ResourceFileElements(resources: resources) + resources: .resources(resources) ), ] ) diff --git a/fixtures/ios_app_with_framework_and_disabled_resources/StaticFramework/Project.swift b/fixtures/ios_app_with_framework_and_disabled_resources/StaticFramework/Project.swift index 15a0bf5a145..4c59502703d 100644 --- a/fixtures/ios_app_with_framework_and_disabled_resources/StaticFramework/Project.swift +++ b/fixtures/ios_app_with_framework_and_disabled_resources/StaticFramework/Project.swift @@ -7,18 +7,18 @@ let project = Project( disableSynthesizedResourceAccessors: true ), targets: [ - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework", infoPlist: "Config/StaticFramework-Info.plist", sources: "Sources/**", dependencies: [] ), - Target( + .target( name: "StaticFrameworkResources", - platform: .iOS, + destinations: .iOS, product: .bundle, bundleId: "io.tuist.StaticFrameworkResources", infoPlist: .default, diff --git a/fixtures/ios_app_with_framework_and_resources/App/Project.swift b/fixtures/ios_app_with_framework_and_resources/App/Project.swift index c96aa0d673c..d3a02079100 100644 --- a/fixtures/ios_app_with_framework_and_resources/App/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/App/Project.swift @@ -3,27 +3,43 @@ import ProjectDescription let project = Project( name: "MainApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", sources: "Sources/**", - resources: [ - "Resources/**/*.png", - "Resources/*.xcassets", - "Resources/**/*.txt", - "Resources/**/*.strings", - "Resources/**/*.stringsdict", - "Resources/**/*.plist", - "Resources/**/*.otf", - "Resources/resource_without_extension", - .glob(pattern: "ODRResources/*.png", tags: ["tag1"]), - .glob(pattern: "ODRResources/odr_text.txt", tags: ["tag2"]), - .folderReference(path: "Examples"), - .folderReference(path: "ODRExamples", tags: ["tag1", "tag2"]), - ], + resources: .resources( + [ + "Resources/**/*.png", + "Resources/*.xcassets", + "Resources/**/*.txt", + "Resources/**/*.strings", + "Resources/**/*.stringsdict", + "Resources/**/*.plist", + "Resources/**/*.otf", + "Resources/resource_without_extension", + .glob(pattern: "ODRResources/*.png", tags: ["tag1"]), + .glob(pattern: "ODRResources/odr_text.txt", tags: ["tag2"]), + .folderReference(path: "Examples"), + .folderReference(path: "ODRExamples", tags: ["tag1", "tag2"]), + ], + privacyManifest: .privacyManifest( + tracking: false, + trackingDomains: [], + collectedDataTypes: [ + ], + accessedApiTypes: [ + [ + "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults", + "NSPrivacyAccessedAPITypeReasons": [ + "CA92.1", + ], + ], + ] + ) + ), dependencies: [ .project(target: "Framework1", path: "../Framework1"), .project(target: "StaticFramework", path: "../StaticFramework"), @@ -34,9 +50,9 @@ let project = Project( .project(target: "StaticFramework5", path: "../StaticFramework5"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_framework_and_resources/Framework1/Project.swift b/fixtures/ios_app_with_framework_and_resources/Framework1/Project.swift index 3b6d8d43aa3..ac3036f1ac1 100644 --- a/fixtures/ios_app_with_framework_and_resources/Framework1/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/Framework1/Project.swift @@ -12,14 +12,14 @@ let resources: [ResourceFileElement] = [ let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework1", infoPlist: "Config/Framework1-Info.plist", sources: "Sources/**", - resources: ResourceFileElements(resources: resources) + resources: .resources(resources) ), ] ) diff --git a/fixtures/ios_app_with_framework_and_resources/README.md b/fixtures/ios_app_with_framework_and_resources/README.md new file mode 100644 index 00000000000..f97e44547f6 --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/README.md @@ -0,0 +1,21 @@ +# iOS app with a framework and resources + +A workspace with an application that includes resources. + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - Framework1: + - Framework1 (dynamic iOS framework) + - StaticFramework + - StaticFramework (static iOS framework) + - StaticFrameworkResources (iOS bundle) +``` + +Dependencies: + +- App -> Framework1 +- App -> StaticFramework +- App -> StaticFrameworkResources \ No newline at end of file diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework/Project.swift b/fixtures/ios_app_with_framework_and_resources/StaticFramework/Project.swift index b9ac0707766..9e993e9bf2e 100644 --- a/fixtures/ios_app_with_framework_and_resources/StaticFramework/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework/Project.swift @@ -3,18 +3,18 @@ import ProjectDescription let project = Project( name: "StaticFramework", targets: [ - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework", infoPlist: "Config/StaticFramework-Info.plist", sources: "Sources/**", dependencies: [] ), - Target( + .target( name: "StaticFrameworkResources", - platform: .iOS, + destinations: .iOS, product: .bundle, bundleId: "io.tuist.StaticFrameworkResources", infoPlist: .default, diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework2/Project.swift b/fixtures/ios_app_with_framework_and_resources/StaticFramework2/Project.swift index c81a7f5b60f..8211bce5c33 100644 --- a/fixtures/ios_app_with_framework_and_resources/StaticFramework2/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework2/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFramework2", targets: [ - Target( + .target( name: "StaticFramework2", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework2", infoPlist: .default, @@ -14,9 +14,9 @@ let project = Project( .target(name: "StaticFramework2Resources"), ] ), - Target( + .target( name: "StaticFramework2Resources", - platform: .iOS, + destinations: .iOS, product: .bundle, bundleId: "io.tuist.StaticFramework2Resources", infoPlist: .default, diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework3/Project.swift b/fixtures/ios_app_with_framework_and_resources/StaticFramework3/Project.swift index f1dd5309b1e..5984830f017 100644 --- a/fixtures/ios_app_with_framework_and_resources/StaticFramework3/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework3/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFramework3", targets: [ - Target( + .target( name: "StaticFramework3", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework3", infoPlist: .default, diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework4/Project.swift b/fixtures/ios_app_with_framework_and_resources/StaticFramework4/Project.swift index 4df1f43a79d..473a2bc879f 100644 --- a/fixtures/ios_app_with_framework_and_resources/StaticFramework4/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework4/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFramework4", targets: [ - Target( + .target( name: "StaticFramework4", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework4", infoPlist: .default, diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Project.swift b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Project.swift index 0bd3dc778fc..605f1b0f8bd 100644 --- a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Project.swift +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Project.swift @@ -3,15 +3,16 @@ import ProjectDescription let project = Project( name: "StaticFramework5", targets: [ - Target( + .target( name: "StaticFramework5", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework5", infoPlist: .default, resources: "Resources/**", dependencies: [ - ] + ], + settings: .settings(base: ["SWIFT_STRICT_CONCURRENCY": .string("complete")], defaultSettings: .recommended()) ), ] ) diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/Property List.plist b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/Property List.plist new file mode 100644 index 00000000000..44d117057aa --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/Property List.plist @@ -0,0 +1,8 @@ + + + + + Key + Value + + diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Color.colorset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Color.colorset/Contents.json new file mode 100644 index 00000000000..22c4bb0a8d9 --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Contents.json new file mode 100644 index 00000000000..78233b34938 --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Contents.json @@ -0,0 +1,38 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "textures" : [ + { + "cube-face" : "x-", + "filename" : "Universal -X.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "y-", + "filename" : "Universal -Y.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "z-", + "filename" : "Universal -Z.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "x+", + "filename" : "Universal +X.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "y+", + "filename" : "Universal +Y.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "z+", + "filename" : "Universal +Z.mipmapset", + "idiom" : "universal" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +X.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +X.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +X.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +Y.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +Y.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +Y.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +Z.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +Z.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal +Z.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -X.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -X.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -X.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -Y.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -Y.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -Y.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -Z.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -Z.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Cube Texture.cubetextureset/Universal -Z.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Data.dataset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Data.dataset/Contents.json new file mode 100644 index 00000000000..377a42c7902 --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Data.dataset/Contents.json @@ -0,0 +1,11 @@ +{ + "data" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Sprites.spriteatlas/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Sprites.spriteatlas/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Sprites.spriteatlas/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Sprites.spriteatlas/Sprite.imageset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Sprites.spriteatlas/Sprite.imageset/Contents.json new file mode 100644 index 00000000000..a19a5492203 --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Sprites.spriteatlas/Sprite.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Symbol.symbolset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Symbol.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Symbol.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Texture.textureset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Texture.textureset/Contents.json new file mode 100644 index 00000000000..cb0e052adba --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Texture.textureset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "textures" : [ + { + "filename" : "Universal.mipmapset", + "idiom" : "universal" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Texture.textureset/Universal.mipmapset/Contents.json b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Texture.textureset/Universal.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/assets.xcassets/Texture.textureset/Universal.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/en.lproj/Localizable.strings b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/en.lproj/Localizable.strings new file mode 100644 index 00000000000..7ecbdc9e767 --- /dev/null +++ b/fixtures/ios_app_with_framework_and_resources/StaticFramework5/Resources/en.lproj/Localizable.strings @@ -0,0 +1 @@ +"Test String" = "Test String"; diff --git a/fixtures/ios_app_with_framework_linking_static_framework/App/Project.swift b/fixtures/ios_app_with_framework_linking_static_framework/App/Project.swift index 29c7275f25b..37ace4784ed 100644 --- a/fixtures/ios_app_with_framework_linking_static_framework/App/Project.swift +++ b/fixtures/ios_app_with_framework_linking_static_framework/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MainApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -14,9 +14,9 @@ let project = Project( .project(target: "Framework1", path: "../Framework1"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_framework_linking_static_framework/Framework1/Project.swift b/fixtures/ios_app_with_framework_linking_static_framework/Framework1/Project.swift index d07a3345e86..becb75134d4 100644 --- a/fixtures/ios_app_with_framework_linking_static_framework/Framework1/Project.swift +++ b/fixtures/ios_app_with_framework_linking_static_framework/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework1", infoPlist: "Config/Framework1-Info.plist", @@ -17,9 +17,9 @@ let project = Project( ] ), - Target( + .target( name: "Framework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Config/Framework1Tests-Info.plist", diff --git a/fixtures/ios_app_with_framework_linking_static_framework/Framework2/Project.swift b/fixtures/ios_app_with_framework_linking_static_framework/Framework2/Project.swift index ed7dac97b2f..bec8853b09d 100644 --- a/fixtures/ios_app_with_framework_linking_static_framework/Framework2/Project.swift +++ b/fixtures/ios_app_with_framework_linking_static_framework/Framework2/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework2", targets: [ - Target( + .target( name: "Framework2", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.Framework2", infoPlist: "Config/Framework2-Info.plist", @@ -13,9 +13,9 @@ let project = Project( dependencies: [] ), - Target( + .target( name: "Framework2Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework2Tests", infoPlist: "Config/Framework2Tests-Info.plist", diff --git a/fixtures/ios_app_with_framework_linking_static_framework/Framework3/Project.swift b/fixtures/ios_app_with_framework_linking_static_framework/Framework3/Project.swift index b7165340250..8a403fded82 100644 --- a/fixtures/ios_app_with_framework_linking_static_framework/Framework3/Project.swift +++ b/fixtures/ios_app_with_framework_linking_static_framework/Framework3/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework3", targets: [ - Target( + .target( name: "Framework3", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.Framework3", infoPlist: "Config/Framework3-Info.plist", @@ -15,9 +15,9 @@ let project = Project( ] ), - Target( + .target( name: "Framework3Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework3Tests", infoPlist: "Config/Framework3Tests-Info.plist", diff --git a/fixtures/ios_app_with_framework_linking_static_framework/Framework4/Project.swift b/fixtures/ios_app_with_framework_linking_static_framework/Framework4/Project.swift index 23507c3ab73..3d62ba539a3 100644 --- a/fixtures/ios_app_with_framework_linking_static_framework/Framework4/Project.swift +++ b/fixtures/ios_app_with_framework_linking_static_framework/Framework4/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework4", targets: [ - Target( + .target( name: "Framework4", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.Framework4", infoPlist: "Config/Framework4-Info.plist", @@ -13,9 +13,9 @@ let project = Project( dependencies: [] ), - Target( + .target( name: "Framework4Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework4Tests", infoPlist: "Config/Framework4Tests-Info.plist", diff --git a/fixtures/ios_app_with_framework_linking_static_framework/README.md b/fixtures/ios_app_with_framework_linking_static_framework/README.md new file mode 100644 index 00000000000..25fd131c333 --- /dev/null +++ b/fixtures/ios_app_with_framework_linking_static_framework/README.md @@ -0,0 +1,34 @@ +# iOS app with a dynamic framework that links a static framework + + +An example project demonstrating an iOS application linking a dynamic framework which itself depends on a static framework with transitive static dependencies. + +Only `Framework1.framework` should be linked and included into App, everything else should be statically linked into the Framework1 executable. + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - Framework1: + - Framework1 (dynamic iOS framework) + - Framework1Tests (iOS unit tests) + - Framework2: + - Framework2 (static iOS framework) + - Framework2Tests (iOS unit tests) + - Framework3: + - Framework3 (static iOS framework) + - Framework3Tests (iOS unit tests) + - Framework4: + - Framework4 (static iOS framework) + - Framework4Tests (iOS unit tests) +``` + +Dependencies: + +- App -> Framework1 +- Framework1 -> Framework2 +- Framework1 -> Framework3 +- Framework3 -> Framework4 + + diff --git a/fixtures/ios_app_with_frameworks/App/App.entitlements b/fixtures/ios_app_with_frameworks/App/App.entitlements new file mode 100644 index 00000000000..ee95ab7e582 --- /dev/null +++ b/fixtures/ios_app_with_frameworks/App/App.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/fixtures/ios_app_with_frameworks/App/Project.swift b/fixtures/ios_app_with_frameworks/App/Project.swift index 5242c0115a3..e4ab68289fe 100644 --- a/fixtures/ios_app_with_frameworks/App/Project.swift +++ b/fixtures/ios_app_with_frameworks/App/Project.swift @@ -8,29 +8,143 @@ let project = Project( name: "MainApp", settings: settings, targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: .extendingDefault(with: [:]), - sources: "Sources/**", - resources: "Sources/Main.storyboard", + sources: "App/Sources/**", + resources: "App/Sources/Main.storyboard", dependencies: [ - .project(target: "Framework1", path: "../Framework1"), - .project(target: "Framework2-iOS", path: "../Framework2"), + .target(name: "Framework1") + .target(name: "Framework2-iOS"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: .extendingDefault(with: [:]), - sources: "Tests/**", + sources: "App/Tests/**", dependencies: [ .target(name: "App"), ] ), + .target( + name: "Framework1", + destinations: .iOS, + product: .framework, + productName: "Framework1", + bundleId: "io.tuist.Framework1", + infoPlist: .dictionary( + [ + "CFBundleDevelopmentRegion": "$(DEVELOPMENT_LANGUAGE)", + "CFBundleExecutable": "$(EXECUTABLE_NAME)", + "CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)", + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "$(PRODUCT_NAME)", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": "1.0", + "CFBundleVersion": "1", + "LSRequiresIPhoneOS": true, + "NSHumanReadableCopyright": "Copyright ©. All rights reserved.", + "Test": "Value", + ] + ), + sources: "Framework1/Sources/**", + dependencies: [ + .target(name: "Framework2-iOS"), + ] + ), + .target( + name: "Framework1Tests", + destinations: .iOS, + product: .unitTests, + productName: "Framework1Tests", + bundleId: "io.tuist.Framework1Tests", + infoPlist: "Config/Framework1Tests-Info.plist", + sources: "Framework1/Tests/**", + dependencies: [ + .target(name: "Framework1"), + ] + ), + .target( + name: "Framework2-iOS", + destinations: .iOS, + product: .framework, + productName: "Framework2", + bundleId: "io.tuist.Framework2", + infoPlist: "Config/Framework2-Info.plist", + sources: "Sources/**", + headers: .headers( + public: "Sources/Public/**", + private: "Sources/Private/**", + project: "Sources/Project/**" + ), + dependencies: [ + .target(name: "Framework3"), + ] + ), + .target( + name: "Framework2-macOS", + destinations: [.mac], + product: .framework, + productName: "Framework2", + bundleId: "io.tuist.Framework2", + infoPlist: "Config/Framework2-Info.plist", + sources: "Framework2/Sources/**", + headers: .headers( + public: "Framework2/Sources/Public/**", + private: "Framework2/Sources/Private/**", + project: "Framework2/Sources/Project/**" + ), + dependencies: [] + ), + .target( + name: "Framework2Tests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.Framework2Tests", + infoPlist: "Config/Framework2Tests-Info.plist", + sources: "Framework2/Tests/**", + dependencies: [ + .target(name: "Framework2-iOS"), + ] + ), + .target( + name: "Framework3", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.Framework3", + infoPlist: "Config/Framework3-Info.plist", + sources: "Framework3/Sources/**", + dependencies: [ + .target(name: "Framework4"), + ] + ), + .target( + name: "Framework4", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.Framework4", + infoPlist: "Config/Framework4-Info.plist", + sources: "Framework4/Sources/**", + dependencies: [ + .target(name: "Framework5"), + ] + ), + .target( + name: "Framework5", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.Framework5", + infoPlist: "Config/Framework5-Info.plist", + sources: "Framework5/Sources/**", + dependencies: [ + .sdk(name: "ARKit", type: .framework), + ] + ), ] ) diff --git a/fixtures/ios_app_with_frameworks/Framework1/Project.swift b/fixtures/ios_app_with_frameworks/Framework1/Project.swift deleted file mode 100644 index 3508b330183..00000000000 --- a/fixtures/ios_app_with_frameworks/Framework1/Project.swift +++ /dev/null @@ -1,45 +0,0 @@ -import ProjectDescription - -let infoPlist: [String: Plist.Value] = [ - "CFBundleDevelopmentRegion": "$(DEVELOPMENT_LANGUAGE)", - "CFBundleExecutable": "$(EXECUTABLE_NAME)", - "CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)", - "CFBundleInfoDictionaryVersion": "6.0", - "CFBundleName": "$(PRODUCT_NAME)", - "CFBundlePackageType": "APPL", - "CFBundleShortVersionString": "1.0", - "CFBundleVersion": "1", - "LSRequiresIPhoneOS": true, - "NSHumanReadableCopyright": "Copyright ©. All rights reserved.", - "Test": "Value", -] - -let project = Project( - name: "Framework1", - targets: [ - Target( - name: "Framework1", - platform: .iOS, - product: .framework, - productName: "Framework1", - bundleId: "io.tuist.Framework1", - infoPlist: .dictionary(infoPlist), - sources: "Sources/**", - dependencies: [ - .project(target: "Framework2-iOS", path: "../Framework2"), - ] - ), - Target( - name: "Framework1Tests", - platform: .iOS, - product: .unitTests, - productName: "Framework1Tests", - bundleId: "io.tuist.Framework1Tests", - infoPlist: "Config/Framework1Tests-Info.plist", - sources: "Tests/**", - dependencies: [ - .target(name: "Framework1"), - ] - ), - ] -) diff --git a/fixtures/ios_app_with_frameworks/Framework2/Project.swift b/fixtures/ios_app_with_frameworks/Framework2/Project.swift deleted file mode 100644 index 1cd09e99c21..00000000000 --- a/fixtures/ios_app_with_frameworks/Framework2/Project.swift +++ /dev/null @@ -1,50 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "Framework2", - targets: [ - Target( - name: "Framework2-iOS", - platform: .iOS, - product: .framework, - productName: "Framework2", - bundleId: "io.tuist.Framework2", - infoPlist: "Config/Framework2-Info.plist", - sources: "Sources/**", - headers: .headers( - public: "Sources/Public/**", - private: "Sources/Private/**", - project: "Sources/Project/**" - ), - dependencies: [ - .project(target: "Framework3", path: "../Framework3"), - ] - ), - Target( - name: "Framework2-macOS", - platform: .macOS, - product: .framework, - productName: "Framework2", - bundleId: "io.tuist.Framework2", - infoPlist: "Config/Framework2-Info.plist", - sources: "Sources/**", - headers: .headers( - public: "Sources/Public/**", - private: "Sources/Private/**", - project: "Sources/Project/**" - ), - dependencies: [] - ), - Target( - name: "Framework2Tests", - platform: .iOS, - product: .unitTests, - bundleId: "io.tuist.Framework2Tests", - infoPlist: "Config/Framework2Tests-Info.plist", - sources: "Tests/**", - dependencies: [ - .target(name: "Framework2-iOS"), - ] - ), - ] -) diff --git a/fixtures/ios_app_with_frameworks/Framework3/Project.swift b/fixtures/ios_app_with_frameworks/Framework3/Project.swift deleted file mode 100644 index b4dde76373c..00000000000 --- a/fixtures/ios_app_with_frameworks/Framework3/Project.swift +++ /dev/null @@ -1,18 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "Framework3", - targets: [ - Target( - name: "Framework3", - platform: .iOS, - product: .framework, - bundleId: "io.tuist.Framework3", - infoPlist: "Config/Framework3-Info.plist", - sources: "Sources/**", - dependencies: [ - .project(target: "Framework4", path: "../Framework4"), - ] - ), - ] -) diff --git a/fixtures/ios_app_with_frameworks/Framework4/Project.swift b/fixtures/ios_app_with_frameworks/Framework4/Project.swift deleted file mode 100644 index 407b19daf28..00000000000 --- a/fixtures/ios_app_with_frameworks/Framework4/Project.swift +++ /dev/null @@ -1,18 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "Framework4", - targets: [ - Target( - name: "Framework4", - platform: .iOS, - product: .framework, - bundleId: "io.tuist.Framework4", - infoPlist: "Config/Framework4-Info.plist", - sources: "Sources/**", - dependencies: [ - .project(target: "Framework5", path: "../Framework5"), - ] - ), - ] -) diff --git a/fixtures/ios_app_with_frameworks/Framework5/Project.swift b/fixtures/ios_app_with_frameworks/Framework5/Project.swift deleted file mode 100644 index 196fbca88cc..00000000000 --- a/fixtures/ios_app_with_frameworks/Framework5/Project.swift +++ /dev/null @@ -1,18 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "Framework5", - targets: [ - Target( - name: "Framework5", - platform: .iOS, - product: .framework, - bundleId: "io.tuist.Framework5", - infoPlist: "Config/Framework5-Info.plist", - sources: "Sources/**", - dependencies: [ - .sdk(name: "ARKit", type: .framework), - ] - ), - ] -) diff --git a/fixtures/ios_app_with_frameworks/Project.swift b/fixtures/ios_app_with_frameworks/Project.swift new file mode 100644 index 00000000000..0b3f7f9bb3b --- /dev/null +++ b/fixtures/ios_app_with_frameworks/Project.swift @@ -0,0 +1,150 @@ +import ProjectDescription + +let settings: Settings = .settings(base: [ + "HEADER_SEARCH_PATHS": "path/to/lib/include", +]) + +let project = Project( + name: "MainApp", + settings: settings, + targets: [ + .target( + name: "App", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.App", + infoPlist: .extendingDefault(with: [:]), + sources: "App/Sources/**", + resources: "App/Sources/Main.storyboard", + dependencies: [ + .target(name: "Framework1"), + .target(name: "Framework2-iOS"), + ] + ), + .target( + name: "AppTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.AppTests", + infoPlist: .extendingDefault(with: [:]), + sources: "App/Tests/**", + dependencies: [ + .target(name: "App"), + ] + ), + .target( + name: "Framework1", + destinations: .iOS, + product: .framework, + productName: "Framework1", + bundleId: "io.tuist.Framework1", + infoPlist: .dictionary( + [ + "CFBundleDevelopmentRegion": "$(DEVELOPMENT_LANGUAGE)", + "CFBundleExecutable": "$(EXECUTABLE_NAME)", + "CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)", + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "$(PRODUCT_NAME)", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": "1.0", + "CFBundleVersion": "1", + "LSRequiresIPhoneOS": true, + "NSHumanReadableCopyright": "Copyright ©. All rights reserved.", + "Test": "Value", + ] + ), + sources: "Framework1/Sources/**", + dependencies: [ + .target(name: "Framework2-iOS"), + ] + ), + .target( + name: "Framework1Tests", + destinations: .iOS, + product: .unitTests, + productName: "Framework1Tests", + bundleId: "io.tuist.Framework1Tests", + infoPlist: "Framework1/Config/Framework1Tests-Info.plist", + sources: "Framework1/Tests/**", + dependencies: [ + .target(name: "Framework1"), + ] + ), + .target( + name: "Framework2-iOS", + destinations: .iOS, + product: .framework, + productName: "Framework2", + bundleId: "io.tuist.Framework2", + infoPlist: "Framework2/Config/Framework2-Info.plist", + sources: "Framework2/Sources/**", + headers: .headers( + public: "Framework2/Sources/Public/**", + private: "Framework2/Sources/Private/**", + project: "Framework2/Sources/Project/**" + ), + dependencies: [ + .target(name: "Framework3"), + ] + ), + .target( + name: "Framework2-macOS", + destinations: [.mac], + product: .framework, + productName: "Framework2", + bundleId: "io.tuist.Framework2", + infoPlist: "Framework2/Config/Framework2-Info.plist", + sources: "Framework2/Sources/**", + headers: .headers( + public: "Framework2/Sources/Public/**", + private: "Framework2/Sources/Private/**", + project: "Framework2/Sources/Project/**" + ), + dependencies: [] + ), + .target( + name: "Framework2Tests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.Framework2Tests", + infoPlist: "Framework2/Config/Framework2Tests-Info.plist", + sources: "Framework2/Tests/**", + dependencies: [ + .target(name: "Framework2-iOS"), + ] + ), + .target( + name: "Framework3", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.Framework3", + infoPlist: "Framework3/Config/Framework3-Info.plist", + sources: "Framework3/Sources/**", + dependencies: [ + .target(name: "Framework4"), + ] + ), + .target( + name: "Framework4", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.Framework4", + infoPlist: "Framework4/Config/Framework4-Info.plist", + sources: "Framework4/Sources/**", + dependencies: [ + .target(name: "Framework5"), + ] + ), + .target( + name: "Framework5", + destinations: .iOS, + product: .framework, + bundleId: "io.tuist.Framework5", + infoPlist: "Framework5/Config/Framework5-Info.plist", + sources: "Framework5/Sources/**", + dependencies: [ + .sdk(name: "ARKit", type: .framework), + ] + ), + ] +) diff --git a/fixtures/ios_app_with_frameworks/README.md b/fixtures/ios_app_with_frameworks/README.md new file mode 100644 index 00000000000..709a38c9145 --- /dev/null +++ b/fixtures/ios_app_with_frameworks/README.md @@ -0,0 +1,32 @@ +# iOS app with frameworks + + +Slightly more complicated project that consists of an iOS app and few frameworks. + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - Framework1: + - Framework1 (dynamic iOS framework) + - Framework1Tests (iOS unit tests) + - Framework2: + - Framework2 (dynamic iOS framework) + - Framework2Tests (iOS unit tests) + - Framework3: + - Framework3 (dynamic iOS framework) + - Framework4: + - Framework4 (dynamic iOS framework) + - Framework5: + - Framework5 (dynamic iOS framework) +``` + +Dependencies: + +- App -> Framework1 +- App -> Framework2 +- Framework1 -> Framework2 +- Framework2 -> Framework3 +- Framework3 -> Framework4 +- Framework4 -> Framework5 \ No newline at end of file diff --git a/fixtures/ios_app_with_frameworks/Tuist/Config.swift b/fixtures/ios_app_with_frameworks/Tuist/Config.swift index ff8e64fa9ee..872d0fe9614 100644 --- a/fixtures/ios_app_with_frameworks/Tuist/Config.swift +++ b/fixtures/ios_app_with_frameworks/Tuist/Config.swift @@ -1,3 +1,9 @@ import ProjectDescription -let config = Config() +let config = Config( + fullHandle: "tuist/ios_app_with_frameworks", + url: "https://canary.tuist.io", + generationOptions: .options( + optionalAuthentication: true + ) +) diff --git a/fixtures/ios_app_with_frameworks/Workspace.swift b/fixtures/ios_app_with_frameworks/Workspace.swift deleted file mode 100644 index 929bf3b53aa..00000000000 --- a/fixtures/ios_app_with_frameworks/Workspace.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ProjectDescription - -let workspace = Workspace( - name: "Workspace", - projects: ["App", "Framework1", "Framework2"] -) diff --git a/fixtures/ios_app_with_headers/App/Project.swift b/fixtures/ios_app_with_headers/App/Project.swift index 91d1c7e9036..fd425cd0fca 100644 --- a/fixtures/ios_app_with_headers/App/Project.swift +++ b/fixtures/ios_app_with_headers/App/Project.swift @@ -7,9 +7,9 @@ let project = Project( name: "MainApp", settings: settings, targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -18,9 +18,9 @@ let project = Project( .project(target: "Framework1-iOS", path: "../Framework1"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_headers/Framework1/Project.swift b/fixtures/ios_app_with_headers/Framework1/Project.swift index 088855416a0..4239029d07c 100644 --- a/fixtures/ios_app_with_headers/Framework1/Project.swift +++ b/fixtures/ios_app_with_headers/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1-iOS", - platform: .iOS, + destinations: .iOS, product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -18,9 +18,9 @@ let project = Project( ), dependencies: [] ), - Target( + .target( name: "Framework1-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -33,9 +33,9 @@ let project = Project( ), dependencies: [] ), - Target( + .target( name: "Framework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Config/Framework1Tests-Info.plist", diff --git a/fixtures/ios_app_with_headers_in_one_dir/App/Project.swift b/fixtures/ios_app_with_headers_in_one_dir/App/Project.swift index 91d1c7e9036..fd425cd0fca 100644 --- a/fixtures/ios_app_with_headers_in_one_dir/App/Project.swift +++ b/fixtures/ios_app_with_headers_in_one_dir/App/Project.swift @@ -7,9 +7,9 @@ let project = Project( name: "MainApp", settings: settings, targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -18,9 +18,9 @@ let project = Project( .project(target: "Framework1-iOS", path: "../Framework1"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_headers_in_one_dir/Framework1/Project.swift b/fixtures/ios_app_with_headers_in_one_dir/Framework1/Project.swift index 6d3ced8aa00..394d8b2a4ac 100644 --- a/fixtures/ios_app_with_headers_in_one_dir/Framework1/Project.swift +++ b/fixtures/ios_app_with_headers_in_one_dir/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1-iOS", - platform: .iOS, + destinations: .iOS, product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -18,9 +18,9 @@ let project = Project( ), dependencies: [] ), - Target( + .target( name: "Framework1-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -33,9 +33,9 @@ let project = Project( ), dependencies: [] ), - Target( + .target( name: "Framework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Config/Framework1Tests-Info.plist", diff --git a/fixtures/ios_app_with_helpers/Projects/App/Project.swift b/fixtures/ios_app_with_helpers/Projects/App/Project.swift index f89b263f690..4121f2f2a2d 100644 --- a/fixtures/ios_app_with_helpers/Projects/App/Project.swift +++ b/fixtures/ios_app_with_helpers/Projects/App/Project.swift @@ -1,6 +1,6 @@ import ProjectDescription import ProjectDescriptionHelpers -let project = Project.app(name: "App", platform: .iOS, dependencies: [ +let project = Project.app(name: "App", destinations: .iOS, dependencies: [ .project(target: "AppKit", path: "//Projects/AppKit"), ]) diff --git a/fixtures/ios_app_with_helpers/Projects/AppKit/Project.swift b/fixtures/ios_app_with_helpers/Projects/AppKit/Project.swift index 58117aadcf5..29990fbff19 100644 --- a/fixtures/ios_app_with_helpers/Projects/AppKit/Project.swift +++ b/fixtures/ios_app_with_helpers/Projects/AppKit/Project.swift @@ -1,6 +1,6 @@ import ProjectDescription import ProjectDescriptionHelpers -let project = Project.framework(name: "AppKit", platform: .iOS, dependencies: [ +let project = Project.framework(name: "AppKit", destinations: .iOS, dependencies: [ .project(target: "AppSupport", path: "//Projects/AppSupport"), ]) diff --git a/fixtures/ios_app_with_helpers/Projects/AppSupport/Project.swift b/fixtures/ios_app_with_helpers/Projects/AppSupport/Project.swift index 9dad7ece8cb..0770da5a5b2 100644 --- a/fixtures/ios_app_with_helpers/Projects/AppSupport/Project.swift +++ b/fixtures/ios_app_with_helpers/Projects/AppSupport/Project.swift @@ -1,4 +1,4 @@ import ProjectDescription import ProjectDescriptionHelpers -let project = Project.framework(name: "AppSupport", platform: .iOS, dependencies: []) +let project = Project.framework(name: "AppSupport", destinations: .iOS, dependencies: []) diff --git a/fixtures/ios_app_with_helpers/README.md b/fixtures/ios_app_with_helpers/README.md new file mode 100644 index 00000000000..39cc669261c --- /dev/null +++ b/fixtures/ios_app_with_helpers/README.md @@ -0,0 +1,3 @@ +# iOS app with helpers + +A basic iOS app that leverages `ProjectDescriptionHelpers`. diff --git a/fixtures/ios_app_with_helpers/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/fixtures/ios_app_with_helpers/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index c01c59a4e94..cd547b2b5d8 100644 --- a/fixtures/ios_app_with_helpers/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/fixtures/ios_app_with_helpers/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -1,30 +1,30 @@ import ProjectDescription extension Project { - public static func app(name: String, platform: Platform, dependencies: [TargetDependency] = []) -> Project { - project(name: name, product: .app, platform: platform, dependencies: dependencies, infoPlist: [ + public static func app(name: String, destinations: Destinations, dependencies: [TargetDependency] = []) -> Project { + project(name: name, product: .app, destinations: destinations, dependencies: dependencies, infoPlist: [ "CFBundleShortVersionString": "1.0", "CFBundleVersion": "1", ]) } - public static func framework(name: String, platform: Platform, dependencies: [TargetDependency] = []) -> Project { - project(name: name, product: .framework, platform: platform, dependencies: dependencies) + public static func framework(name: String, destinations: Destinations, dependencies: [TargetDependency] = []) -> Project { + project(name: name, product: .framework, destinations: destinations, dependencies: dependencies) } public static func project( name: String, product: Product, - platform: Platform, + destinations: Destinations, dependencies: [TargetDependency] = [], infoPlist: [String: Plist.Value] = [:] ) -> Project { Project( name: name, targets: [ - Target( + .target( name: name, - platform: platform, + destinations: destinations, product: product, bundleId: "io.tuist.\(name)", infoPlist: .extendingDefault(with: infoPlist), @@ -32,9 +32,9 @@ extension Project { resources: [], dependencies: dependencies ), - Target( + .target( name: "\(name)Tests", - platform: platform, + destinations: destinations, product: .unitTests, bundleId: "io.tuist.\(name)Tests", infoPlist: .default, diff --git a/fixtures/ios_app_with_implicit_dependencies/Project.swift b/fixtures/ios_app_with_implicit_dependencies/Project.swift index ace61ab3a76..2f4b216d686 100644 --- a/fixtures/ios_app_with_implicit_dependencies/Project.swift +++ b/fixtures/ios_app_with_implicit_dependencies/Project.swift @@ -4,7 +4,7 @@ let project = Project( name: "App", organizationName: "tuist.io", targets: [ - Target( + .target( name: "App", destinations: .iOS, product: .app, @@ -15,21 +15,21 @@ let project = Project( .target(name: "FrameworkB"), ] ), - Target( + .target( name: "FrameworkA", destinations: .iOS, product: .framework, bundleId: "io.tuist.FrameworkA", sources: ["Targets/FrameworkA/Sources/**"] ), - Target( + .target( name: "FrameworkB", destinations: .iOS, product: .framework, bundleId: "io.tuist.FrameworkB", sources: ["Targets/FrameworkB/Sources/**"] ), - Target( + .target( name: "FrameworkC", destinations: .iOS, product: .framework, diff --git a/fixtures/ios_app_with_incompatible_dependencies/Project.swift b/fixtures/ios_app_with_incompatible_dependencies/Project.swift index f61c399244f..290888bc40e 100644 --- a/fixtures/ios_app_with_incompatible_dependencies/Project.swift +++ b/fixtures/ios_app_with_incompatible_dependencies/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -18,9 +18,9 @@ let project = Project( "CODE_SIGNING_REQUIRED": "NO", ]) ), - Target( + .target( name: "Framework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework", infoPlist: "Framework.plist", diff --git a/fixtures/ios_app_with_incompatible_dependencies/README.md b/fixtures/ios_app_with_incompatible_dependencies/README.md new file mode 100644 index 00000000000..29d0dedfbd5 --- /dev/null +++ b/fixtures/ios_app_with_incompatible_dependencies/README.md @@ -0,0 +1,3 @@ +# iOS app with incompatible dependencies + +An iOS app that has a dependency with a dependency on a framework for macOS. \ No newline at end of file diff --git a/fixtures/ios_app_with_incompatible_xcode/Project.swift b/fixtures/ios_app_with_incompatible_xcode/Project.swift index beda0c2d795..4ea128ddc88 100644 --- a/fixtures/ios_app_with_incompatible_xcode/Project.swift +++ b/fixtures/ios_app_with_incompatible_xcode/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_incompatible_xcode/README.md b/fixtures/ios_app_with_incompatible_xcode/README.md new file mode 100644 index 00000000000..504c7379872 --- /dev/null +++ b/fixtures/ios_app_with_incompatible_xcode/README.md @@ -0,0 +1,3 @@ +# iOS app with incompatible Xcode + +An iOS app whose Config file requires an Xcode version that is not available in the system. \ No newline at end of file diff --git a/fixtures/ios_app_with_intents/Project.swift b/fixtures/ios_app_with_intents/Project.swift index fc8a69b125d..0ec543638aa 100644 --- a/fixtures/ios_app_with_intents/Project.swift +++ b/fixtures/ios_app_with_intents/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_local_binary_swift_package/Packages/LocalPackage/MyFramework/Project.swift b/fixtures/ios_app_with_local_binary_swift_package/Packages/LocalPackage/MyFramework/Project.swift index 1804b2e354e..8bdec89d744 100644 --- a/fixtures/ios_app_with_local_binary_swift_package/Packages/LocalPackage/MyFramework/Project.swift +++ b/fixtures/ios_app_with_local_binary_swift_package/Packages/LocalPackage/MyFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MyFramework", targets: [ - Target( + .target( name: "MyFramework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.MyFramework", infoPlist: .default, diff --git a/fixtures/ios_app_with_local_binary_swift_package/Project.swift b/fixtures/ios_app_with_local_binary_swift_package/Project.swift index b7348e5f31b..bbe634d7838 100644 --- a/fixtures/ios_app_with_local_binary_swift_package/Project.swift +++ b/fixtures/ios_app_with_local_binary_swift_package/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "Packages/LocalPackage"), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: .default, @@ -19,9 +19,9 @@ let project = Project( .package(product: "MyFramework"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: .default, diff --git a/fixtures/ios_app_with_local_swift_package/Frameworks/FrameworkA/Project.swift b/fixtures/ios_app_with_local_swift_package/Frameworks/FrameworkA/Project.swift index 7d7e7e396a1..c8cd5ea0a55 100644 --- a/fixtures/ios_app_with_local_swift_package/Frameworks/FrameworkA/Project.swift +++ b/fixtures/ios_app_with_local_swift_package/Frameworks/FrameworkA/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "../../Packages/PackageA"), ], targets: [ - Target( + .target( name: "FrameworkA", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.FrameworkA", infoPlist: "Config/FrameworkA-Info.plist", diff --git a/fixtures/ios_app_with_local_swift_package/Project.swift b/fixtures/ios_app_with_local_swift_package/Project.swift index 6d6150e9dfc..b68084f9333 100644 --- a/fixtures/ios_app_with_local_swift_package/Project.swift +++ b/fixtures/ios_app_with_local_swift_package/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "Packages/PackageA"), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/Info.plist", @@ -23,9 +23,9 @@ let project = Project( .package(product: "LibraryB"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/ios_app_with_local_swift_package/README.md b/fixtures/ios_app_with_local_swift_package/README.md new file mode 100644 index 00000000000..89d7ab03c30 --- /dev/null +++ b/fixtures/ios_app_with_local_swift_package/README.md @@ -0,0 +1,3 @@ +# iOS app with a local Swift package + +An iOS application that depends on a local Swift package. \ No newline at end of file diff --git a/fixtures/ios_app_with_multi_configs/App/Project.swift b/fixtures/ios_app_with_multi_configs/App/Project.swift index 2d8b422cfd9..fe92251b2ad 100644 --- a/fixtures/ios_app_with_multi_configs/App/Project.swift +++ b/fixtures/ios_app_with_multi_configs/App/Project.swift @@ -11,7 +11,7 @@ let settings: Settings = .settings( ] ) -let betaScheme = Scheme( +let betaScheme: Scheme = .scheme( name: "App-Beta", shared: true, buildAction: .buildAction(targets: ["App"]), @@ -21,13 +21,19 @@ let betaScheme = Scheme( analyzeAction: .analyzeAction(configuration: "Debug") ) +let betaArchiveOnlyScheme: Scheme = .scheme( + name: "App-Beta-ArchiveOnly", + shared: true, + archiveAction: .archiveAction(configuration: "Beta") +) + let project = Project( name: "MainApp", settings: settings, targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/App-Info.plist", @@ -37,9 +43,9 @@ let project = Project( .project(target: "Framework2", path: "../Framework2"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/AppTests-Info.plist", @@ -49,5 +55,8 @@ let project = Project( ] ), ], - schemes: [betaScheme] + schemes: [ + betaScheme, + betaArchiveOnlyScheme, + ] ) diff --git a/fixtures/ios_app_with_multi_configs/Framework1/Project.swift b/fixtures/ios_app_with_multi_configs/Framework1/Project.swift index c78c8b6bc6b..f275b144dd4 100644 --- a/fixtures/ios_app_with_multi_configs/Framework1/Project.swift +++ b/fixtures/ios_app_with_multi_configs/Framework1/Project.swift @@ -15,9 +15,9 @@ let project = Project( name: "Framework1", settings: settings, targets: [ - Target( + .target( name: "Framework1", - platform: .iOS, + destinations: .iOS, product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -27,9 +27,9 @@ let project = Project( .project(target: "Framework2", path: "../Framework2"), ] ), - Target( + .target( name: "Framework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, productName: "Framework1Tests", bundleId: "io.tuist.Framework1Tests", diff --git a/fixtures/ios_app_with_multi_configs/Framework2/Project.swift b/fixtures/ios_app_with_multi_configs/Framework2/Project.swift index bb110cbdb8c..bbec1c7f3e2 100644 --- a/fixtures/ios_app_with_multi_configs/Framework2/Project.swift +++ b/fixtures/ios_app_with_multi_configs/Framework2/Project.swift @@ -22,9 +22,9 @@ let project = Project( name: "Framework2", settings: settings, targets: [ - Target( + .target( name: "Framework2", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.Framework2", infoPlist: "Support/Framework2-Info.plist", @@ -32,9 +32,9 @@ let project = Project( dependencies: [], settings: targetSettings ), - Target( + .target( name: "Framework2Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework2Tests", infoPlist: "Support/Framework2Tests-Info.plist", diff --git a/fixtures/ios_app_with_multi_configs/README.md b/fixtures/ios_app_with_multi_configs/README.md new file mode 100644 index 00000000000..f2f40c42d93 --- /dev/null +++ b/fixtures/ios_app_with_multi_configs/README.md @@ -0,0 +1,3 @@ +# iOS app with multiple configurations and an xcconfig + +A workspace that contains an application and frameworks that leverage multiple configurations (Debug, Beta and Release) each of which also has an associated xcconfig file within `ConfigurationFiles`. \ No newline at end of file diff --git a/fixtures/ios_app_with_on_demand_resources/.gitignore b/fixtures/ios_app_with_on_demand_resources/.gitignore new file mode 100644 index 00000000000..24b244f9c45 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/.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/.build \ No newline at end of file diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/OnDemandResources/on-demand-data.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/OnDemandResources/on-demand-data.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AR Resources.arresourcegroup/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AR Resources.arresourcegroup/Contents.json new file mode 100644 index 00000000000..55e99f6affc --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AR Resources.arresourcegroup/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "ar-resource-group" + ] + }, + "resources" : [ + + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..9221b9bb1a3 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/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/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Contents.json new file mode 100644 index 00000000000..3d253b36e8a --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Contents.json @@ -0,0 +1,43 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "cube-texture" + ] + }, + "textures" : [ + { + "cube-face" : "x-", + "filename" : "Universal -X.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "y-", + "filename" : "Universal -Y.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "z-", + "filename" : "Universal -Z.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "x+", + "filename" : "Universal +X.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "y+", + "filename" : "Universal +Y.mipmapset", + "idiom" : "universal" + }, + { + "cube-face" : "z+", + "filename" : "Universal +Z.mipmapset", + "idiom" : "universal" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +X.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +X.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +X.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +Y.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +Y.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +Y.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +Z.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +Z.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal +Z.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -X.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -X.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -X.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -Y.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -Y.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -Y.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -Z.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -Z.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Cube Texture.cubetextureset/Universal -Z.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Data.dataset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Data.dataset/Contents.json new file mode 100644 index 00000000000..201158c1dcf --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Data.dataset/Contents.json @@ -0,0 +1,16 @@ +{ + "data" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "data" + ] + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Image.imageset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 00000000000..844e883a81d --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "image" + ] + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/NewFolder/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/NewFolder/Contents.json new file mode 100644 index 00000000000..31835dbfe49 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/NewFolder/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "newfolder" + ] + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/NewFolder/nestedimage.imageset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/NewFolder/nestedimage.imageset/Contents.json new file mode 100644 index 00000000000..a7d77d928d2 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/NewFolder/nestedimage.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "nestedimage" + ] + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Sprites.spriteatlas/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Sprites.spriteatlas/Contents.json new file mode 100644 index 00000000000..7ff56454647 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Sprites.spriteatlas/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "sprite" + ] + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Sprites.spriteatlas/Sprite.imageset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Sprites.spriteatlas/Sprite.imageset/Contents.json new file mode 100644 index 00000000000..a19a5492203 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Sprites.spriteatlas/Sprite.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..795cce17243 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Back.imagestacklayer/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Contents.json new file mode 100644 index 00000000000..0503b6204e8 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Contents.json @@ -0,0 +1,22 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "properties" : { + "on-demand-resource-tags" : [ + "image-stack" + ] + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..795cce17243 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Front.imagestacklayer/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..795cce17243 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Middle.imagestacklayer/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Stack.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Texture.textureset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Texture.textureset/Contents.json new file mode 100644 index 00000000000..ce82bfc2c9b --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Texture.textureset/Contents.json @@ -0,0 +1,18 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "on-demand-resource-tags" : [ + "tag with space", + "texture" + ] + }, + "textures" : [ + { + "filename" : "Universal.mipmapset", + "idiom" : "universal" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Texture.textureset/Universal.mipmapset/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Texture.textureset/Universal.mipmapset/Contents.json new file mode 100644 index 00000000000..bdd33d560db --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Assets.xcassets/Texture.textureset/Universal.mipmapset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "levels" : [ + { + "mipmap-level" : "base" + } + ] +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/LaunchScreen.storyboard b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000000..865e9329f37 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Sources/AppWithOnDemandResourcesApp.swift b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Sources/AppWithOnDemandResourcesApp.swift new file mode 100644 index 00000000000..78a641b4810 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Sources/AppWithOnDemandResourcesApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct AppWithOnDemandResourcesApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Sources/ContentView.swift b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Sources/ContentView.swift new file mode 100644 index 00000000000..48f62ebea83 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Sources/ContentView.swift @@ -0,0 +1,16 @@ +import SwiftUI + +public struct ContentView: View { + public init() {} + + public var body: some View { + Text("Hello, World!") + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Tests/AppWithOnDemandResourcesTests.swift b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Tests/AppWithOnDemandResourcesTests.swift new file mode 100644 index 00000000000..6fc1cbd2f6f --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/Tests/AppWithOnDemandResourcesTests.swift @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +final class AppWithOnDemandResourcesTests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/on-demand-data.txt b/fixtures/ios_app_with_on_demand_resources/AppWithOnDemandResources/on-demand-data.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/ios_app_with_on_demand_resources/Project.swift b/fixtures/ios_app_with_on_demand_resources/Project.swift new file mode 100644 index 00000000000..6e8d9bdcd05 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/Project.swift @@ -0,0 +1,39 @@ +import ProjectDescription + +let project = Project( + name: "AppWithOnDemandResources", + targets: [ + .target( + name: "AppWithOnDemandResources", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.AppWithOnDemandResources", + infoPlist: .extendingDefault( + with: [ + "UILaunchStoryboardName": "LaunchScreen.storyboard", + ] + ), + sources: ["AppWithOnDemandResources/Sources/**"], + resources: [ + "AppWithOnDemandResources/Resources/**", + .folderReference(path: "AppWithOnDemandResources/OnDemandResources", tags: ["datafolder"]), + .glob(pattern: "AppWithOnDemandResources/on-demand-data.txt", tags: ["datafile"]), + ], + dependencies: [], + onDemandResourcesTags: .tags( + initialInstall: ["json", "data file"], + prefetchOrder: ["image-stack", "image", "tag with space"] + ) + ), + .target( + name: "AppWithOnDemandResourcesTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.AppWithOnDemandResourcesTests", + infoPlist: .default, + sources: ["AppWithOnDemandResources/Tests/**"], + resources: [], + dependencies: [.target(name: "AppWithOnDemandResources")] + ), + ] +) diff --git a/fixtures/ios_app_with_on_demand_resources/README.md b/fixtures/ios_app_with_on_demand_resources/README.md new file mode 100644 index 00000000000..a5bddf95356 --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/README.md @@ -0,0 +1,6 @@ +# iOS app with on demand resources + +An iOS applicaiton with on-demand resources. It contains file resources and asset catalogs associated with tags which in turn are distributed between three categories: +- Initial install tags +- Prefetch tag order +- Dowloaded only on demand \ No newline at end of file diff --git a/fixtures/ios_app_with_on_demand_resources/Tuist/Config.swift b/fixtures/ios_app_with_on_demand_resources/Tuist/Config.swift new file mode 100644 index 00000000000..ff8e64fa9ee --- /dev/null +++ b/fixtures/ios_app_with_on_demand_resources/Tuist/Config.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let config = Config() diff --git a/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/Plugin.swift b/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/Plugin.swift new file mode 100644 index 00000000000..fca960c0d35 --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/Plugin.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let plugin = Plugin(name: "LocalPlugin") diff --git a/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/ProjectDescriptionHelpers/Project+Extensions.swift b/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/ProjectDescriptionHelpers/Project+Extensions.swift new file mode 100644 index 00000000000..ecd77f6696b --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/ProjectDescriptionHelpers/Project+Extensions.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +extension Project { + public static var name: String { "LocalPlugin" } +} diff --git a/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/Templates/plugin/plugin.swift b/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/Templates/plugin/plugin.swift new file mode 100644 index 00000000000..81ad1fb2374 --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/LocalPlugin/Templates/plugin/plugin.swift @@ -0,0 +1,11 @@ +import LocalPlugin +import ProjectDescription + +let template = Template( + description: "plugin", + items: [.item( + path: "./Sources/PluginTemplateTest.swift", + + contents: .string("// Generated file named \(Project.name) from plugin") + )] +) diff --git a/fixtures/ios_app_with_plugins_and_templates/Project.swift b/fixtures/ios_app_with_plugins_and_templates/Project.swift new file mode 100644 index 00000000000..d79ebf2977b --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/Project.swift @@ -0,0 +1,4 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project(name: Constants.name) diff --git a/fixtures/ios_app_with_plugins_and_templates/ProjectDescriptionHelpers/Constants.swift b/fixtures/ios_app_with_plugins_and_templates/ProjectDescriptionHelpers/Constants.swift new file mode 100644 index 00000000000..ebf0b4dc15c --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/ProjectDescriptionHelpers/Constants.swift @@ -0,0 +1,10 @@ +import LocalPlugin +import ProjectDescription + +public enum Constants { + public static let name: String = Project.name +} + +// public struct Constants { +// public static let name: String = "WithoutPlugin" +// } diff --git a/fixtures/ios_app_with_plugins_and_templates/Sources/Empty.swift b/fixtures/ios_app_with_plugins_and_templates/Sources/Empty.swift new file mode 100644 index 00000000000..58194fb4656 --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/Sources/Empty.swift @@ -0,0 +1 @@ +struct Empty {} diff --git a/fixtures/ios_app_with_plugins_and_templates/Tuist/Config.swift b/fixtures/ios_app_with_plugins_and_templates/Tuist/Config.swift new file mode 100644 index 00000000000..a2784a09af6 --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/Tuist/Config.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let config = Config(plugins: [.local(path: "../../LocalPlugin")]) diff --git a/fixtures/ios_app_with_plugins_and_templates/Tuist/ProjectDescriptionHelpers/Constants.swift b/fixtures/ios_app_with_plugins_and_templates/Tuist/ProjectDescriptionHelpers/Constants.swift new file mode 100644 index 00000000000..a81dc751f2a --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/Tuist/ProjectDescriptionHelpers/Constants.swift @@ -0,0 +1,6 @@ +import LocalPlugin +import ProjectDescription + +public enum Constants { + public static let name: String = Project.name +} diff --git a/fixtures/ios_app_with_plugins_and_templates/Tuist/Templates/example/example.swift b/fixtures/ios_app_with_plugins_and_templates/Tuist/Templates/example/example.swift new file mode 100644 index 00000000000..2aad2cfd608 --- /dev/null +++ b/fixtures/ios_app_with_plugins_and_templates/Tuist/Templates/example/example.swift @@ -0,0 +1,11 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let template = Template( + description: "example", + items: [.item( + path: "./Sources/LocalTemplateTest.swift", + + contents: .string("// Generated file named \(Constants.name) from local template") + )] +) diff --git a/fixtures/ios_app_with_privacy_manifest/.gitignore b/fixtures/ios_app_with_privacy_manifest/.gitignore new file mode 100644 index 00000000000..24b244f9c45 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/.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/.build \ No newline at end of file diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..9221b9bb1a3 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/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/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/Contents.json b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/LaunchScreen.storyboard b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000000..865e9329f37 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Sources/ContentView.swift b/fixtures/ios_app_with_privacy_manifest/MyApp/Sources/ContentView.swift new file mode 100644 index 00000000000..48f62ebea83 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Sources/ContentView.swift @@ -0,0 +1,16 @@ +import SwiftUI + +public struct ContentView: View { + public init() {} + + public var body: some View { + Text("Hello, World!") + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Sources/MyAppApp.swift b/fixtures/ios_app_with_privacy_manifest/MyApp/Sources/MyAppApp.swift new file mode 100644 index 00000000000..2db0d884171 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Sources/MyAppApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MyAppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/ios_app_with_privacy_manifest/MyApp/Tests/MyAppTests.swift b/fixtures/ios_app_with_privacy_manifest/MyApp/Tests/MyAppTests.swift new file mode 100644 index 00000000000..c36005b54f2 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/MyApp/Tests/MyAppTests.swift @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +final class MyAppTests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/fixtures/ios_app_with_privacy_manifest/Project.swift b/fixtures/ios_app_with_privacy_manifest/Project.swift new file mode 100644 index 00000000000..f9d0e8d5127 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/Project.swift @@ -0,0 +1,55 @@ +import ProjectDescription + +let project = Project( + name: "MyApp", + targets: [ + .target( + name: "MyApp", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.MyApp", + infoPlist: .extendingDefault( + with: [ + "UILaunchStoryboardName": "LaunchScreen.storyboard", + ] + ), + sources: ["MyApp/Sources/**"], + resources: .resources( + ["MyApp/Resources/**"], + privacyManifest: .privacyManifest( + tracking: false, + trackingDomains: [], + collectedDataTypes: [ + [ + "NSPrivacyCollectedDataType": "NSPrivacyCollectedDataTypeName", + "NSPrivacyCollectedDataTypeLinked": false, + "NSPrivacyCollectedDataTypeTracking": false, + "NSPrivacyCollectedDataTypePurposes": [ + "NSPrivacyCollectedDataTypePurposeAppFunctionality", + ], + ], + ], + accessedApiTypes: [ + [ + "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults", + "NSPrivacyAccessedAPITypeReasons": [ + "CA92.1", + ], + ], + ] + ) + ), + dependencies: [] + ), + .target( + name: "MyAppTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.MyAppTests", + infoPlist: .default, + sources: ["MyApp/Tests/**"], + resources: [], + dependencies: [.target(name: "MyApp")] + ), + ] +) diff --git a/fixtures/ios_app_with_privacy_manifest/README.md b/fixtures/ios_app_with_privacy_manifest/README.md new file mode 100644 index 00000000000..45d6d874f94 --- /dev/null +++ b/fixtures/ios_app_with_privacy_manifest/README.md @@ -0,0 +1,3 @@ +# Application with Privacy Manifest + +This example contains an example that uses a [Privacy Manifest](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files), which describe the data the app collects and the reasons required APIs it uses. \ No newline at end of file diff --git a/fixtures/ios_app_with_remote_swift_package/Project.swift b/fixtures/ios_app_with_remote_swift_package/Project.swift index c618ef76424..68086b05b80 100644 --- a/fixtures/ios_app_with_remote_swift_package/Project.swift +++ b/fixtures/ios_app_with_remote_swift_package/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(url: "https://github.com/ReactiveX/RxSwift", .upToNextMajor(from: "5.0.0")), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/Info.plist", @@ -22,9 +22,9 @@ let project = Project( .package(product: "RxBlocking"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/ios_app_with_remote_swift_package/README.md b/fixtures/ios_app_with_remote_swift_package/README.md new file mode 100644 index 00000000000..14993460770 --- /dev/null +++ b/fixtures/ios_app_with_remote_swift_package/README.md @@ -0,0 +1,3 @@ +# iOS App with a remote Swift package + +An iOS application with a remote Swift package. \ No newline at end of file diff --git a/fixtures/ios_app_with_sdk/Framework/FrameworkClass.swift b/fixtures/ios_app_with_sdk/Framework/FrameworkClass.swift index 35939de4cb3..c5d120653d5 100644 --- a/fixtures/ios_app_with_sdk/Framework/FrameworkClass.swift +++ b/fixtures/ios_app_with_sdk/Framework/FrameworkClass.swift @@ -1,4 +1,5 @@ import CloudKit +import Observation import SQLite3 public class FrameworkClass { diff --git a/fixtures/ios_app_with_sdk/Modules/StaticFramework/Project.swift b/fixtures/ios_app_with_sdk/Modules/StaticFramework/Project.swift index 45708c1a6a3..6aece5d8b22 100644 --- a/fixtures/ios_app_with_sdk/Modules/StaticFramework/Project.swift +++ b/fixtures/ios_app_with_sdk/Modules/StaticFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFramework", targets: [ - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework", infoPlist: "Support/Info.plist", @@ -15,9 +15,9 @@ let project = Project( .sdk(name: "c++", type: .library), ] ), - Target( + .target( name: "StaticFrameworkTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.StaticFrameworkTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/ios_app_with_sdk/Project.swift b/fixtures/ios_app_with_sdk/Project.swift index 552a9cda806..ff1246908b7 100644 --- a/fixtures/ios_app_with_sdk/Project.swift +++ b/fixtures/ios_app_with_sdk/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Project", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/App-Info.plist", @@ -15,12 +15,13 @@ let project = Project( .sdk(name: "ARKit", type: .framework, status: .required), .sdk(name: "StoreKit", type: .framework, status: .optional), .sdk(name: "MobileCoreServices", type: .framework, status: .required), + .sdk(name: "Observation", type: .swiftLibrary), .project(target: "StaticFramework", path: "Modules/StaticFramework"), ] ), - Target( + .target( name: "MyTestFramework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.MyTestFramework", infoPlist: .default, @@ -29,9 +30,9 @@ let project = Project( .xctest, ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", @@ -41,9 +42,9 @@ let project = Project( .target(name: "MyTestFramework"), ] ), - Target( + .target( name: "MacFramework", - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "io.tuist.MacFramework", infoPlist: "Support/Framework-Info.plist", @@ -53,9 +54,9 @@ let project = Project( .sdk(name: "sqlite3", type: .library), ] ), - Target( + .target( name: "TVFramework", - platform: .tvOS, + destinations: [.appleTv], product: .framework, bundleId: "io.tuist.MacFramework", infoPlist: "Support/Framework-Info.plist", diff --git a/fixtures/ios_app_with_sdk/README.md b/fixtures/ios_app_with_sdk/README.md new file mode 100644 index 00000000000..fe15d894ce4 --- /dev/null +++ b/fixtures/ios_app_with_sdk/README.md @@ -0,0 +1,5 @@ +# iOS app with an SDK + +An application that contains an application target that depends on system libraries and frameworks (`.framework` and `.tbd`). + +One of the dependencies is declared as `.optional` i.e. will be linked weakly. \ No newline at end of file diff --git a/fixtures/ios_app_with_signing/Project.swift b/fixtures/ios_app_with_signing/Project.swift index dd6ee60e2b6..338c5bd97a6 100644 --- a/fixtures/ios_app_with_signing/Project.swift +++ b/fixtures/ios_app_with_signing/Project.swift @@ -8,9 +8,9 @@ let project = Project( name: "SignApp", settings: settings, targets: [ - Target( + .target( name: "SignApp", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", infoPlist: "Info.plist", @@ -21,9 +21,9 @@ let project = Project( .release(name: "Release", xcconfig: "ConfigurationFiles/Release.xcconfig"), ]) ), - Target( + .target( name: "AppA", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.test.appA", infoPlist: "Info.plist", @@ -33,9 +33,9 @@ let project = Project( .debug(name: "Debug", settings: ["PRODUCT_BUNDLE_IDENTIFIER": .string("io.tuist.test.appA")]), ]) ), - Target( + .target( name: "AppB", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.test.appB", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..13613e3ee1a --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/Contents.json b/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/App/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_spm_dependencies/App/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/fixtures/ios_app_with_spm_dependencies/App/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/App/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/ios_app_with_spm_dependencies/App/Sources/AppApp.swift b/fixtures/ios_app_with_spm_dependencies/App/Sources/AppApp.swift new file mode 100644 index 00000000000..1295f66aa3e --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/App/Sources/AppApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct AppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/ios_app_with_spm_dependencies/App/Sources/ContentView.swift b/fixtures/ios_app_with_spm_dependencies/App/Sources/ContentView.swift new file mode 100644 index 00000000000..64638708a26 --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/App/Sources/ContentView.swift @@ -0,0 +1,30 @@ +import Buy +import JWTKit +import KSCrash_Installations +import Pay +import SwiftUI + +struct ContentView: View { + init() { + // Use Mobile Buy SDK + _ = Card.CreditCard(firstName: "", lastName: "", number: "", expiryMonth: "", expiryYear: "") + _ = PayAddress() + // Use KSCrash + _ = KSCrashInstallationStandard() + // Use JWTKit + _ = JWTKit.ES256PrivateKey() + } + + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/fixtures/ios_app_with_spm_dependencies/AppTests/AppTests.swift b/fixtures/ios_app_with_spm_dependencies/AppTests/AppTests.swift new file mode 100644 index 00000000000..d62dc3d9072 --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/AppTests/AppTests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import App + +final class AppTests: XCTestCase { + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } +} diff --git a/fixtures/ios_app_with_spm_dependencies/Project.swift b/fixtures/ios_app_with_spm_dependencies/Project.swift new file mode 100644 index 00000000000..8d55b98f37d --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/Project.swift @@ -0,0 +1,34 @@ +import ProjectDescription + +let project = Project( + name: "App", + targets: [ + .target( + name: "App", + destinations: .iOS, + product: .app, + bundleId: "io.tuist.app", + deploymentTargets: .iOS("16.0"), + infoPlist: .default, + sources: "App/Sources/**", + resources: "App/Resources/**", + dependencies: [ + .external(name: "Buy"), + .external(name: "Pay"), + .external(name: "KSCrash"), + .external(name: "JWTKit"), + .sdk(name: "c++", type: .library, status: .required), + ] + ), + .target( + name: "AppTests", + destinations: .iOS, + product: .unitTests, + bundleId: "io.tuist.app.tests", + deploymentTargets: .iOS("16.0"), + infoPlist: .default, + sources: "AppTests/**", + dependencies: [.target(name: "App")] + ), + ] +) diff --git a/fixtures/ios_app_with_spm_dependencies/README.md b/fixtures/ios_app_with_spm_dependencies/README.md new file mode 100644 index 00000000000..2b052bf2b94 --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/README.md @@ -0,0 +1,3 @@ +# iOS app with SPM dependencies + +An iOS application project with various SPM dependencies. \ No newline at end of file diff --git a/fixtures/ios_app_with_spm_dependencies/Tuist/Package.resolved b/fixtures/ios_app_with_spm_dependencies/Tuist/Package.resolved new file mode 100644 index 00000000000..0933805b29f --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/Tuist/Package.resolved @@ -0,0 +1,69 @@ +{ + "originHash" : "1daeb9a853c51e17c138cf3bca0322cfb85340fe1fbf28fe3baeb52cd037df48", + "pins" : [ + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "6f512cb5a531e684f2e5238924be81db0b3303b4", + "version" : "5.0.0-beta.3" + } + }, + { + "identity" : "kscrash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kstenerud/KSCrash", + "state" : { + "revision" : "0ad825211ec38404e03b6f677a2312deb728528a", + "version" : "1.17.1" + } + }, + { + "identity" : "mobile-buy-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Shopify/mobile-buy-sdk-ios", + "state" : { + "revision" : "6bd23c2f6243f73ef6ec135723d4909bc068409e", + "version" : "12.0.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "c7e239b5c1492ffc3ebd7fbcc7a92548ce4e78f0", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "83640c8097acaec17c9835a083e89678cb0f2b66", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "bc1c29221f6dfeb0ebbfbc98eb95cd3d4967868e", + "version" : "3.4.0" + } + } + ], + "version" : 3 +} diff --git a/fixtures/ios_app_with_spm_dependencies/Tuist/Package.swift b/fixtures/ios_app_with_spm_dependencies/Tuist/Package.swift new file mode 100644 index 00000000000..c449bf33443 --- /dev/null +++ b/fixtures/ios_app_with_spm_dependencies/Tuist/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version: 5.10 +import PackageDescription + +let package = Package( + name: "PackageName", + dependencies: [ + // Has space symbols in package name + .package(url: "https://github.com/Shopify/mobile-buy-sdk-ios", exact: "12.0.0"), + // Has targets with slash symbols in their names + .package(url: "https://github.com/kstenerud/KSCrash", exact: "1.17.1"), + // Has custom `swiftSettings` and uses the package access level + .package(url: "https://github.com/vapor/jwt-kit.git", .upToNextMajor(from: "5.0.0-beta.2.1")), + ] +) diff --git a/fixtures/ios_app_with_static_frameworks/Modules/A/Project.swift b/fixtures/ios_app_with_static_frameworks/Modules/A/Project.swift index 1019595e028..787abbb9b8c 100755 --- a/fixtures/ios_app_with_static_frameworks/Modules/A/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Modules/A/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "A", targets: [ - Target( + .target( name: "A", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.A", infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( .framework(path: "../../Prebuilt/prebuilt/PrebuiltStaticFramework.framework"), ] ), - Target( + .target( name: "ATests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.ATests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks/Modules/AppTestsSupport/Project.swift b/fixtures/ios_app_with_static_frameworks/Modules/AppTestsSupport/Project.swift index 9a02c51e060..140c50e4086 100755 --- a/fixtures/ios_app_with_static_frameworks/Modules/AppTestsSupport/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Modules/AppTestsSupport/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "AppTestsSupport", targets: [ - Target( + .target( name: "AppTestsSupport", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.AppTestsSupport", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_static_frameworks/Modules/B/Project.swift b/fixtures/ios_app_with_static_frameworks/Modules/B/Project.swift index ecc657415af..5bc88261bf5 100755 --- a/fixtures/ios_app_with_static_frameworks/Modules/B/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Modules/B/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "B", targets: [ - Target( + .target( name: "B", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.B", infoPlist: "Info.plist", @@ -15,9 +15,9 @@ let project = Project( /* .framework(path: "framework") */ ] ), - Target( + .target( name: "BTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.BTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks/Modules/C/Project.swift b/fixtures/ios_app_with_static_frameworks/Modules/C/Project.swift index 0463a098178..67ac48e5930 100755 --- a/fixtures/ios_app_with_static_frameworks/Modules/C/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Modules/C/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "C", targets: [ - Target( + .target( name: "C", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.C", infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( .project(target: "D", path: "../D"), ] ), - Target( + .target( name: "CTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.CTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks/Modules/D/Project.swift b/fixtures/ios_app_with_static_frameworks/Modules/D/Project.swift index 428ffddbb94..8506bc75ed9 100755 --- a/fixtures/ios_app_with_static_frameworks/Modules/D/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Modules/D/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "D", targets: [ - Target( + .target( name: "D", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.D", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_static_frameworks/Prebuilt/Project.swift b/fixtures/ios_app_with_static_frameworks/Prebuilt/Project.swift index 06ed66799ac..c5f93a2166d 100644 --- a/fixtures/ios_app_with_static_frameworks/Prebuilt/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Prebuilt/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Prebuilt", targets: [ - Target( + .target( name: "PrebuiltStaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.PrebuiltStaticFramework", infoPlist: "Config/Info.plist", diff --git a/fixtures/ios_app_with_static_frameworks/Project.swift b/fixtures/ios_app_with_static_frameworks/Project.swift index 3cfb95c3476..2ea27419a9b 100755 --- a/fixtures/ios_app_with_static_frameworks/Project.swift +++ b/fixtures/ios_app_with_static_frameworks/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( .framework(path: "Prebuilt/prebuilt/PrebuiltStaticFramework.framework"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks/README.md b/fixtures/ios_app_with_static_frameworks/README.md new file mode 100644 index 00000000000..903637fbeb7 --- /dev/null +++ b/fixtures/ios_app_with_static_frameworks/README.md @@ -0,0 +1,41 @@ +# iOS app with static frameworks + + +This fixture contains an application that depends on static frameworks, both directly and transitively. + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - Modules + - A: + - A (static framework iOS) + - ATests (iOS unit tests) + - B: + - B (static framework iOS) + - BTests (iOS unit tests) + - C: + - C (static framework iOS) + - CTests (iOS unit tests) + - D: + - D (dynamic framework iOS) +``` + +A standalone `Prebuilt` project is used to generate a prebuilt static framework: + +``` +- Prebuilt + - PrebuiltStaticFramework (static framework iOS) +``` + +Dependencies: + +- App -> A +- App -> C +- App -> PrebuiltStaticFramework +- A -> B +- A -> C +- C -> D + +Note: to re-create `PrebuiltStaticFramework.framework` run `ios_app_with_static_frameworks/Prebuilt//build.sh` \ No newline at end of file diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/A/Project.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/A/Project.swift index 9464b27c5de..14e97a9d01d 100755 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/A/Project.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/A/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "A", targets: [ - Target( + .target( name: "A", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.A", infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( .project(target: "C", path: "../C"), ] ), - Target( + .target( name: "ATests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.ATests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/B/Project.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/B/Project.swift index a1b620f6f6c..a3b7a66767b 100755 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/B/Project.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/B/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "B", targets: [ - Target( + .target( name: "B", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.B", infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( /* .framework(path: "framework") */ ] ), - Target( + .target( name: "BTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.BTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Project.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Project.swift index 2008d15b01c..55af3f0722c 100755 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Project.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "C", targets: [ - Target( + .target( name: "C", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.C", infoPlist: "Info.plist", @@ -17,9 +17,9 @@ let project = Project( .project(target: "D", path: "../D"), ] ), - Target( + .target( name: "CTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.CTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/D/Project.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/D/Project.swift index 428ffddbb94..8506bc75ed9 100755 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/D/Project.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/D/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "D", targets: [ - Target( + .target( name: "D", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.D", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Prebuilt/Project.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Prebuilt/Project.swift index 06ed66799ac..c5f93a2166d 100644 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Prebuilt/Project.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Prebuilt/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Prebuilt", targets: [ - Target( + .target( name: "PrebuiltStaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.PrebuiltStaticFramework", infoPlist: "Config/Info.plist", diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Project.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Project.swift index 69325a782c9..33d4934060f 100755 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Project.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -21,9 +21,9 @@ let project = Project( ] ) ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_libraries/Modules/A/Project.swift b/fixtures/ios_app_with_static_libraries/Modules/A/Project.swift index 02f4102c5c9..9dfa3e4abe8 100755 --- a/fixtures/ios_app_with_static_libraries/Modules/A/Project.swift +++ b/fixtures/ios_app_with_static_libraries/Modules/A/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "A", targets: [ - Target( + .target( name: "A", - platform: .iOS, + destinations: .iOS, product: .staticLibrary, bundleId: "io.tuist.A", infoPlist: nil, @@ -21,9 +21,9 @@ let project = Project( settings: .settings(base: ["HEADER_SEARCH_PATHS": "$(SRCROOT)/CustomHeaders"]) ), - Target( + .target( name: "ATests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.ATests", infoPlist: nil, diff --git a/fixtures/ios_app_with_static_libraries/Modules/B/Project.swift b/fixtures/ios_app_with_static_libraries/Modules/B/Project.swift index cc6ce0c9b32..50e3d978760 100755 --- a/fixtures/ios_app_with_static_libraries/Modules/B/Project.swift +++ b/fixtures/ios_app_with_static_libraries/Modules/B/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "B", targets: [ - Target( + .target( name: "B", - platform: .iOS, + destinations: .iOS, product: .staticLibrary, bundleId: "io.tuist.B", infoPlist: "Info.plist", @@ -15,9 +15,9 @@ let project = Project( /* .framework(path: "framework") */ ] ), - Target( + .target( name: "BTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.BTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_libraries/Modules/C/Project.swift b/fixtures/ios_app_with_static_libraries/Modules/C/Project.swift index 10d389623c2..c78a12499bb 100755 --- a/fixtures/ios_app_with_static_libraries/Modules/C/Project.swift +++ b/fixtures/ios_app_with_static_libraries/Modules/C/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "C", targets: [ - Target( + .target( name: "C", - platform: .iOS, + destinations: .iOS, product: .staticLibrary, bundleId: "io.tuist.C", infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( ], settings: .settings(base: ["BUILD_LIBRARY_FOR_DISTRIBUTION": "YES"]) ), - Target( + .target( name: "CTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.BTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_libraries/Project.swift b/fixtures/ios_app_with_static_libraries/Project.swift index 3affffdb243..fb70657a3c9 100755 --- a/fixtures/ios_app_with_static_libraries/Project.swift +++ b/fixtures/ios_app_with_static_libraries/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "iOSAppWithTransistiveStaticLibraries", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -14,9 +14,9 @@ let project = Project( .project(target: "A", path: "Modules/A"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_static_libraries/README.md b/fixtures/ios_app_with_static_libraries/README.md new file mode 100644 index 00000000000..9832024700e --- /dev/null +++ b/fixtures/ios_app_with_static_libraries/README.md @@ -0,0 +1,33 @@ +# iOS app with static libraries + + +This application provides a top level application with two static library dependencies. The first static library dependency has another static library dependency so that we are able to test how Tuist handles the transitiveness of the static libraries in the linked frameworks of the main app. + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - A: + - A (static library iOS) + - ATests (iOS unit tests) + - B: + - B (static library iOS) + - BTests (iOS unit tests) +``` + +A standalone C project is used to generate a prebuilt static library: + +``` + - C: + - C (static library iOS) + - CTests (iOS unit tests) +``` + +Dependencies: + +- App -> A +- A -> B +- A -> prebuild C (libC.a) + +Note: to re-create `libC.a` run `ios_app_with_static_libraries/Modules/C/build.sh` \ No newline at end of file diff --git a/fixtures/ios_app_with_static_library_and_package/Prebuilt/Project.swift b/fixtures/ios_app_with_static_library_and_package/Prebuilt/Project.swift index c7fd31b7165..018deee1803 100644 --- a/fixtures/ios_app_with_static_library_and_package/Prebuilt/Project.swift +++ b/fixtures/ios_app_with_static_library_and_package/Prebuilt/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "../Packages/PackageA"), ], targets: [ - Target( + .target( name: "PrebuiltStaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.PrebuiltStaticFramework", infoPlist: "Config/Info.plist", diff --git a/fixtures/ios_app_with_static_library_and_package/Project.swift b/fixtures/ios_app_with_static_library_and_package/Project.swift index 26298dcb71c..c6cb700d6cb 100644 --- a/fixtures/ios_app_with_static_library_and_package/Project.swift +++ b/fixtures/ios_app_with_static_library_and_package/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "Packages/PackageA"), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/Info.plist", @@ -22,9 +22,9 @@ let project = Project( .package(product: "LibraryA"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/ios_app_with_static_library_and_package/README.md b/fixtures/ios_app_with_static_library_and_package/README.md new file mode 100644 index 00000000000..5213863c2e4 --- /dev/null +++ b/fixtures/ios_app_with_static_library_and_package/README.md @@ -0,0 +1,5 @@ +# iOS app with a static library and a package + +An iOS application that depends on static library that depends on Swift package where static library is defined first. + +Note: to re-create `PrebuiltStaticFramework.framework` run `ios_app_with_static_library_and_package/Prebuilt/build.sh` \ No newline at end of file diff --git a/fixtures/ios_app_with_templates/Project.swift b/fixtures/ios_app_with_templates/Project.swift index b554efdec4e..8b2101e4bed 100644 --- a/fixtures/ios_app_with_templates/Project.swift +++ b/fixtures/ios_app_with_templates/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/Info.plist", @@ -15,9 +15,9 @@ let project = Project( // "Resources/**" ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/ios_app_with_templates/Tuist/Templates/custom/custom.swift b/fixtures/ios_app_with_templates/Tuist/Templates/custom/custom.swift index 00b94d933f4..f6cafec36f9 100644 --- a/fixtures/ios_app_with_templates/Tuist/Templates/custom/custom.swift +++ b/fixtures/ios_app_with_templates/Tuist/Templates/custom/custom.swift @@ -1,7 +1,7 @@ import ProjectDescription let nameAttribute: Template.Attribute = .required("name") -let platformAttribute: Template.Attribute = .optional("platform", default: "ios") +let platformAttribute: Template.Attribute = .optional("platform", default: .string("ios")) let testContents = """ // this is test \(nameAttribute) content diff --git a/fixtures/ios_app_with_templates/Tuist/Templates/custom_using_attribute/custom_using_attribute.swift b/fixtures/ios_app_with_templates/Tuist/Templates/custom_using_attribute/custom_using_attribute.swift new file mode 100644 index 00000000000..1007e9f7781 --- /dev/null +++ b/fixtures/ios_app_with_templates/Tuist/Templates/custom_using_attribute/custom_using_attribute.swift @@ -0,0 +1,24 @@ +import ProjectDescription + +let nameAttribute: Template.Attribute = .required("name") +let platformAttribute: Template.Attribute = .optional("platforms", default: [ + "iOS": true, + "macOS": false, + "watchOS": false, +]) + +let testContents = """ +// this is test \(nameAttribute) content +""" + +let template = Template( + description: "Custom template", + attributes: [ + nameAttribute, + platformAttribute, + ], + items: [ + .string(path: "\(nameAttribute)/custom.swift", contents: testContents), + .file(path: "\(nameAttribute)/generated.swift", templatePath: "platform.stencil"), + ] +) diff --git a/fixtures/ios_app_with_templates/Tuist/Templates/custom_using_attribute/platform.stencil b/fixtures/ios_app_with_templates/Tuist/Templates/custom_using_attribute/platform.stencil new file mode 100644 index 00000000000..fe1eeb642d9 --- /dev/null +++ b/fixtures/ios_app_with_templates/Tuist/Templates/custom_using_attribute/platform.stencil @@ -0,0 +1,7 @@ +// Generated file name: {{ name }} +// Generated file with supporting platforms +{% for key, value in platforms %} + {% if value %} +// {{ key }} + {% endif %} +{% endfor %} \ No newline at end of file diff --git a/fixtures/ios_app_with_tests/Project.swift b/fixtures/ios_app_with_tests/Project.swift index 3315acfa0fa..2e367f2cf64 100644 --- a/fixtures/ios_app_with_tests/Project.swift +++ b/fixtures/ios_app_with_tests/Project.swift @@ -11,30 +11,30 @@ let project = Project( ) ), targets: [ - Target( + .target( name: "AppCore", - platform: .iOS, + destinations: [.iPhone], product: .framework, bundleId: "io.tuist.AppCore", - deploymentTarget: .iOS(targetVersion: "12.0", devices: .iphone), + deploymentTargets: .iOS("12.0"), infoPlist: .default, sources: .paths([.relativeToManifest("AppCore/Sources/**")]) ), - Target( + .target( name: "AppCoreTests", - platform: .iOS, + destinations: [.iPhone], product: .unitTests, bundleId: "io.tuist.AppCoreTests", - deploymentTarget: .iOS(targetVersion: "12.0", devices: .iphone), + deploymentTargets: .iOS("12.0"), infoPlist: "Tests.plist", sources: "AppCore/Tests/**", dependencies: [ .target(name: "AppCore"), ] ), - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: .file(path: .relativeToManifest("Info.plist")), @@ -47,9 +47,9 @@ let project = Project( "CODE_SIGNING_REQUIRED": "NO", ]) ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Tests.plist", @@ -62,12 +62,12 @@ let project = Project( "CODE_SIGNING_REQUIRED": "NO", ]) ), - Target( + .target( name: "MacFramework", - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "io.tuist.MacFramework", - deploymentTarget: .macOS(targetVersion: "10.15"), + deploymentTargets: .macOS("10.15"), infoPlist: .file(path: .relativeToManifest("Info.plist")), sources: .paths([.relativeToManifest("MacFramework/Sources/**")]), settings: .settings(base: [ @@ -75,12 +75,12 @@ let project = Project( "CODE_SIGNING_REQUIRED": "NO", ]) ), - Target( + .target( name: "MacFrameworkTests", - platform: .macOS, + destinations: [.mac], product: .unitTests, bundleId: "io.tuist.MacFrameworkTests", - deploymentTarget: .macOS(targetVersion: "10.15"), + deploymentTargets: .macOS("10.15"), infoPlist: "Tests.plist", sources: "MacFramework/Tests/**", dependencies: [ @@ -91,9 +91,9 @@ let project = Project( "CODE_SIGNING_REQUIRED": "NO", ]) ), - Target( + .target( name: "AppUITests", - platform: .iOS, + destinations: .iOS, product: .uiTests, bundleId: "io.tuist.AppUITests", infoPlist: "Tests.plist", @@ -102,9 +102,9 @@ let project = Project( .target(name: "App"), ] ), - Target( + .target( name: "App-dash", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.AppDash", infoPlist: "Info.plist", @@ -118,9 +118,9 @@ let project = Project( "CODE_SIGNING_REQUIRED": "NO", ]) ), - Target( + .target( name: "App-dashUITests", - platform: .iOS, + destinations: .iOS, product: .uiTests, bundleId: "io.tuist.AppDashUITests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_tests/README.md b/fixtures/ios_app_with_tests/README.md new file mode 100644 index 00000000000..c3edb1919b2 --- /dev/null +++ b/fixtures/ios_app_with_tests/README.md @@ -0,0 +1,3 @@ +# iOS app with tests + +A simple iOS app with tests, which includes a setup manifest and uses `.notGrouped` autogenerated schemes. \ No newline at end of file diff --git a/fixtures/ios_app_with_transitive_framework/App/Project.swift b/fixtures/ios_app_with_transitive_framework/App/Project.swift index d15f742e05c..7cdc60c854b 100644 --- a/fixtures/ios_app_with_transitive_framework/App/Project.swift +++ b/fixtures/ios_app_with_transitive_framework/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MainApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -14,9 +14,9 @@ let project = Project( .project(target: "Framework1-iOS", path: "../Framework1"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", @@ -25,9 +25,9 @@ let project = Project( .target(name: "App"), ] ), - Target( + .target( name: "AppUITests", - platform: .iOS, + destinations: .iOS, product: .uiTests, bundleId: "io.tuist.AppUITests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_transitive_framework/Framework1/Project.swift b/fixtures/ios_app_with_transitive_framework/Framework1/Project.swift index 686ad0e9525..9628d4d53cc 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework1/Project.swift +++ b/fixtures/ios_app_with_transitive_framework/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1-iOS", - platform: .iOS, + destinations: .iOS, product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -15,9 +15,9 @@ let project = Project( .framework(path: "../Framework2/prebuilt/iOS/Framework2.framework"), ] ), - Target( + .target( name: "Framework1-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -27,9 +27,9 @@ let project = Project( .framework(path: "../Framework2/prebuilt/Mac/Framework2.framework"), ] ), - Target( + .target( name: "Framework1Tests-iOS", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Tests/Info.plist", @@ -38,9 +38,9 @@ let project = Project( .target(name: "Framework1-iOS"), ] ), - Target( + .target( name: "Framework1Tests-macOS", - platform: .macOS, + destinations: [.mac], product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Tests/Info.plist", diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/Project.swift b/fixtures/ios_app_with_transitive_framework/Framework2/Project.swift index 5865a221e64..f477c081a0b 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/Project.swift +++ b/fixtures/ios_app_with_transitive_framework/Framework2/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework2", targets: [ - Target( + .target( name: "Framework2-iOS", - platform: .iOS, + destinations: .iOS, product: .framework, productName: "Framework2", bundleId: "io.tuist.Framework2", @@ -15,9 +15,9 @@ let project = Project( ], settings: .settings(base: ["BUILD_LIBRARY_FOR_DISTRIBUTION": "YES"]) ), - Target( + .target( name: "Framework2-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "Framework2", bundleId: "io.tuist.Framework2", diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/build.sh b/fixtures/ios_app_with_transitive_framework/Framework2/build.sh index ccbf765e43c..a12e6fcacac 100755 --- a/fixtures/ios_app_with_transitive_framework/Framework2/build.sh +++ b/fixtures/ios_app_with_transitive_framework/Framework2/build.sh @@ -3,14 +3,14 @@ rm -rf prebuild tuist generate --no-open -TEMP_DIR="/tmp/tuist-framework-2-fixture" +TEMP_DIR=/private$(mktemp -d) IPHONE_SIM_DIR="$TEMP_DIR/Build/Products/Debug-iphonesimulator" MAC_OS_DIR="$TEMP_DIR/Build/Products/Debug" -rm -rf $TEMP_DIR -mkdir -p $TEMP_DIR +echo $TEMP_DIR +trap "rm -rf $TEMP_DIR" EXIT # Ensures it gets deleted -xcrun xcodebuild build -scheme Framework2-iOS -workspace Framework2.xcworkspace -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 14 Pro,OS=latest" -derivedDataPath $TEMP_DIR ONLY_ACTIVE_ARCH=NO +xcrun xcodebuild build -scheme Framework2-iOS -workspace Framework2.xcworkspace -destination generic/platform=iOS -destination generic/platform=iOS\ Simulator -derivedDataPath $TEMP_DIR ONLY_ACTIVE_ARCH=NO xcrun xcodebuild build -scheme Framework2-macOS -workspace Framework2.xcworkspace -derivedDataPath $TEMP_DIR mkdir -p prebuilt/iOS/Framework2.framework diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Framework2 b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Framework2 index f4901b49998..0189f66a91a 100755 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Framework2 and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Framework2 differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Headers/Framework2-Swift.h b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Headers/Framework2-Swift.h index 46ec08501b5..563bd8684b0 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Headers/Framework2-Swift.h +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Headers/Framework2-Swift.h @@ -1,6 +1,6 @@ #if 0 -#elif defined(__x86_64__) && __x86_64__ -// Generated by Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) +#elif defined(__arm64__) && __arm64__ +// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) #ifndef FRAMEWORK2_SWIFT_H #define FRAMEWORK2_SWIFT_H #pragma clang diagnostic push @@ -23,7 +23,6 @@ # include #endif -#pragma clang diagnostic ignored "-Wduplicate-method-match" #pragma clang diagnostic ignored "-Wauto-import" #if defined(__OBJC__) #include @@ -32,10 +31,30 @@ #include #include #include +#include +#include +#include +#include #else #include #include #include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif #endif #if !defined(SWIFT_TYPEDEFS) @@ -71,53 +90,66 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); # if __has_feature(objc_class_property) # define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ # else -# define SWIFT_CLASS_PROPERTY(...) +# define SWIFT_CLASS_PROPERTY(...) # endif #endif - -#if __has_attribute(objc_runtime_name) -# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) -#else -# define SWIFT_RUNTIME_NAME(X) +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif #endif -#if __has_attribute(swift_name) -# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) -#else -# define SWIFT_COMPILE_NAME(X) +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif #endif -#if __has_attribute(objc_method_family) -# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) -#else -# define SWIFT_METHOD_FAMILY(X) +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif #endif -#if __has_attribute(noescape) -# define SWIFT_NOESCAPE __attribute__((noescape)) -#else -# define SWIFT_NOESCAPE +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif #endif -#if __has_attribute(ns_consumed) -# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) -#else -# define SWIFT_RELEASES_ARGUMENT +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif #endif -#if __has_attribute(warn_unused_result) -# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) -#else -# define SWIFT_WARN_UNUSED_RESULT +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif #endif -#if __has_attribute(noreturn) -# define SWIFT_NORETURN __attribute__((noreturn)) -#else -# define SWIFT_NORETURN +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif #endif #if !defined(SWIFT_CLASS_EXTRA) -# define SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_EXTRA #endif #if !defined(SWIFT_PROTOCOL_EXTRA) -# define SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_EXTRA #endif #if !defined(SWIFT_ENUM_EXTRA) -# define SWIFT_ENUM_EXTRA +# define SWIFT_ENUM_EXTRA #endif #if !defined(SWIFT_CLASS) # if __has_attribute(objc_subclassing_restricted) @@ -137,28 +169,25 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); # define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) # endif #endif - #if !defined(SWIFT_PROTOCOL) # define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA # define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA #endif - #if !defined(SWIFT_EXTENSION) # define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) #endif - #if !defined(OBJC_DESIGNATED_INITIALIZER) # if __has_attribute(objc_designated_initializer) # define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) # else -# define OBJC_DESIGNATED_INITIALIZER +# define OBJC_DESIGNATED_INITIALIZER # endif #endif #if !defined(SWIFT_ENUM_ATTR) -# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# if __has_attribute(enum_extensibility) # define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) # else -# define SWIFT_ENUM_ATTR(_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) # endif #endif #if !defined(SWIFT_ENUM) @@ -187,14 +216,16 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #if !defined(SWIFT_DEPRECATED_MSG) # define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) #endif -#if __has_feature(attribute_diagnose_if_objc) -# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) -#else -# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif #endif #if defined(__OBJC__) #if !defined(IBSegueAction) -# define IBSegueAction +# define IBSegueAction #endif #endif #if !defined(SWIFT_EXTERN) @@ -207,26 +238,42 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #if !defined(SWIFT_CALL) # define SWIFT_CALL __attribute__((swiftcall)) #endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif #if defined(__cplusplus) -#if !defined(SWIFT_NOEXCEPT) # define SWIFT_NOEXCEPT noexcept -#endif #else -#if !defined(SWIFT_NOEXCEPT) # define SWIFT_NOEXCEPT #endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif #endif -#if defined(__cplusplus) -#if !defined(SWIFT_CXX_INT_DEFINED) -#define SWIFT_CXX_INT_DEFINED -namespace swift { -using Int = ptrdiff_t; -using UInt = size_t; -} +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL #endif #endif #if defined(__OBJC__) -#if __has_feature(modules) +#if __has_feature(objc_modules) #if __has_warning("-Watimport-in-framework-header") #pragma clang diagnostic ignored "-Watimport-in-framework-header" #endif @@ -251,11 +298,11 @@ using UInt = size_t; #if defined(__OBJC__) #endif -#if defined(__cplusplus) -#endif #if __has_attribute(external_source_symbol) # pragma clang attribute pop #endif +#if defined(__cplusplus) +#endif #pragma clang diagnostic pop #endif diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Info.plist b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Info.plist index a0991779f9e..08d45571ba2 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Info.plist +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Info.plist @@ -3,7 +3,7 @@ BuildMachineOSBuild - 22E261 + 23C71 CFBundleDevelopmentRegion en CFBundleExecutable @@ -27,21 +27,21 @@ DTCompiler com.apple.compilers.llvm.clang.1_0 DTPlatformBuild - 14C18 + DTPlatformName macosx DTPlatformVersion - 13.1 + 14.2 DTSDKBuild - 22C55 + 23C53 DTSDKName - macosx13.1 + macosx14.2 DTXcode - 1420 + 1520 DTXcodeBuild - 14C18 + 15C500b LSMinimumSystemVersion - 13.1 + 14.2 NSHumanReadableCopyright Copyright Tuist©. All rights reserved. diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo index 0cbfad9637e..9674059f0c0 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.private.swiftinterface b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.private.swiftinterface index bf4ced477df..9b632896c17 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.private.swiftinterface +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.private.swiftinterface @@ -1,11 +1,12 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) -// swift-module-flags: -target arm64-apple-macos13.1 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 +// swift-compiler-version: Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) +// swift-module-flags: -target arm64-apple-macos14.2 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 // swift-module-flags-ignorable: -enable-bare-slash-regex import Foundation import Swift import _Concurrency import _StringProcessing +import _SwiftConcurrencyShims public class Framework2File { public init() public func hello() -> Swift.String diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftdoc b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftdoc index 330ca146f1d..5618c6a78e5 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftdoc and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftdoc differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftinterface b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftinterface index bf4ced477df..9b632896c17 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftinterface +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftinterface @@ -1,11 +1,12 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) -// swift-module-flags: -target arm64-apple-macos13.1 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 +// swift-compiler-version: Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) +// swift-module-flags: -target arm64-apple-macos14.2 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 // swift-module-flags-ignorable: -enable-bare-slash-regex import Foundation import Swift import _Concurrency import _StringProcessing +import _SwiftConcurrencyShims public class Framework2File { public init() public func hello() -> Swift.String diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftmodule b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftmodule index f909f21416e..bd827560686 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftmodule and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/Mac/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-macos.swiftmodule differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Framework2 b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Framework2 index 7bb5fe927bd..9f4e3eb8933 100755 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Framework2 and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Framework2 differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Headers/Framework2-Swift.h b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Headers/Framework2-Swift.h index 5cc89b42271..d4d6c9db6b5 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Headers/Framework2-Swift.h +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Headers/Framework2-Swift.h @@ -1,6 +1,6 @@ #if 0 #elif defined(__arm64__) && __arm64__ -// Generated by Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) +// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) #ifndef FRAMEWORK2_SWIFT_H #define FRAMEWORK2_SWIFT_H #pragma clang diagnostic push @@ -23,7 +23,6 @@ # include #endif -#pragma clang diagnostic ignored "-Wduplicate-method-match" #pragma clang diagnostic ignored "-Wauto-import" #if defined(__OBJC__) #include @@ -32,10 +31,30 @@ #include #include #include +#include +#include +#include +#include #else #include #include #include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif #endif #if !defined(SWIFT_TYPEDEFS) @@ -71,53 +90,66 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); # if __has_feature(objc_class_property) # define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ # else -# define SWIFT_CLASS_PROPERTY(...) +# define SWIFT_CLASS_PROPERTY(...) # endif #endif - -#if __has_attribute(objc_runtime_name) -# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) -#else -# define SWIFT_RUNTIME_NAME(X) +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif #endif -#if __has_attribute(swift_name) -# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) -#else -# define SWIFT_COMPILE_NAME(X) +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif #endif -#if __has_attribute(objc_method_family) -# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) -#else -# define SWIFT_METHOD_FAMILY(X) +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif #endif -#if __has_attribute(noescape) -# define SWIFT_NOESCAPE __attribute__((noescape)) -#else -# define SWIFT_NOESCAPE +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif #endif -#if __has_attribute(ns_consumed) -# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) -#else -# define SWIFT_RELEASES_ARGUMENT +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif #endif -#if __has_attribute(warn_unused_result) -# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) -#else -# define SWIFT_WARN_UNUSED_RESULT +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif #endif -#if __has_attribute(noreturn) -# define SWIFT_NORETURN __attribute__((noreturn)) -#else -# define SWIFT_NORETURN +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif #endif #if !defined(SWIFT_CLASS_EXTRA) -# define SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_EXTRA #endif #if !defined(SWIFT_PROTOCOL_EXTRA) -# define SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_EXTRA #endif #if !defined(SWIFT_ENUM_EXTRA) -# define SWIFT_ENUM_EXTRA +# define SWIFT_ENUM_EXTRA #endif #if !defined(SWIFT_CLASS) # if __has_attribute(objc_subclassing_restricted) @@ -137,28 +169,25 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); # define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) # endif #endif - #if !defined(SWIFT_PROTOCOL) # define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA # define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA #endif - #if !defined(SWIFT_EXTENSION) # define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) #endif - #if !defined(OBJC_DESIGNATED_INITIALIZER) # if __has_attribute(objc_designated_initializer) # define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) # else -# define OBJC_DESIGNATED_INITIALIZER +# define OBJC_DESIGNATED_INITIALIZER # endif #endif #if !defined(SWIFT_ENUM_ATTR) -# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# if __has_attribute(enum_extensibility) # define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) # else -# define SWIFT_ENUM_ATTR(_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) # endif #endif #if !defined(SWIFT_ENUM) @@ -187,14 +216,16 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #if !defined(SWIFT_DEPRECATED_MSG) # define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) #endif -#if __has_feature(attribute_diagnose_if_objc) -# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) -#else -# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif #endif #if defined(__OBJC__) #if !defined(IBSegueAction) -# define IBSegueAction +# define IBSegueAction #endif #endif #if !defined(SWIFT_EXTERN) @@ -207,26 +238,42 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #if !defined(SWIFT_CALL) # define SWIFT_CALL __attribute__((swiftcall)) #endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif #if defined(__cplusplus) -#if !defined(SWIFT_NOEXCEPT) # define SWIFT_NOEXCEPT noexcept -#endif #else -#if !defined(SWIFT_NOEXCEPT) # define SWIFT_NOEXCEPT #endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif #endif -#if defined(__cplusplus) -#if !defined(SWIFT_CXX_INT_DEFINED) -#define SWIFT_CXX_INT_DEFINED -namespace swift { -using Int = ptrdiff_t; -using UInt = size_t; -} +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL #endif #endif #if defined(__OBJC__) -#if __has_feature(modules) +#if __has_feature(objc_modules) #if __has_warning("-Watimport-in-framework-header") #pragma clang diagnostic ignored "-Watimport-in-framework-header" #endif @@ -251,16 +298,16 @@ using UInt = size_t; #if defined(__OBJC__) #endif -#if defined(__cplusplus) -#endif #if __has_attribute(external_source_symbol) # pragma clang attribute pop #endif +#if defined(__cplusplus) +#endif #pragma clang diagnostic pop #endif #elif defined(__x86_64__) && __x86_64__ -// Generated by Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) +// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) #ifndef FRAMEWORK2_SWIFT_H #define FRAMEWORK2_SWIFT_H #pragma clang diagnostic push @@ -283,7 +330,6 @@ using UInt = size_t; # include #endif -#pragma clang diagnostic ignored "-Wduplicate-method-match" #pragma clang diagnostic ignored "-Wauto-import" #if defined(__OBJC__) #include @@ -292,10 +338,30 @@ using UInt = size_t; #include #include #include +#include +#include +#include +#include #else #include #include #include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif #endif #if !defined(SWIFT_TYPEDEFS) @@ -331,53 +397,66 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); # if __has_feature(objc_class_property) # define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ # else -# define SWIFT_CLASS_PROPERTY(...) +# define SWIFT_CLASS_PROPERTY(...) # endif #endif - -#if __has_attribute(objc_runtime_name) -# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) -#else -# define SWIFT_RUNTIME_NAME(X) +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif #endif -#if __has_attribute(swift_name) -# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) -#else -# define SWIFT_COMPILE_NAME(X) +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif #endif -#if __has_attribute(objc_method_family) -# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) -#else -# define SWIFT_METHOD_FAMILY(X) +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif #endif -#if __has_attribute(noescape) -# define SWIFT_NOESCAPE __attribute__((noescape)) -#else -# define SWIFT_NOESCAPE +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif #endif -#if __has_attribute(ns_consumed) -# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) -#else -# define SWIFT_RELEASES_ARGUMENT +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif #endif -#if __has_attribute(warn_unused_result) -# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) -#else -# define SWIFT_WARN_UNUSED_RESULT +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif #endif -#if __has_attribute(noreturn) -# define SWIFT_NORETURN __attribute__((noreturn)) -#else -# define SWIFT_NORETURN +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif #endif #if !defined(SWIFT_CLASS_EXTRA) -# define SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_EXTRA #endif #if !defined(SWIFT_PROTOCOL_EXTRA) -# define SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_EXTRA #endif #if !defined(SWIFT_ENUM_EXTRA) -# define SWIFT_ENUM_EXTRA +# define SWIFT_ENUM_EXTRA #endif #if !defined(SWIFT_CLASS) # if __has_attribute(objc_subclassing_restricted) @@ -397,28 +476,25 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); # define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) # endif #endif - #if !defined(SWIFT_PROTOCOL) # define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA # define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA #endif - #if !defined(SWIFT_EXTENSION) # define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) #endif - #if !defined(OBJC_DESIGNATED_INITIALIZER) # if __has_attribute(objc_designated_initializer) # define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) # else -# define OBJC_DESIGNATED_INITIALIZER +# define OBJC_DESIGNATED_INITIALIZER # endif #endif #if !defined(SWIFT_ENUM_ATTR) -# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# if __has_attribute(enum_extensibility) # define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) # else -# define SWIFT_ENUM_ATTR(_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) # endif #endif #if !defined(SWIFT_ENUM) @@ -447,14 +523,16 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #if !defined(SWIFT_DEPRECATED_MSG) # define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) #endif -#if __has_feature(attribute_diagnose_if_objc) -# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) -#else -# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif #endif #if defined(__OBJC__) #if !defined(IBSegueAction) -# define IBSegueAction +# define IBSegueAction #endif #endif #if !defined(SWIFT_EXTERN) @@ -467,26 +545,42 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #if !defined(SWIFT_CALL) # define SWIFT_CALL __attribute__((swiftcall)) #endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif #if defined(__cplusplus) -#if !defined(SWIFT_NOEXCEPT) # define SWIFT_NOEXCEPT noexcept -#endif #else -#if !defined(SWIFT_NOEXCEPT) # define SWIFT_NOEXCEPT #endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif #endif -#if defined(__cplusplus) -#if !defined(SWIFT_CXX_INT_DEFINED) -#define SWIFT_CXX_INT_DEFINED -namespace swift { -using Int = ptrdiff_t; -using UInt = size_t; -} +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL #endif #endif #if defined(__OBJC__) -#if __has_feature(modules) +#if __has_feature(objc_modules) #if __has_warning("-Watimport-in-framework-header") #pragma clang diagnostic ignored "-Watimport-in-framework-header" #endif @@ -511,11 +605,11 @@ using UInt = size_t; #if defined(__OBJC__) #endif -#if defined(__cplusplus) -#endif #if __has_attribute(external_source_symbol) # pragma clang attribute pop #endif +#if defined(__cplusplus) +#endif #pragma clang diagnostic pop #endif diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Info.plist b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Info.plist index 01865eed0ac..440a48af5e5 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Info.plist and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Info.plist differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo index 40f93618ff4..604e30d67cd 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo index ba9a4de7fc4..0f4eaa674e7 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface index 346639174d2..df83246e406 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface @@ -1,11 +1,12 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) -// swift-module-flags: -target arm64-apple-ios16.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 +// swift-compiler-version: Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) +// swift-module-flags: -target arm64-apple-ios17.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 // swift-module-flags-ignorable: -enable-bare-slash-regex import Foundation import Swift import _Concurrency import _StringProcessing +import _SwiftConcurrencyShims public class Framework2File { public init() public func hello() -> Swift.String diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftdoc b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftdoc index 00533eba208..1ed091506ac 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftdoc and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftdoc differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftinterface b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftinterface index 346639174d2..df83246e406 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftinterface +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftinterface @@ -1,11 +1,12 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) -// swift-module-flags: -target arm64-apple-ios16.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 +// swift-compiler-version: Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) +// swift-module-flags: -target arm64-apple-ios17.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 // swift-module-flags-ignorable: -enable-bare-slash-regex import Foundation import Swift import _Concurrency import _StringProcessing +import _SwiftConcurrencyShims public class Framework2File { public init() public func hello() -> Swift.String diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftmodule b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftmodule index e8e1adac3b1..4b07dfa8e08 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftmodule and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/arm64-apple-ios-simulator.swiftmodule differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface index 4bf09e24e54..7c4d9572ca6 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface @@ -1,11 +1,12 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) -// swift-module-flags: -target x86_64-apple-ios16.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 +// swift-compiler-version: Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) +// swift-module-flags: -target x86_64-apple-ios17.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 // swift-module-flags-ignorable: -enable-bare-slash-regex import Foundation import Swift import _Concurrency import _StringProcessing +import _SwiftConcurrencyShims public class Framework2File { public init() public func hello() -> Swift.String diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftdoc index acb7cacf69d..8a6e1edfa88 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftdoc and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftdoc differ diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftinterface b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftinterface index 4bf09e24e54..7c4d9572ca6 100644 --- a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +++ b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftinterface @@ -1,11 +1,12 @@ // swift-interface-format-version: 1.0 -// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) -// swift-module-flags: -target x86_64-apple-ios16.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 +// swift-compiler-version: Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) +// swift-module-flags: -target x86_64-apple-ios17.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name Framework2 // swift-module-flags-ignorable: -enable-bare-slash-regex import Foundation import Swift import _Concurrency import _StringProcessing +import _SwiftConcurrencyShims public class Framework2File { public init() public func hello() -> Swift.String diff --git a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftmodule b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftmodule index b4a4e53192c..642d1d19cc0 100644 Binary files a/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftmodule and b/fixtures/ios_app_with_transitive_framework/Framework2/prebuilt/iOS/Framework2.framework/Modules/Framework2.swiftmodule/x86_64-apple-ios-simulator.swiftmodule differ diff --git a/fixtures/ios_app_with_transitive_framework/README.md b/fixtures/ios_app_with_transitive_framework/README.md new file mode 100644 index 00000000000..9ef4648a858 --- /dev/null +++ b/fixtures/ios_app_with_transitive_framework/README.md @@ -0,0 +1,25 @@ +# iOS app with a transitive framework + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - Framework1: + - Framework1 (dynamic iOS framework) + - Framework1Tests (iOS unit tests) +``` + +A standalone Framework2 project is used to generate a prebuilt dynamic framework: + +``` + - Framework2: + - Framework2 (dynamic iOS framework) +``` + +Dependencies: + +- App -> Framework1 +- Framework1 -> Framework2 (prebuilt) + +Note: to re-create `Framework2.framework` run `ios_app_with_transitive_framework/Framework2/build.sh` \ No newline at end of file diff --git a/fixtures/ios_app_with_transitive_framework/StaticFramework1/Project.swift b/fixtures/ios_app_with_transitive_framework/StaticFramework1/Project.swift index 0c323a444cd..ab1af823f77 100644 --- a/fixtures/ios_app_with_transitive_framework/StaticFramework1/Project.swift +++ b/fixtures/ios_app_with_transitive_framework/StaticFramework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFramework1", targets: [ - Target( + .target( name: "StaticFramework1", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework1", infoPlist: .default, @@ -14,9 +14,9 @@ let project = Project( .framework(path: "../Framework2/prebuilt/iOS/Framework2.framework"), ] ), - Target( + .target( name: "StaticFramework1Tests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.StaticFramework1Tests", infoPlist: .default, diff --git a/fixtures/ios_app_with_transitive_project/App/Project.swift b/fixtures/ios_app_with_transitive_project/App/Project.swift index 246e1389041..a19ad103073 100644 --- a/fixtures/ios_app_with_transitive_project/App/Project.swift +++ b/fixtures/ios_app_with_transitive_project/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MainApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Config/App-Info.plist", @@ -15,9 +15,9 @@ let project = Project( .project(target: "FrameworkA-iOS", path: "../FrameworkA"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Config/AppTests-Info.plist", @@ -26,9 +26,9 @@ let project = Project( .target(name: "App"), ] ), - Target( + .target( name: "AppUITests", - platform: .iOS, + destinations: .iOS, product: .uiTests, bundleId: "io.tuist.AppUITests", infoPlist: "Config/AppTests-Info.plist", diff --git a/fixtures/ios_app_with_transitive_project/Framework1/Project.swift b/fixtures/ios_app_with_transitive_project/Framework1/Project.swift index b68b9c168c0..2d086ea46ea 100644 --- a/fixtures/ios_app_with_transitive_project/Framework1/Project.swift +++ b/fixtures/ios_app_with_transitive_project/Framework1/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework1", targets: [ - Target( + .target( name: "Framework1-iOS", - platform: .iOS, + destinations: .iOS, product: .staticFramework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -15,9 +15,9 @@ let project = Project( .project(target: "Framework2-iOS", path: "../Framework2"), ] ), - Target( + .target( name: "Framework1-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "Framework1", bundleId: "io.tuist.Framework1", @@ -27,9 +27,9 @@ let project = Project( .project(target: "Framework2-macOS", path: "../Framework2"), ] ), - Target( + .target( name: "Framework1Tests-iOS", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Tests/Info.plist", @@ -38,9 +38,9 @@ let project = Project( .target(name: "Framework1-iOS"), ] ), - Target( + .target( name: "Framework1Tests-macOS", - platform: .macOS, + destinations: [.mac], product: .unitTests, bundleId: "io.tuist.Framework1Tests", infoPlist: "Tests/Info.plist", diff --git a/fixtures/ios_app_with_transitive_project/Framework2/Project.swift b/fixtures/ios_app_with_transitive_project/Framework2/Project.swift index 4770e771e77..a4d47a373ce 100644 --- a/fixtures/ios_app_with_transitive_project/Framework2/Project.swift +++ b/fixtures/ios_app_with_transitive_project/Framework2/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Framework2", targets: [ - Target( + .target( name: "Framework2-iOS", - platform: .iOS, + destinations: .iOS, product: .staticFramework, productName: "Framework2", bundleId: "io.tuist.Framework2", @@ -14,9 +14,9 @@ let project = Project( dependencies: [ ] ), - Target( + .target( name: "Framework2-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "Framework2", bundleId: "io.tuist.Framework2", diff --git a/fixtures/ios_app_with_transitive_project/FrameworkA/Project.swift b/fixtures/ios_app_with_transitive_project/FrameworkA/Project.swift index 4f7ba09e998..6ee8379d90c 100644 --- a/fixtures/ios_app_with_transitive_project/FrameworkA/Project.swift +++ b/fixtures/ios_app_with_transitive_project/FrameworkA/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FrameworkA", targets: [ - Target( + .target( name: "FrameworkA-iOS", - platform: .iOS, + destinations: .iOS, product: .staticFramework, productName: "FrameworkA", bundleId: "io.tuist.FrameworkA", @@ -15,9 +15,9 @@ let project = Project( .project(target: "FrameworkB-iOS", path: "../FrameworkB"), ] ), - Target( + .target( name: "FrameworkA-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "FrameworkA", bundleId: "io.tuist.FrameworkA", diff --git a/fixtures/ios_app_with_transitive_project/FrameworkB/Project.swift b/fixtures/ios_app_with_transitive_project/FrameworkB/Project.swift index b45c9cd4371..418eea65e60 100644 --- a/fixtures/ios_app_with_transitive_project/FrameworkB/Project.swift +++ b/fixtures/ios_app_with_transitive_project/FrameworkB/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FrameworkB", targets: [ - Target( + .target( name: "FrameworkB-iOS", - platform: .iOS, + destinations: .iOS, product: .staticFramework, productName: "FrameworkB", bundleId: "io.tuist.FrameworkB", @@ -15,9 +15,9 @@ let project = Project( .project(target: "FrameworkC-iOS", path: "../FrameworkC"), ] ), - Target( + .target( name: "FrameworkB-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "FrameworkB", bundleId: "io.tuist.FrameworkB", diff --git a/fixtures/ios_app_with_transitive_project/FrameworkC/Project.swift b/fixtures/ios_app_with_transitive_project/FrameworkC/Project.swift index 8acfcf6c70f..6aee65a1aef 100644 --- a/fixtures/ios_app_with_transitive_project/FrameworkC/Project.swift +++ b/fixtures/ios_app_with_transitive_project/FrameworkC/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FrameworkC", targets: [ - Target( + .target( name: "FrameworkC-iOS", - platform: .iOS, + destinations: .iOS, product: .staticFramework, productName: "FrameworkC", bundleId: "io.tuist.FrameworkC", @@ -13,9 +13,9 @@ let project = Project( sources: "Sources/**", dependencies: [] ), - Target( + .target( name: "FrameworkC-macOS", - platform: .macOS, + destinations: [.mac], product: .framework, productName: "FrameworkC", bundleId: "io.tuist.FrameworkC", diff --git a/fixtures/ios_app_with_watch_application/Project.swift b/fixtures/ios_app_with_watch_application/Project.swift index 8ac74b435ab..8d9cc9fbce7 100644 --- a/fixtures/ios_app_with_watch_application/Project.swift +++ b/fixtures/ios_app_with_watch_application/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "AppWithWatchApp", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: .default, @@ -20,25 +20,25 @@ let project = Project( .target(name: "Framework_a_ios"), ] ), - Target( + .target( name: "Framework_a_ios", - platform: .iOS, + destinations: .iOS, product: .framework, productName: "FrameworkA", bundleId: "io.tuist.framework.a" ), - Target( + .target( name: "Framework_a_watchos", - platform: .watchOS, + destinations: [.appleWatch], product: .framework, productName: "FrameworkA", bundleId: "io.tuist.framework.a" ), // In Xcode 14, watch application can now leverage the `.app` product type // rather than the previous `.watch2App` type - Target( + .target( name: "WatchApp", - platform: .watchOS, + destinations: [.appleWatch], product: .app, bundleId: "io.tuist.App.watchkitapp", infoPlist: nil, @@ -62,9 +62,9 @@ let project = Project( ] ) ), - Target( + .target( name: "WatchWidgetExtension", - platform: .watchOS, + destinations: [.appleWatch], product: .appExtension, bundleId: "io.tuist.App.watchkitapp.widgetExtension", infoPlist: .extendingDefault(with: [ @@ -79,9 +79,9 @@ let project = Project( .target(name: "Framework_a_watchos"), ] ), - Target( + .target( name: "WatchAppTests", - platform: .watchOS, + destinations: [.appleWatch], product: .unitTests, bundleId: "io.tuist.App.watchkitapptests", infoPlist: .default, diff --git a/fixtures/ios_app_with_watchapp2/Project.swift b/fixtures/ios_app_with_watchapp2/Project.swift index 9170015a257..52d8a5069de 100644 --- a/fixtures/ios_app_with_watchapp2/Project.swift +++ b/fixtures/ios_app_with_watchapp2/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "Packages/LibraryA"), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/App-Info.plist", @@ -24,9 +24,9 @@ let project = Project( .package(product: "LibraryA"), ] ), - Target( + .target( name: "WatchApp", - platform: .watchOS, + destinations: [.appleWatch], product: .watch2App, bundleId: "io.tuist.App.watchkitapp", infoPlist: .default, @@ -35,9 +35,9 @@ let project = Project( .target(name: "WatchAppExtension"), ] ), - Target( + .target( name: "WatchAppExtension", - platform: .watchOS, + destinations: [.appleWatch], product: .watch2Extension, bundleId: "io.tuist.App.watchkitapp.watchkitextension", infoPlist: .extendingDefault(with: [ @@ -49,9 +49,9 @@ let project = Project( .package(product: "LibraryA"), ] ), - Target( + .target( name: "WatchAppUITests", - platform: .watchOS, + destinations: [.appleWatch], product: .uiTests, bundleId: "io.tuist.App.watchkitapp.uitests", dependencies: [.target(name: "WatchApp")] diff --git a/fixtures/ios_app_with_watchapp2_xcode14/Project.swift b/fixtures/ios_app_with_watchapp2_xcode14/Project.swift index b46821424df..25081b3f246 100644 --- a/fixtures/ios_app_with_watchapp2_xcode14/Project.swift +++ b/fixtures/ios_app_with_watchapp2_xcode14/Project.swift @@ -6,9 +6,9 @@ let project = Project( .package(path: "Packages/LibraryA"), ], targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Support/App-Info.plist", @@ -24,7 +24,7 @@ let project = Project( .package(product: "LibraryA"), ] ), - Target( + .target( name: "WatchApp", platform: .watchOS, product: .watch2App, @@ -35,7 +35,7 @@ let project = Project( .target(name: "WatchAppExtension"), ] ), - Target( + .target( name: "WatchAppExtension", platform: .watchOS, product: .watch2Extension, @@ -50,7 +50,7 @@ let project = Project( .target(name: "WatchAppWidgetExtension"), ] ), - Target( + .target( name: "WatchAppWidgetExtension", platform: .watchOS, product: .appExtension, @@ -67,7 +67,7 @@ let project = Project( .sdk(name: "SwiftUI", type: .framework, status: .required), ] ), - Target( + .target( name: "WatchAppUITests", platform: .watchOS, product: .uiTests, diff --git a/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkA/Project.swift b/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkA/Project.swift index b20325891bb..f01b00af53e 100644 --- a/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkA/Project.swift +++ b/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkA/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFrameworkA", targets: [ - Target( + .target( name: "StaticFrameworkA", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFrameworkA", infoPlist: .default, diff --git a/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkB/Project.swift b/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkB/Project.swift index db110421ca7..7d2c3d44559 100644 --- a/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkB/Project.swift +++ b/fixtures/ios_app_with_xcframeworks/Modules/StaticFrameworkB/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFrameworkB", targets: [ - Target( + .target( name: "StaticFrameworkB", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFrameworkB", infoPlist: .default, diff --git a/fixtures/ios_app_with_xcframeworks/Project.swift b/fixtures/ios_app_with_xcframeworks/Project.swift index 97a9c7fbb59..91a16eac8d8 100644 --- a/fixtures/ios_app_with_xcframeworks/Project.swift +++ b/fixtures/ios_app_with_xcframeworks/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -22,9 +22,9 @@ let project = Project( "BITCODE_ENABLED": "NO", ]) ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_xcframeworks/README.md b/fixtures/ios_app_with_xcframeworks/README.md new file mode 100644 index 00000000000..7f7bb68683e --- /dev/null +++ b/fixtures/ios_app_with_xcframeworks/README.md @@ -0,0 +1,19 @@ +# iOS app with xcframeworks + +``` +Workspace: + - App: + - MainApp (iOS app) + - MainAppTests (iOS unit tests) + - MyFramework: + - MyFramework (dynamic iOS framework) + - MyStaticFramework: + - MyStaticFramework (static iOS framework) + - MyStaticLibirary: + - MyStaticLibrary (static iOS libraries) +``` + +An example of an application which depends on prebuilt `.xcframework`s. + +The `.xcframework` can be obtained by running the `build.sh` script within the each of the xcframework directories +e.g. `ios_app_with_xcframeworks/XCFrameworks/MyFramework/build.sh`. \ No newline at end of file diff --git a/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyFramework/Project.swift b/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyFramework/Project.swift index 56d0a9dd40c..b1051d72382 100644 --- a/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyFramework/Project.swift +++ b/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MyFramework", targets: [ - Target( + .target( name: "MyFramework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.MyFramework", infoPlist: "Info.plist", @@ -22,9 +22,9 @@ let project = Project( "BUILD_LIBRARY_FOR_DISTRIBUTION": "YES", ]) ), - Target( + .target( name: "MyFrameworkTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.MyFrameworkTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticFramework/Project.swift b/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticFramework/Project.swift index dc70ef4a912..67cda05d3e4 100644 --- a/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticFramework/Project.swift +++ b/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MyStaticFramework", targets: [ - Target( + .target( name: "MyStaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.MyStaticFramework", infoPlist: "Info.plist", diff --git a/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticLibrary/Project.swift b/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticLibrary/Project.swift index 0dc044768c3..de88f9f0355 100644 --- a/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticLibrary/Project.swift +++ b/fixtures/ios_app_with_xcframeworks/XCFrameworks/MyStaticLibrary/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "MyStaticLibrary", targets: [ - Target( + .target( name: "MyStaticLibrary", - platform: .iOS, + destinations: .iOS, product: .staticLibrary, bundleId: "io.tuist.MyStaticLibrary", infoPlist: "Info.plist", diff --git a/fixtures/ios_workspace_with_dependency_cycle/App/Project.swift b/fixtures/ios_workspace_with_dependency_cycle/App/Project.swift index 2705db712be..996e2cd40d0 100644 --- a/fixtures/ios_workspace_with_dependency_cycle/App/Project.swift +++ b/fixtures/ios_workspace_with_dependency_cycle/App/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( .project(target: "FrameworkA", path: "../Frameworks/FrameworkA"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkA/Project.swift b/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkA/Project.swift index c6b653c5e85..58ce793b477 100644 --- a/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkA/Project.swift +++ b/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkA/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FrameworkA", targets: [ - Target( + .target( name: "FrameworkA", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.FrameworkA", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( .project(target: "FrameworkB", path: "../FrameworkB"), ] ), - Target( + .target( name: "FrameworkATests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.FrameworkATests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkB/Project.swift b/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkB/Project.swift index 9f12f879e5b..fb9390f23fb 100644 --- a/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkB/Project.swift +++ b/fixtures/ios_workspace_with_dependency_cycle/Frameworks/FrameworkB/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FrameworkB", targets: [ - Target( + .target( name: "FrameworkB", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.FrameworkB", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( .project(target: "FrameworkA", path: "../FrameworkA"), ] ), - Target( + .target( name: "FrameworkBTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.FrameworkBTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_dependency_cycle/README.md b/fixtures/ios_workspace_with_dependency_cycle/README.md new file mode 100644 index 00000000000..f092838b117 --- /dev/null +++ b/fixtures/ios_workspace_with_dependency_cycle/README.md @@ -0,0 +1,3 @@ +# iOS workspace with a dependency cycle + +An example of a workspace that has a dependency cycle between targets in different projects. \ No newline at end of file diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/CoreFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/CoreFramework/Project.swift index 3a5da32895a..91a5213c5e3 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/CoreFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/CoreFramework/Project.swift @@ -4,9 +4,9 @@ import ProjectDescription let project = Project( name: "Core", targets: [ - Target( + .target( name: "Core", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: .bundleId(for: "Core"), infoPlist: "Info.plist", @@ -16,9 +16,9 @@ let project = Project( // "Resources/**" ] ), - Target( + .target( name: "CoreTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: .bundleId(for: "CoreTests"), infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/DataFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/DataFramework/Project.swift index 1e4d34c3c34..94b97b9d33d 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/DataFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/DataFramework/Project.swift @@ -4,9 +4,9 @@ import ProjectDescription let project = Project( name: "Data", targets: [ - Target( + .target( name: "Data", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: .bundleId(for: "Data"), infoPlist: "Info.plist", @@ -21,9 +21,9 @@ let project = Project( .project(target: "Core", path: "../CoreFramework"), ] ), - Target( + .target( name: "DataTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: .bundleId(for: "DataFrameworkTests"), infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureAFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureAFramework/Project.swift index 22a2d5a13bc..75eeeb9987d 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureAFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureAFramework/Project.swift @@ -4,9 +4,9 @@ import ProjectDescription let project = Project( name: "FrameworkA", targets: [ - Target( + .target( name: "FrameworkA", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: .bundleId(for: "FrameworkA"), infoPlist: "Info.plist", @@ -24,9 +24,9 @@ let project = Project( .project(target: "UIComponents", path: "../UIComponentsFramework"), ] ), - Target( + .target( name: "FrameworkATests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: .bundleId(for: "FrameworkATests"), infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Project.swift index 7a79176a9ce..6d721810d87 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Project.swift @@ -4,9 +4,9 @@ import ProjectDescription let project = Project( name: "FeatureContracts", targets: [ - Target( + .target( name: "FeatureContracts", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: .bundleId(for: "FeatureContracts"), infoPlist: "Info.plist", @@ -22,9 +22,9 @@ let project = Project( .project(target: "Core", path: "../CoreFramework"), ] ), - Target( + .target( name: "FeatureContractsTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: .bundleId(for: "FeatureContractsTests"), infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/UIComponentsFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/UIComponentsFramework/Project.swift index 3be8d9c041a..b3306eb6a09 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/UIComponentsFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/UIComponentsFramework/Project.swift @@ -4,9 +4,9 @@ import ProjectDescription let project = Project( name: "UIComponents", targets: [ - Target( + .target( name: "UIComponents", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: .bundleId(for: "UIComponents"), infoPlist: "Info.plist", @@ -21,9 +21,9 @@ let project = Project( .project(target: "FeatureContracts", path: "../FeatureContracts"), ] ), - Target( + .target( name: "UIComponentsTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: .bundleId(for: "UIComponentsTests"), infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture/Project.swift index 1c184494546..b9e625ce7b5 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Project.swift @@ -4,9 +4,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: .bundleId(for: "App"), infoPlist: "App/Info.plist", @@ -21,9 +21,9 @@ let project = Project( .project(target: "FrameworkA", path: "Frameworks/FeatureAFramework"), ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: .bundleId(for: "AppTests"), infoPlist: "App/Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/CoreFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/CoreFramework/Project.swift index a8e867cfc30..f5d10ede9ce 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/CoreFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/CoreFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Core", targets: [ - Target( + .target( name: "Core", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.Core", infoPlist: "Info.plist", @@ -15,9 +15,9 @@ let project = Project( // "Resources/**" ] ), - Target( + .target( name: "CoreTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.CoreTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/DataFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/DataFramework/Project.swift index 8357b410f4f..f1c3b5cb3bd 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/DataFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/DataFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Data", targets: [ - Target( + .target( name: "Data", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.Data", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( .project(target: "Core", path: "../CoreFramework"), ] ), - Target( + .target( name: "DataTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.DataFrameworkTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureAFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureAFramework/Project.swift index 66455d054cf..4d1c420cd13 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureAFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureAFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FrameworkA", targets: [ - Target( + .target( name: "FrameworkA", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.FrameworkA", infoPlist: "Info.plist", @@ -23,9 +23,9 @@ let project = Project( .project(target: "UIComponents", path: "../UIComponentsFramework"), ] ), - Target( + .target( name: "FrameworkATests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.FrameworkATests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Project.swift index 4c3edfc4e55..77e4fc633e4 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "FeatureContracts", targets: [ - Target( + .target( name: "FeatureContracts", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.FeatureContracts", infoPlist: "Info.plist", @@ -21,9 +21,9 @@ let project = Project( .project(target: "Core", path: "../CoreFramework"), ] ), - Target( + .target( name: "FeatureContractsTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.FeatureContractsTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/UIComponentsFramework/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/UIComponentsFramework/Project.swift index 846cb25d8c4..f4b82d8713d 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/UIComponentsFramework/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/UIComponentsFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "UIComponents", targets: [ - Target( + .target( name: "UIComponents", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.UIComponents", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( .project(target: "FeatureContracts", path: "../FeatureContracts"), ] ), - Target( + .target( name: "UIComponentsTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.UIComponentsTests", infoPlist: "Tests.plist", diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/StaticApp/Project.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/StaticApp/Project.swift index 4374cacf21a..b2e3abe6dbe 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/StaticApp/Project.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/StaticApp/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticApp", targets: [ - Target( + .target( name: "StaticApp", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.StaticApp", infoPlist: "Info.plist", @@ -20,9 +20,9 @@ let project = Project( .project(target: "FrameworkA", path: "../Frameworks/FeatureAFramework"), ] ), - Target( + .target( name: "StaticAppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.StaticAppTests", infoPlist: "Tests.plist", diff --git a/fixtures/macos_app_with_app_inside/Project.swift b/fixtures/macos_app_with_app_inside/Project.swift index ae835426a56..bf3692f9bcf 100644 --- a/fixtures/macos_app_with_app_inside/Project.swift +++ b/fixtures/macos_app_with_app_inside/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Embedded App", targets: [ - Target( + .target( name: "MainApp", - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.MainApp", infoPlist: "MainApp/Info.plist", @@ -18,17 +18,17 @@ let project = Project( .target(name: "InnerCLI"), ] ), - Target( + .target( name: "InnerApp", - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.InnerApp", infoPlist: "InnerApp/Info.plist", sources: ["InnerApp/Sources/**"] ), - Target( + .target( name: "InnerCLI", - platform: .macOS, + destinations: [.mac], product: .commandLineTool, bundleId: "io.tuist.InnerCLI", sources: ["InnerCLI/**"] diff --git a/fixtures/macos_app_with_copy_files/Project.swift b/fixtures/macos_app_with_copy_files/Project.swift index 002a6c300de..d7aca689ec2 100644 --- a/fixtures/macos_app_with_copy_files/Project.swift +++ b/fixtures/macos_app_with_copy_files/Project.swift @@ -1,9 +1,9 @@ import ProjectDescription func target(name: String) -> Target { - Target( + .target( name: name, - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.\(name)", infoPlist: .file(path: .relativeToManifest("Info.plist")), @@ -12,7 +12,7 @@ func target(name: String) -> Target { .sharedSupport( name: "Copy Templates", subpath: "Templates", - files: ["Templates/**"] + files: [.glob(pattern: "Templates/**", condition: .when([.macos]))] ), ], settings: .settings(base: ["CODE_SIGN_IDENTITY": "", "CODE_SIGNING_REQUIRED": "NO"]) diff --git a/fixtures/macos_app_with_extensions/Project.swift b/fixtures/macos_app_with_extensions/Project.swift index a9b159dc496..3b0857ac0e6 100644 --- a/fixtures/macos_app_with_extensions/Project.swift +++ b/fixtures/macos_app_with_extensions/Project.swift @@ -15,18 +15,18 @@ let workflowExtensionSettings: SettingsDictionary = [ let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.app", infoPlist: .default, sources: "App/**", dependencies: [.target(name: "Workflow")] ), - Target( + .target( name: "Workflow", - platform: .macOS, + destinations: [.mac], product: .appExtension, bundleId: "io.tuist.app.workflow", infoPlist: .extendingDefault(with: [ diff --git a/fixtures/macos_app_with_extensions/README.md b/fixtures/macos_app_with_extensions/README.md new file mode 100644 index 00000000000..b3aa4872a80 --- /dev/null +++ b/fixtures/macos_app_with_extensions/README.md @@ -0,0 +1,3 @@ +# macOS app with extensions + +The project contains a macOS app with various types of extensions. \ No newline at end of file diff --git a/fixtures/macos_app_with_system_extension/Project.swift b/fixtures/macos_app_with_system_extension/Project.swift index a94a1411f74..20d324ddff5 100644 --- a/fixtures/macos_app_with_system_extension/Project.swift +++ b/fixtures/macos_app_with_system_extension/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App with SystemExtension", targets: [ - Target( + .target( name: "MainApp", - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.MainApp", infoPlist: "MainApp/Info.plist", @@ -14,9 +14,9 @@ let project = Project( .target(name: "SystemExtension"), ] ), - Target( + .target( name: "SystemExtension", - platform: .macOS, + destinations: [.mac], product: .systemExtension, bundleId: "io.tuist.SystemExtension", sources: ["SystemExtension/Sources/**"] diff --git a/fixtures/macos_app_with_xpc/Project.swift b/fixtures/macos_app_with_xpc/Project.swift index 1a6dbebebdc..da9d3f79c90 100644 --- a/fixtures/macos_app_with_xpc/Project.swift +++ b/fixtures/macos_app_with_xpc/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App with XPC", targets: [ - Target( + .target( name: "MainApp", - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.MainApp", infoPlist: "MainApp/Info.plist", @@ -14,9 +14,9 @@ let project = Project( .target(name: "XPCApp"), ] ), - Target( + .target( name: "XPCApp", - platform: .macOS, + destinations: [.mac], product: .xpc, bundleId: "io.tuist.XPCApp", sources: ["XPCApp/Sources/**"], @@ -25,16 +25,16 @@ let project = Project( .target(name: "StaticFramework"), ] ), - Target( + .target( name: "DynamicFramework", - platform: .macOS, + destinations: [.mac], product: .framework, bundleId: "io.tuist.DynamicFramework", sources: ["DynamicFramework/Sources/**"] ), - Target( + .target( name: "StaticFramework", - platform: .macOS, + destinations: [.mac], product: .staticFramework, bundleId: "io.tuist.StaticFramework", sources: ["StaticFramework/Sources/**"] diff --git a/fixtures/manifest_with_logs/Project.swift b/fixtures/manifest_with_logs/Project.swift index 2ec384f63ee..a491d470a26 100644 --- a/fixtures/manifest_with_logs/Project.swift +++ b/fixtures/manifest_with_logs/Project.swift @@ -2,9 +2,9 @@ import ProjectDescription func target(name: String) -> Target { print("Target name - \(name)") - return Target( + return .target( name: name, - platform: .macOS, + destinations: [.mac], product: .app, bundleId: "io.tuist.\(name)", infoPlist: .default, diff --git a/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift b/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift index 7989b1df37a..8f571923807 100644 --- a/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift +++ b/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift @@ -1,7 +1,7 @@ import SwiftUI @main -struct TuistApp: App { +struct TuistServer: App { var body: some Scene { WindowGroup { Text("Tuist is great") diff --git a/fixtures/multiplatform_app_with_extension/Project.swift b/fixtures/multiplatform_app_with_extension/Project.swift index ca9a7dbaba5..316b154b0db 100644 --- a/fixtures/multiplatform_app_with_extension/Project.swift +++ b/fixtures/multiplatform_app_with_extension/Project.swift @@ -1,6 +1,6 @@ import ProjectDescription -let appTarget = Target( +let appTarget: Target = .target( name: "App", destinations: [.iPhone, .iPad, .appleVision], product: .app, @@ -13,7 +13,7 @@ let appTarget = Target( ] ) -let widgetExtensionTarget = Target( +let widgetExtensionTarget: Target = .target( name: "WidgetExtension", destinations: [.iPhone, .iPad], product: .appExtension, @@ -28,7 +28,7 @@ let widgetExtensionTarget = Target( resources: "Extensions/WidgetExtension/Resources/**" ) -let watchApp = Target( +let watchApp: Target = .target( name: "WatchApp", destinations: [.appleWatch], product: .app, diff --git a/fixtures/multiplatform_app_with_extension/Tuist/Config.swift b/fixtures/multiplatform_app_with_extension/Tuist/Config.swift index ff8e64fa9ee..f00d6c2e8de 100644 --- a/fixtures/multiplatform_app_with_extension/Tuist/Config.swift +++ b/fixtures/multiplatform_app_with_extension/Tuist/Config.swift @@ -1,3 +1,9 @@ import ProjectDescription -let config = Config() +let config = Config( + fullHandle: "tuist/multiplatform_app_with_extension", + url: "https://canary.tuist.io", + generationOptions: .options( + optionalAuthentication: true + ) +) diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/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_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..13613e3ee1a --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/Contents.json b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Sources/ContentView.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Sources/ContentView.swift new file mode 100644 index 00000000000..8b74b2b1f4a --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Sources/ContentView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + Text("Hello, world!") + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Sources/TestApp.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Sources/TestApp.swift new file mode 100644 index 00000000000..78f112a8178 --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/App/Sources/TestApp.swift @@ -0,0 +1,15 @@ +import ModuleA +import SwiftUI + +@main +struct TestApp: App { + init() { + ModuleA.test() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Modules/ModuleA/Sources/ModuleA.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Modules/ModuleA/Sources/ModuleA.swift new file mode 100644 index 00000000000..b99b065aa49 --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Modules/ModuleA/Sources/ModuleA.swift @@ -0,0 +1,12 @@ +import CasePaths + +@CasePathable +enum AppAction { + case home +} + +public enum ModuleA { + public static func test() { + print("Module A") + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Project.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Project.swift new file mode 100644 index 00000000000..369ed294cac --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Project.swift @@ -0,0 +1,62 @@ +import ProjectDescription + +let project = Project( + name: "AppWithWatchApp", + targets: [ + .target( + name: "App", + destinations: [.iPhone, .appleVision], + product: .app, + bundleId: "io.tuist.App", + infoPlist: .default, + sources: [ + "App/Sources/**", + ], + resources: [ + "App/Resources/**", + ], + dependencies: [ + .target(name: "WatchApp", condition: .when([.ios])), + .target(name: "ModuleA"), + ] + ), + .target( + name: "WatchApp", + destinations: [.appleWatch], + product: .app, + bundleId: "io.tuist.App.watchkitapp", + infoPlist: nil, + sources: "WatchApp/Sources/**", + resources: "WatchApp/Resources/**", + dependencies: [ + .target(name: "ModuleA"), + ], + 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, + ] + ) + ), + .target( + name: "ModuleA", + destinations: [.iPhone, .appleVision, .appleWatch], + product: .framework, + productName: "ModuleA", + bundleId: "io.tuist.modulea", + sources: [ + "Modules/ModuleA/Sources/**", + ], + dependencies: [ + .external(name: "CasePaths"), + ] + ), + ] +) diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Tuist/Package.resolved b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Tuist/Package.resolved new file mode 100644 index 00000000000..d71a38c54fc --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Tuist/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "e593aba2c6222daad7c4f2732a431eed2c09bb07", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "b13b1d1a8e787a5ffc71ac19dcaf52183ab27ba2", + "version" : "1.1.1" + } + } + ], + "version" : 2 +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Tuist/Package.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Tuist/Package.swift new file mode 100644 index 00000000000..f2fed31dcfe --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/Tuist/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "Dependencies", + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.3.0"), + ] +) diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/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_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..49c81cd8c4c --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/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_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/Contents.json b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/ContentView.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/ContentView.swift new file mode 100644 index 00000000000..8b74b2b1f4a --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/ContentView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + Text("Hello, world!") + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/WatchApp.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/WatchApp.swift new file mode 100644 index 00000000000..0755c22ee46 --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/WatchApp.swift @@ -0,0 +1,21 @@ +import CasePaths +import ModuleA +import SwiftUI + +@CasePathable +enum WatchAppAction { + case home +} + +@main +struct WatchApp: App { + init() { + ModuleA.test() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/WatchConfig.swift b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/WatchConfig.swift new file mode 100644 index 00000000000..4485cd7c527 --- /dev/null +++ b/fixtures/multiplatform_app_with_macros_and_embedded_watchos_app/WatchApp/Sources/WatchConfig.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum WatchConfig { + public static let title: String = "Hello, watchOS" +} diff --git a/fixtures/multiplatform_app_with_sdk/App/AppDelegate.swift b/fixtures/multiplatform_app_with_sdk/App/AppDelegate.swift index 7989b1df37a..8f571923807 100644 --- a/fixtures/multiplatform_app_with_sdk/App/AppDelegate.swift +++ b/fixtures/multiplatform_app_with_sdk/App/AppDelegate.swift @@ -1,7 +1,7 @@ import SwiftUI @main -struct TuistApp: App { +struct TuistServer: App { var body: some Scene { WindowGroup { Text("Tuist is great") diff --git a/fixtures/multiplatform_app_with_sdk/Modules/StaticFramework/Project.swift b/fixtures/multiplatform_app_with_sdk/Modules/StaticFramework/Project.swift index 45708c1a6a3..6aece5d8b22 100644 --- a/fixtures/multiplatform_app_with_sdk/Modules/StaticFramework/Project.swift +++ b/fixtures/multiplatform_app_with_sdk/Modules/StaticFramework/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "StaticFramework", targets: [ - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.StaticFramework", infoPlist: "Support/Info.plist", @@ -15,9 +15,9 @@ let project = Project( .sdk(name: "c++", type: .library), ] ), - Target( + .target( name: "StaticFrameworkTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.StaticFrameworkTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/multiplatform_app_with_sdk/Package.resolved b/fixtures/multiplatform_app_with_sdk/Package.resolved new file mode 100644 index 00000000000..59f42b3a641 --- /dev/null +++ b/fixtures/multiplatform_app_with_sdk/Package.resolved @@ -0,0 +1,113 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", + "version" : "1.2022062300.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "8872dbd7d947acf757abab933da10e83c1842280", + "version" : "10.17.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "6b332152355c372ace9966d8ee76ed191f97025e", + "version" : "10.17.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "aae45a320fd0d11811820335b1eabc8753902a40", + "version" : "9.2.5" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3", + "version" : "7.12.1" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", + "version" : "1.49.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", + "version" : "3.1.1" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "9d108e9112aa1d65ce508facf804674546116d9c", + "version" : "1.22.3" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", + "version" : "2.30909.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "07f7f26ded8df9645c072f220378879c4642e063", + "version" : "1.25.1" + } + } + ], + "version" : 2 +} diff --git a/fixtures/multiplatform_app_with_sdk/Project.swift b/fixtures/multiplatform_app_with_sdk/Project.swift index 1864cb8bb4d..ad1d85ba1f7 100644 --- a/fixtures/multiplatform_app_with_sdk/Project.swift +++ b/fixtures/multiplatform_app_with_sdk/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "Project", targets: [ - Target( + .target( name: "MyTestFramework", - platform: .iOS, + destinations: .iOS, product: .framework, bundleId: "io.tuist.MyTestFramework", infoPlist: .default, @@ -14,9 +14,9 @@ let project = Project( .xctest, ] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", @@ -26,7 +26,7 @@ let project = Project( .target(name: "MyTestFramework"), ] ), - Target( + .target( name: "App", destinations: [.iPhone, .iPad, .mac], product: .app, @@ -38,10 +38,12 @@ let project = Project( .sdk(name: "ARKit", type: .framework, status: .required, condition: .when([.ios])), .external(name: "FirebaseAnalytics"), .external(name: "FirebasePerformance", condition: .when([.ios])), + .external(name: "FirebaseRemoteConfig"), + .external(name: "FirebaseInAppMessaging-Beta"), .sdk(name: "MobileCoreServices", type: .framework, status: .required, condition: .when([.ios])), ] ), - Target( + .target( name: "MultiPlatformFramework", destinations: [.iPad, .iPhone, .mac, .appleTv], product: .framework, diff --git a/fixtures/multiplatform_app_with_sdk/Tuist/.gitkeep b/fixtures/multiplatform_app_with_sdk/Tuist/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/multiplatform_app_with_sdk/Tuist/Dependencies.swift b/fixtures/multiplatform_app_with_sdk/Tuist/Dependencies.swift deleted file mode 100644 index 45f7093e0a4..00000000000 --- a/fixtures/multiplatform_app_with_sdk/Tuist/Dependencies.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ProjectDescription - -let dependencies = Dependencies( - swiftPackageManager: .init(), - platforms: [.macOS, .iOS] -) diff --git a/fixtures/multiplatform_app_with_sdk/Tuist/Package.resolved b/fixtures/multiplatform_app_with_sdk/Tuist/Package.resolved index 59f42b3a641..b9679b58f6a 100644 --- a/fixtures/multiplatform_app_with_sdk/Tuist/Package.resolved +++ b/fixtures/multiplatform_app_with_sdk/Tuist/Package.resolved @@ -5,8 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", - "version" : "1.2022062300.0" + "revision" : "7ce7be095bc3ed3c98b009532fe2d7698c132614", + "version" : "1.2024011601.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" } }, { @@ -14,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "8872dbd7d947acf757abab933da10e83c1842280", - "version" : "10.17.0" + "revision" : "888f0b6026e2441a69e3ee2ad5293c7a92031e62", + "version" : "10.23.1" } }, { @@ -23,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "6b332152355c372ace9966d8ee76ed191f97025e", - "version" : "10.17.0" + "revision" : "c7a5917ebe48d69f421aadf154ef3969c8b7f12d", + "version" : "10.23.1" } }, { @@ -32,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "aae45a320fd0d11811820335b1eabc8753902a40", - "version" : "9.2.5" + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" } }, { @@ -41,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3", - "version" : "7.12.1" + "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", + "version" : "7.13.1" } }, { @@ -50,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", - "version" : "1.49.1" + "revision" : "67043f6389d0e28b38fa02d1c6952afeb04d807f", + "version" : "1.62.1" } }, { @@ -59,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", - "version" : "3.1.1" + "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", + "version" : "3.3.1" } }, { @@ -77,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/leveldb.git", "state" : { - "revision" : "9d108e9112aa1d65ce508facf804674546116d9c", - "version" : "1.22.3" + "revision" : "43aaef65e0c665daadf848761d560e446d350d3d", + "version" : "1.22.4" } }, { @@ -86,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { - "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", - "version" : "2.30909.0" + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" } }, { @@ -95,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", - "version" : "2.3.1" + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" } }, { @@ -104,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "07f7f26ded8df9645c072f220378879c4642e063", - "version" : "1.25.1" + "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version" : "1.25.2" } } ], diff --git a/fixtures/multiplatform_app_with_sdk/Tuist/Package.swift b/fixtures/multiplatform_app_with_sdk/Tuist/Package.swift index 49994d9822a..4255f2867d9 100644 --- a/fixtures/multiplatform_app_with_sdk/Tuist/Package.swift +++ b/fixtures/multiplatform_app_with_sdk/Tuist/Package.swift @@ -4,6 +4,6 @@ import PackageDescription let package = Package( name: "PackageName", dependencies: [ - .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.15.0"), + .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.23.1"), ] ) diff --git a/fixtures/multiplatform_app_with_sdk/Workspace.swift b/fixtures/multiplatform_app_with_sdk/Workspace.swift new file mode 100644 index 00000000000..1409a94ed1c --- /dev/null +++ b/fixtures/multiplatform_app_with_sdk/Workspace.swift @@ -0,0 +1,6 @@ +import ProjectDescription + +let workspace = Workspace( + name: "App", + projects: ["./**"] +) diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/.gitignore" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/.gitignore" new file mode 100644 index 00000000000..cb44bf9984c --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/.gitignore" @@ -0,0 +1,66 @@ +### 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 managed dependencies ### +Tuist/Dependencies diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Example/Sources/AppDelegate.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Example/Sources/AppDelegate.swift" new file mode 100644 index 00000000000..f94821a6efc --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Example/Sources/AppDelegate.swift" @@ -0,0 +1,11 @@ +import ModuleAImplementation +import SwiftUI + +@main +struct TuistServer: App { + var body: some Scene { + WindowGroup { + Text("Tuist is great") + } + } +} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Example/SupportingFiles/App-Info.plist" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Example/SupportingFiles/App-Info.plist" new file mode 100644 index 00000000000..c407fafb087 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Example/SupportingFiles/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_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Implementation/Sources/FrameworkA.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Implementation/Sources/FrameworkA.swift" new file mode 100644 index 00000000000..790a21ff743 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Implementation/Sources/FrameworkA.swift" @@ -0,0 +1,6 @@ +import Foundation +import ModuleAInterface + +public class FrameworkA: FrameworkAInterface { + public init() {} +} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Interface/Sources/FrameworkAInterface.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Interface/Sources/FrameworkAInterface.swift" new file mode 100644 index 00000000000..1645c3e268e --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Interface/Sources/FrameworkAInterface.swift" @@ -0,0 +1 @@ +public protocol FrameworkAInterface {} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Project.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Project.swift" new file mode 100644 index 00000000000..fa110eab7fd --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Project.swift" @@ -0,0 +1,76 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let supportedPlatforms: Set = [.iOS] + +let project = Project( + name: "ModuleA", + targets: [ + .target( + name: "ModuleAInterface", + destinations: .destinations(for: supportedPlatforms), + product: .framework, + bundleId: "io.tuist.ModuleA.interface", + deploymentTargets: .deploymentTargets(for: supportedPlatforms), + sources: [ + .glob("Interface/Sources/**"), + ] + ), + .target( + name: "ModuleAImplementation", + destinations: .destinations(for: supportedPlatforms), + product: .framework, + bundleId: "io.tuist.ModuleA.implementation", + deploymentTargets: .deploymentTargets(for: supportedPlatforms), + sources: [ + .glob("Implementation/Sources/**"), + ], + dependencies: [ + .target(name: "ModuleAInterface"), + ] + ), + .target( + name: "ModuleATestSupporting", + destinations: .destinations(for: supportedPlatforms), + product: .framework, + bundleId: "io.tuist.ModuleA.testSupporting", + deploymentTargets: .deploymentTargets(for: supportedPlatforms), + sources: [ + .glob("TestSupporting/Sources/**"), + ], + dependencies: [ + .target(name: "ModuleAInterface"), + .external(name: "Mocker"), + ] + ), + .target( + name: "ModuleATests", + destinations: .destinations(for: supportedPlatforms), + product: .unitTests, + bundleId: "io.tuist.ModuleA.tests", + deploymentTargets: .deploymentTargets(for: supportedPlatforms), + sources: [ + .glob("Tests/Sources/**"), + ], + dependencies: [ + .target(name: "ModuleATestSupporting"), + .xctest, + .external(name: "Mocker"), + ] + ), + .target( + name: "ExampleApp", + destinations: .destinations(for: [.iOS]), + product: .app, + bundleId: "io.tuist.ModuleA.example", + deploymentTargets: .deploymentTargets(for: [.iOS]), + infoPlist: "Example/SupportingFiles/App-Info.plist", + sources: "Example/Sources/**", + dependencies: [ + .target(name: "ModuleAInterface"), + .target(name: "ModuleAImplementation"), + .target(name: "ModuleATestSupporting"), + ] + ), + ] +) diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/TestSupporting/Sources/FrameworkATestSupporting.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/TestSupporting/Sources/FrameworkATestSupporting.swift" new file mode 100644 index 00000000000..d702c15ad72 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/TestSupporting/Sources/FrameworkATestSupporting.swift" @@ -0,0 +1,13 @@ +import Foundation +import Mocker + +public enum NetworkResponseMocks { + public static var testMock: Mock { + Mock( + url: URL(string: "https://apple.com")!, + dataType: .json, + statusCode: 200, + data: [.get: Data()] + ) + } +} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Tests/Sources/FrameworkATests.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Tests/Sources/FrameworkATests.swift" new file mode 100644 index 00000000000..0fbcb22fa87 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Modules/ModuleA/Tests/Sources/FrameworkATests.swift" @@ -0,0 +1,23 @@ +import Mocker +import ModuleATestSupporting +import XCTest + +class FrameworkATests: XCTestCase { + var sut: URLSession! + + override func setUp() { + super.setUp() + + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockingURLProtocol.self] + + sut = URLSession(configuration: configuration) + + NetworkResponseMocks.testMock.register() + } + + func testMock() async throws { + let (_, response) = try await sut.data(from: URL(string: "https://apple.com")!) + XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 200) + } +} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Config.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Config.swift" new file mode 100644 index 00000000000..5f03a578685 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Config.swift" @@ -0,0 +1,3 @@ +import ProjectDescription + +let config = Config(generationOptions: .options(enforceExplicitDependencies: true)) diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Dependencies.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Dependencies.swift" new file mode 100644 index 00000000000..75c4cae49b1 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Dependencies.swift" @@ -0,0 +1,11 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let dependencies = Dependencies( + swiftPackageManager: .init( + productTypes: [ + "Mocker": .framework, + ] + ), + platforms: [.iOS, .watchOS] +) diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Package.resolved" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Package.resolved" new file mode 100644 index 00000000000..59a26b0d65e --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Package.resolved" @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "mocker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WeTransfer/Mocker", + "state" : { + "branch" : "3.0.1", + "revision" : "4384e015cae4916a6828252467a4437173c7ae17" + } + } + ], + "version" : 2 +} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Package.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Package.swift" new file mode 100644 index 00000000000..6e091b3bfc6 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/Package.swift" @@ -0,0 +1,20 @@ +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST + import ProjectDescription + import ProjectDescriptionHelpers + + let packageSettings = PackageSettings( + productTypes: [ // Convert 3rd party frameworks into dynamic frameworks + "Mocker": .framework, + ] + ) +#endif + +let package = Package( + name: "Dependencies", + dependencies: [ + .package(url: "https://github.com/WeTransfer/Mocker", revision: "3.0.1"), + ] +) diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/ProjectDescriptionHelpers/Extensions.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/ProjectDescriptionHelpers/Extensions.swift" new file mode 100644 index 00000000000..75032acfd2c --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Tuist/ProjectDescriptionHelpers/Extensions.swift" @@ -0,0 +1,47 @@ +import ProjectDescription + +extension Destinations { + public static func destinations(for platforms: Set) -> Destinations { + var set: Destinations = [] + if platforms.contains(.iOS) { + set.formUnion([.iPhone, .iPad]) + } + + if platforms.contains(.watchOS) { + set.insert(.appleWatch) + } + + if platforms.contains(.macOS) { + set.insert(.mac) + } + + return set + } +} + +extension DeploymentTargets { + public static func deploymentTargets(for platforms: Set) -> DeploymentTargets { + var iOS: String? = nil + var watchOS: String? = nil + var macOS: String? = nil + var visionOS: String? = nil + + if platforms.contains(.iOS) { + iOS = "16.0" + } + + if platforms.contains(.watchOS) { + watchOS = "9.0" + } + + if platforms.contains(.macOS) { + macOS = "13.0" + } + + if platforms.contains(.visionOS) { + visionOS = "1.0" + } + + return .multiplatform(iOS: iOS, macOS: macOS, watchOS: watchOS, visionOS: visionOS) + } +} diff --git "a/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Workspace.swift" "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Workspace.swift" new file mode 100644 index 00000000000..0f2264674c9 --- /dev/null +++ "b/fixtures/multiplatform_\302\265Feature_unit_tests_with_explicit_dependencies/Workspace.swift" @@ -0,0 +1,11 @@ +import ProjectDescription + +let modules: [Path] = [ + "Modules/ModuleA", +] + +let workspace = Workspace( + name: "Workspace", + projects: modules, + schemes: [] +) diff --git a/fixtures/project_with_class_prefix/Project.swift b/fixtures/project_with_class_prefix/Project.swift new file mode 100644 index 00000000000..4d9d7a9aa05 --- /dev/null +++ b/fixtures/project_with_class_prefix/Project.swift @@ -0,0 +1,6 @@ +import ProjectDescription + +let project = Project( + name: "App", + classPrefix: "TUIST" +) diff --git a/fixtures/project_with_class_prefix/README.md b/fixtures/project_with_class_prefix/README.md new file mode 100644 index 00000000000..55dc80e6caf --- /dev/null +++ b/fixtures/project_with_class_prefix/README.md @@ -0,0 +1,3 @@ +# Project with class prefix + +This example generate Xcode project with custom class prefix setting. \ No newline at end of file diff --git a/fixtures/project_with_class_prefix/Tuist/Config.swift b/fixtures/project_with_class_prefix/Tuist/Config.swift new file mode 100644 index 00000000000..ff8e64fa9ee --- /dev/null +++ b/fixtures/project_with_class_prefix/Tuist/Config.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let config = Config() diff --git a/fixtures/project_with_test_and_run_locales/Project.swift b/fixtures/project_with_test_and_run_locales/Project.swift index 5add4318223..8533c52b557 100644 --- a/fixtures/project_with_test_and_run_locales/Project.swift +++ b/fixtures/project_with_test_and_run_locales/Project.swift @@ -13,13 +13,13 @@ let project = Project( targets: [ .init( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app" ), .init( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.appTests" ), diff --git a/fixtures/spm_package/.gitignore b/fixtures/spm_package/.gitignore new file mode 100644 index 00000000000..0023a534063 --- /dev/null +++ b/fixtures/spm_package/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/fixtures/spm_package/Package.resolved b/fixtures/spm_package/Package.resolved new file mode 100644 index 00000000000..aa5401102b4 --- /dev/null +++ b/fixtures/spm_package/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "b2fa556e4e48cbf06cf8c63def138c98f4b811fa", + "version" : "5.8.0" + } + }, + { + "identity" : "ohhttpstubs", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", + "state" : { + "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version" : "9.1.0" + } + } + ], + "version" : 2 +} diff --git a/fixtures/spm_package/Package.swift b/fixtures/spm_package/Package.swift new file mode 100644 index 00000000000..1a5afb74ea7 --- /dev/null +++ b/fixtures/spm_package/Package.swift @@ -0,0 +1,85 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + productDestinations: [ + "MyUIKitPackage": [ + .iPad, + .iPhone, + ], + ] + ) + +#endif + +let package = Package( + name: "MyPackage", + products: [ + .executable(name: "MyCLI", targets: ["MyCLI"]), + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "MyPackage", + targets: [ + "MyPackage", + "MyCommonPackage", + ] + ), + .library( + name: "MyUIKitPackage", + targets: [ + "MyUIKitPackage", + "MyCommonPackage", + ] + ), + ], + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire", exact: "5.8.0"), + .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", exact: "9.1.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "MyCLI", + dependencies: [ + "MyPackage", + ] + ), + .target( + name: "MyCommonPackage" + ), + .target( + name: "MyPackage", + dependencies: [ + "Alamofire", + ] + ), + .target( + name: "MyUIKitPackage", + dependencies: [ + "Alamofire", + ] + ), + .testTarget( + name: "MyPackageTests", + dependencies: [ + "MyPackage", + "MyCommonPackage", + .product(name: "OHHTTPStubs", package: "OHHTTPStubs"), + .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), + ] + ), + .testTarget( + name: "MyUIKitPackageTests", + dependencies: [ + "MyUIKitPackage", + "MyCommonPackage", + ] + ), + ] +) diff --git a/fixtures/spm_package/Sources/MyCLI/main.swift b/fixtures/spm_package/Sources/MyCLI/main.swift new file mode 100644 index 00000000000..f557129ae62 --- /dev/null +++ b/fixtures/spm_package/Sources/MyCLI/main.swift @@ -0,0 +1,3 @@ +import Foundation + +print("Hello from MYCLI") diff --git a/fixtures/spm_package/Sources/MyCommonPackage/MyCommonPackage.swift b/fixtures/spm_package/Sources/MyCommonPackage/MyCommonPackage.swift new file mode 100644 index 00000000000..69538f5f9c6 --- /dev/null +++ b/fixtures/spm_package/Sources/MyCommonPackage/MyCommonPackage.swift @@ -0,0 +1,7 @@ +import Foundation + +enum MyCommonPackage { + static func myCommonPackage() { + _ = UUID() + } +} diff --git a/fixtures/spm_package/Sources/MyPackage/MyPackage.swift b/fixtures/spm_package/Sources/MyPackage/MyPackage.swift new file mode 100644 index 00000000000..af8a56f24d9 --- /dev/null +++ b/fixtures/spm_package/Sources/MyPackage/MyPackage.swift @@ -0,0 +1,7 @@ +import Alamofire + +enum MyPackage { + static func myPackage() { + _ = AF.download("http://www.tuist.io") + } +} diff --git a/fixtures/spm_package/Sources/MyUIKitPackage/MyUIKitPackage.swift b/fixtures/spm_package/Sources/MyUIKitPackage/MyUIKitPackage.swift new file mode 100644 index 00000000000..828f6363458 --- /dev/null +++ b/fixtures/spm_package/Sources/MyUIKitPackage/MyUIKitPackage.swift @@ -0,0 +1,7 @@ +import UIKit + +enum MyUIKitPackage { + static func myUIKitPackage() { + _ = UIButton() + } +} diff --git a/fixtures/spm_package/Tests/MyPackageTests/MyPackageTests.swift b/fixtures/spm_package/Tests/MyPackageTests/MyPackageTests.swift new file mode 100644 index 00000000000..63ff8ec208d --- /dev/null +++ b/fixtures/spm_package/Tests/MyPackageTests/MyPackageTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import MyPackage + +final class MyPackageTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/fixtures/spm_package/Tests/MyUIKitPackageTests/MyUIKitPackageTests.swift b/fixtures/spm_package/Tests/MyUIKitPackageTests/MyUIKitPackageTests.swift new file mode 100644 index 00000000000..0d2bc5bda69 --- /dev/null +++ b/fixtures/spm_package/Tests/MyUIKitPackageTests/MyUIKitPackageTests.swift @@ -0,0 +1,13 @@ +import XCTest +@testable import MyCommonPackage +@testable import MyUIKitPackage + +final class MyUIKitPackageTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/fixtures/spm_package/Tuist/.gitkeep b/fixtures/spm_package/Tuist/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/tuist_plugin/Package.swift b/fixtures/tuist_plugin/Package.swift index 3c90b8f9589..02c47a9e441 100644 --- a/fixtures/tuist_plugin/Package.swift +++ b/fixtures/tuist_plugin/Package.swift @@ -24,14 +24,14 @@ let package = Package( .target( name: "CreateFile", dependencies: [ - .product(name: "ProjectAutomation", package: "ProjectAutomation") + .product(name: "ProjectAutomation", package: "ProjectAutomation"), ] ), .testTarget(name: "CreateFileTests"), .target( name: "InspectGraph", dependencies: [ - .product(name: "ProjectAutomation", package: "ProjectAutomation") + .product(name: "ProjectAutomation", package: "ProjectAutomation"), ] ), ] diff --git a/fixtures/tuist_plugin/README.md b/fixtures/tuist_plugin/README.md deleted file mode 100644 index 6cbda4c6e6b..00000000000 --- a/fixtures/tuist_plugin/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# ExampleTuistPlugin -This is an example Tuist plugin for testing purposes. diff --git a/fixtures/tuist_plugin/Sources/CreateFile/main.swift b/fixtures/tuist_plugin/Sources/CreateFile/main.swift index 6ab8847c5be..05c9198bede 100644 --- a/fixtures/tuist_plugin/Sources/CreateFile/main.swift +++ b/fixtures/tuist_plugin/Sources/CreateFile/main.swift @@ -5,4 +5,4 @@ try "File created with a plugin".write( atomically: true, encoding: .utf8 ) -print("File created with a plugin!") \ No newline at end of file +print("File created with a plugin!") diff --git a/fixtures/tuist_plugin/Sources/InspectGraph/main.swift b/fixtures/tuist_plugin/Sources/InspectGraph/main.swift index 2114a06761a..716bba8e1bf 100644 --- a/fixtures/tuist_plugin/Sources/InspectGraph/main.swift +++ b/fixtures/tuist_plugin/Sources/InspectGraph/main.swift @@ -3,7 +3,8 @@ import ProjectAutomation let graph: Graph if CommandLine.arguments.contains("--path"), - let path = CommandLine.arguments.last { + let path = CommandLine.arguments.last +{ graph = try Tuist.graph(at: path) } else { graph = try Tuist.graph() diff --git a/fixtures/tuist_plugin/Templates/custom_two/custom_two.swift b/fixtures/tuist_plugin/Templates/custom_two/custom_two.swift index f1fa949e85c..065c37003fd 100644 --- a/fixtures/tuist_plugin/Templates/custom_two/custom_two.swift +++ b/fixtures/tuist_plugin/Templates/custom_two/custom_two.swift @@ -11,7 +11,7 @@ let templateTwo = Template( description: "Custom template", attributes: [ nameAttributeTwo, - platformAttributeTwo + platformAttributeTwo, ], items: [ .string(path: "\(nameAttributeTwo)/custom.swift", contents: testContentsTwo), diff --git a/fixtures/tuist_plugin/Tests/CreateFileTests/Test.swift b/fixtures/tuist_plugin/Tests/CreateFileTests/Test.swift index 37a197b02ce..edd06abdace 100644 --- a/fixtures/tuist_plugin/Tests/CreateFileTests/Test.swift +++ b/fixtures/tuist_plugin/Tests/CreateFileTests/Test.swift @@ -5,4 +5,4 @@ final class CreateFileTests: XCTestCase { func test_example() { XCTAssertEqual("CreateFile", "CreateFile") } -} \ No newline at end of file +} diff --git a/fixtures/tvos_app_with_extensions/Project.swift b/fixtures/tvos_app_with_extensions/Project.swift index e6b89102586..3580cb65645 100644 --- a/fixtures/tvos_app_with_extensions/Project.swift +++ b/fixtures/tvos_app_with_extensions/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .tvOS, + destinations: [.appleTv], product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", @@ -14,9 +14,9 @@ let project = Project( .target(name: "TopShelfExtension"), ] ), - Target( + .target( name: "TopShelfExtension", - platform: .tvOS, + destinations: [.appleTv], product: .tvTopShelfExtension, bundleId: "io.tuist.App.TopShelfExtension", infoPlist: .extendingDefault(with: [ @@ -30,9 +30,9 @@ let project = Project( dependencies: [ ] ), - Target( + .target( name: "StaticFramework", - platform: .iOS, + destinations: .iOS, product: .staticFramework, bundleId: "io.tuist.App.StaticFramework", infoPlist: .default, diff --git a/fixtures/tvos_app_with_uitest/Project.swift b/fixtures/tvos_app_with_uitest/Project.swift index 3fa11ee96ea..b931d715737 100644 --- a/fixtures/tvos_app_with_uitest/Project.swift +++ b/fixtures/tvos_app_with_uitest/Project.swift @@ -3,18 +3,18 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .tvOS, + destinations: [.appleTv], product: .app, bundleId: "io.tuist.App", - deploymentTarget: .tvOS(targetVersion: "14.0"), + deploymentTargets: .tvOS("14.0"), infoPlist: .default, sources: .paths([.relativeToManifest("App/Sources/**")]) ), - Target( + .target( name: "AppUITests", - platform: .tvOS, + destinations: [.appleTv], product: .uiTests, bundleId: "io.tuist.AppUITests", infoPlist: "UITests.plist", diff --git a/fixtures/visionos_app/Project.swift b/fixtures/visionos_app/Project.swift index e29a907f3c8..875347a76e9 100644 --- a/fixtures/visionos_app/Project.swift +++ b/fixtures/visionos_app/Project.swift @@ -3,9 +3,9 @@ import ProjectDescription let project = Project( name: "App", targets: [ - Target( + .target( name: "App", - platform: .visionOS, + destinations: [.appleVision], product: .app, bundleId: "io.tuist.App", infoPlist: "Support/Info.plist", @@ -15,9 +15,9 @@ let project = Project( // "Resources/**" ] ), - Target( + .target( name: "AppTests", - platform: .visionOS, + destinations: [.appleVision], product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: "Support/Tests.plist", diff --git a/fixtures/workspace_with_multiple_projects/ProjectA/Project.swift b/fixtures/workspace_with_multiple_projects/ProjectA/Project.swift index 6525b8b52b5..a5d5daa3254 100644 --- a/fixtures/workspace_with_multiple_projects/ProjectA/Project.swift +++ b/fixtures/workspace_with_multiple_projects/ProjectA/Project.swift @@ -4,18 +4,18 @@ let project = Project( name: "ProjectA", organizationName: "tuist.io", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app", deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone), infoPlist: .default, sources: ["Targets/App/Sources/**"] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: .default, diff --git a/fixtures/workspace_with_multiple_projects/ProjectB/Project.swift b/fixtures/workspace_with_multiple_projects/ProjectB/Project.swift index 75746c35bae..2eea208dacc 100644 --- a/fixtures/workspace_with_multiple_projects/ProjectB/Project.swift +++ b/fixtures/workspace_with_multiple_projects/ProjectB/Project.swift @@ -4,18 +4,18 @@ let project = Project( name: "ProjectB", organizationName: "tuist.io", targets: [ - Target( + .target( name: "App", - platform: .iOS, + destinations: [.iPhone], product: .app, bundleId: "io.tuist.app", - deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone), + deploymentTargets: .iOS("13.0"), infoPlist: .default, sources: ["Targets/App/Sources/**"] ), - Target( + .target( name: "AppTests", - platform: .iOS, + destinations: .iOS, product: .unitTests, bundleId: "io.tuist.AppTests", infoPlist: .default, diff --git a/fixtures/workspace_with_test_locale/Project.swift b/fixtures/workspace_with_test_locale/Project.swift index 2beee910048..212d66689da 100644 --- a/fixtures/workspace_with_test_locale/Project.swift +++ b/fixtures/workspace_with_test_locale/Project.swift @@ -13,7 +13,7 @@ let project = Project( targets: [ .init( name: "App", - platform: .iOS, + destinations: .iOS, product: .app, bundleId: "io.tuist.app" ), diff --git a/fixtures/xcode_app/.gitignore b/fixtures/xcode_app/.gitignore new file mode 100644 index 00000000000..314a1182e38 --- /dev/null +++ b/fixtures/xcode_app/.gitignore @@ -0,0 +1 @@ +!App.xcodeproj \ No newline at end of file diff --git a/fixtures/xcode_app/App.xcodeproj/project.pbxproj b/fixtures/xcode_app/App.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..a413f71cb7a --- /dev/null +++ b/fixtures/xcode_app/App.xcodeproj/project.pbxproj @@ -0,0 +1,368 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + F8DE78AF2C4E957C0023B168 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE78AE2C4E957C0023B168 /* App.swift */; }; + F8DE78B12C4E957C0023B168 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE78B02C4E957C0023B168 /* ContentView.swift */; }; + F8DE78B32C4E957D0023B168 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F8DE78B22C4E957D0023B168 /* Assets.xcassets */; }; + F8DE78B72C4E957D0023B168 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F8DE78B62C4E957D0023B168 /* Preview Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + F8DE78AB2C4E957C0023B168 /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F8DE78AE2C4E957C0023B168 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + F8DE78B02C4E957C0023B168 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + F8DE78B22C4E957D0023B168 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F8DE78B42C4E957D0023B168 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; + F8DE78B62C4E957D0023B168 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F8DE78A82C4E957C0023B168 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F8DE78A22C4E957C0023B168 = { + isa = PBXGroup; + children = ( + F8DE78AD2C4E957C0023B168 /* App */, + F8DE78AC2C4E957C0023B168 /* Products */, + ); + sourceTree = ""; + }; + F8DE78AC2C4E957C0023B168 /* Products */ = { + isa = PBXGroup; + children = ( + F8DE78AB2C4E957C0023B168 /* App.app */, + ); + name = Products; + sourceTree = ""; + }; + F8DE78AD2C4E957C0023B168 /* App */ = { + isa = PBXGroup; + children = ( + F8DE78AE2C4E957C0023B168 /* App.swift */, + F8DE78B02C4E957C0023B168 /* ContentView.swift */, + F8DE78B22C4E957D0023B168 /* Assets.xcassets */, + F8DE78B42C4E957D0023B168 /* App.entitlements */, + F8DE78B52C4E957D0023B168 /* Preview Content */, + ); + path = App; + sourceTree = ""; + }; + F8DE78B52C4E957D0023B168 /* Preview Content */ = { + isa = PBXGroup; + children = ( + F8DE78B62C4E957D0023B168 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F8DE78AA2C4E957C0023B168 /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = F8DE78BA2C4E957D0023B168 /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + F8DE78A72C4E957C0023B168 /* Sources */, + F8DE78A82C4E957C0023B168 /* Frameworks */, + F8DE78A92C4E957C0023B168 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App; + productName = App; + productReference = F8DE78AB2C4E957C0023B168 /* App.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F8DE78A32C4E957C0023B168 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + F8DE78AA2C4E957C0023B168 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = F8DE78A62C4E957C0023B168 /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F8DE78A22C4E957C0023B168; + productRefGroup = F8DE78AC2C4E957C0023B168 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F8DE78AA2C4E957C0023B168 /* App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F8DE78A92C4E957C0023B168 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F8DE78B72C4E957D0023B168 /* Preview Assets.xcassets in Resources */, + F8DE78B32C4E957D0023B168 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F8DE78A72C4E957C0023B168 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F8DE78B12C4E957C0023B168 /* ContentView.swift in Sources */, + F8DE78AF2C4E957C0023B168 /* App.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + F8DE78B82C4E957D0023B168 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F8DE78B92C4E957D0023B168 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + F8DE78BB2C4E957D0023B168 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"App/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.App; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F8DE78BC2C4E957D0023B168 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"App/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.App; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F8DE78A62C4E957C0023B168 /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F8DE78B82C4E957D0023B168 /* Debug */, + F8DE78B92C4E957D0023B168 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F8DE78BA2C4E957D0023B168 /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F8DE78BB2C4E957D0023B168 /* Debug */, + F8DE78BC2C4E957D0023B168 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F8DE78A32C4E957C0023B168 /* Project object */; +} diff --git a/fixtures/xcode_app/App/App.entitlements b/fixtures/xcode_app/App/App.entitlements new file mode 100644 index 00000000000..f2ef3ae0265 --- /dev/null +++ b/fixtures/xcode_app/App/App.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/fixtures/xcode_app/App/App.swift b/fixtures/xcode_app/App/App.swift new file mode 100644 index 00000000000..7cb2ea61064 --- /dev/null +++ b/fixtures/xcode_app/App/App.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/xcode_app/App/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/xcode_app/App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/xcode_app/App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_app/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/xcode_app/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..532cd729c61 --- /dev/null +++ b/fixtures/xcode_app/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_app/App/Assets.xcassets/Contents.json b/fixtures/xcode_app/App/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/xcode_app/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_app/App/ContentView.swift b/fixtures/xcode_app/App/ContentView.swift new file mode 100644 index 00000000000..b000a7e4618 --- /dev/null +++ b/fixtures/xcode_app/App/ContentView.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/fixtures/xcode_app/App/Preview Content/Preview Assets.xcassets/Contents.json b/fixtures/xcode_app/App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/xcode_app/App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_app/README.md b/fixtures/xcode_app/README.md new file mode 100644 index 00000000000..11847e3cfc5 --- /dev/null +++ b/fixtures/xcode_app/README.md @@ -0,0 +1,3 @@ +# Xcode app + +A simple app created with Xcode. This fixture is _not_ using Tuist projects and it is meant to test features that don't require Tuist projects, such as `tuist share`. \ No newline at end of file diff --git a/fixtures/xcode_app/Tuist/Config.swift b/fixtures/xcode_app/Tuist/Config.swift new file mode 100644 index 00000000000..2080f0c8189 --- /dev/null +++ b/fixtures/xcode_app/Tuist/Config.swift @@ -0,0 +1,9 @@ +import ProjectDescription + +let config = Config( + fullHandle: "tuist/xcode_app", + url: "https://canary.tuist.io", + generationOptions: .options( + optionalAuthentication: true + ) +) diff --git a/make/tasks/docs/build.sh b/make/tasks/docs/build.sh deleted file mode 100755 index 6b8c74ea09d..00000000000 --- a/make/tasks/docs/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -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 deleted file mode 100755 index 0ede39049cc..00000000000 --- a/make/tasks/docs/preview.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -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/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 deleted file mode 100755 index db195f763b3..00000000000 --- a/make/tasks/github/build-docc.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/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/github/cancel-workflows.sh b/make/tasks/github/cancel-workflows.sh deleted file mode 100755 index b0bc37250c5..00000000000 --- a/make/tasks/github/cancel-workflows.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -OWNER=tuist -REPO=tuist - -if [[ -n "$GITHUB_TOKEN" ]]; then - TOKEN="$GITHUB_TOKEN" -else - TOKEN=$(echo "url=https://github.com" | git credential fill | grep password | cut -d '=' -f 2) -fi - -echo $TOKEN - -workflow_ids=$(curl -s -H "Authorization: token $TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/$OWNER/$REPO/actions/runs \ - | jq '.workflow_runs[] | select(.status == "queued") | .id') - -for id in $workflow_ids; do - echo "Canceling workflow run with ID $id" - curl -s -X POST -H "Authorization: token $TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/$OWNER/$REPO/actions/runs/$id/cancel -done \ No newline at end of file diff --git a/make/tasks/tuist/build.sh b/make/tasks/tuist/build.sh deleted file mode 100755 index da82c384577..00000000000 --- a/make/tasks/tuist/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist fetch --path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist build --path $ROOT_DIR --generate $@ \ No newline at end of file diff --git a/make/tasks/tuist/edit.sh b/make/tasks/tuist/edit.sh deleted file mode 100755 index a69ab308862..00000000000 --- a/make/tasks/tuist/edit.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist edit --path $ROOT_DIR --only-current-directory \ No newline at end of file diff --git a/make/tasks/tuist/generate.sh b/make/tasks/tuist/generate.sh deleted file mode 100755 index cac87970c5c..00000000000 --- a/make/tasks/tuist/generate.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist fetch --path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist generate --path $ROOT_DIR $@ diff --git a/make/tasks/tuist/run.sh b/make/tasks/tuist/run.sh deleted file mode 100755 index dcd8ceace0a..00000000000 --- a/make/tasks/tuist/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist $@ \ No newline at end of file diff --git a/make/tasks/tuist/test.sh b/make/tasks/tuist/test.sh deleted file mode 100755 index 802e38cd1fc..00000000000 --- a/make/tasks/tuist/test.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist fetch --path $ROOT_DIR -$ROOT_DIR/.build/debug/tuist test --path $ROOT_DIR $@ \ No newline at end of file diff --git a/make/tasks/workspace/benchmark.sh b/make/tasks/workspace/benchmark.sh deleted file mode 100755 index 2fee117df1b..00000000000 --- a/make/tasks/workspace/benchmark.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -FIXTURES_DIRECTORY=$ROOT_DIR/fixtures -source $ROOT_DIR/make/utilities/setup.sh - -temp_dir=$(mktemp -d) - -# Define a cleanup function -cleanup() { - echo "Deleting the temporary directory..." - rm -rf "$temp_dir" - echo "Temporary directory deleted." -} - -echo "$(format_section "Building the fixture generator and benchmark tools")" -swift build --package-path $ROOT_DIR --product tuistfixturegenerator -c release -swift build --package-path $ROOT_DIR --product tuistbenchmark -c release - -echo "$(format_section "Generating a Tuist project with 50 projects")" -DIRECTORY_50_PROJECTS=$temp_dir/50_projects -$ROOT_DIR/.build/release/tuistfixturegenerator generate --path $DIRECTORY_50_PROJECTS --projects 50 -echo "$(format_section "Generating a Tuist project with 2 projects and 2000 sources")" -DIRECTORY_2000_SOURCES=$temp_dir/50_projects -$ROOT_DIR/.build/release/tuistfixturegenerator generate --path $DIRECTORY_2000_SOURCES --projects 2 --sources 2000 - -FIXTURES_JSON_PATH=$temp_dir/fixtures.json -FIXTURES_LIST=($DIRECTORY_50_PROJECTS $DIRECTORY_2000_SOURCES $FIXTURES_DIRECTORY/ios_app_with_static_frameworks $FIXTURES_DIRECTORY/ios_app_with_framework_and_resources $FIXTURES_DIRECTORY/ios_app_with_transitive_framework $FIXTURES_DIRECTORY/ios_app_with_xcframeworks) - -echo "$(format_section "Writing the fixtures.json file")" -FIXTURES_JSON=$(jq -n --argjson arr "$(printf '%s\n' "${FIXTURES_LIST[@]}" | jq -R . | jq -s .)" '{"paths": $arr}') - -echo $FIXTURES_JSON > $FIXTURES_JSON_PATH - -echo "$(format_section "Building tuist and tuistenv")" -swift build --package-path $ROOT_DIR --product tuist -c release -swift build --package-path $ROOT_DIR --product tuistenv -c release -swift build --package-path $ROOT_DIR --product ProjectDescription -c release - -echo "$(format_section "Benchmarking")" -$ROOT_DIR/.build/release/tuistbenchmark benchmark -b $ROOT_DIR/.build/release/tuist -r $ROOT_DIR/.build/release/tuistenv --fixture-list $FIXTURES_JSON_PATH --format markdown \ No newline at end of file diff --git a/make/tasks/workspace/build-with-spm.sh b/make/tasks/workspace/build-with-spm.sh deleted file mode 100755 index 40215074807..00000000000 --- a/make/tasks/workspace/build-with-spm.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -DERIVED_DATA_PATH=$($ROOT_DIR/make/utilities/derived_data_path.sh) - -swift build --package-path $ROOT_DIR --build-path $ROOT_DIR/.build $@ \ No newline at end of file diff --git a/make/tasks/workspace/build/tuistbenchmark.sh b/make/tasks/workspace/build/tuistbenchmark.sh deleted file mode 100755 index e5ada920b5c..00000000000 --- a/make/tasks/workspace/build/tuistbenchmark.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR --target tuistbenchmark $@ \ No newline at end of file diff --git a/make/tasks/workspace/build/tuistfixturegenerator.sh b/make/tasks/workspace/build/tuistfixturegenerator.sh deleted file mode 100755 index 194845e31bb..00000000000 --- a/make/tasks/workspace/build/tuistfixturegenerator.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../../utilities/root_dir.sh) - -swift build --package-path $ROOT_DIR --target tuistfixturegenerator $@ \ No newline at end of file diff --git a/make/tasks/workspace/clean.sh b/make/tasks/workspace/clean.sh deleted file mode 100755 index 366eb4b3f4a..00000000000 --- a/make/tasks/workspace/clean.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -DERIVED_DATA_PATH=$($ROOT_DIR/make/utilities/derived_data_path.sh) - -rm -rf $ROOT_DIR/.build -for dir in "$DERIVED_DATA_PATH"tuist-*; do - # Check if it is a directory before deleting - if [[ -d "$dir" ]]; then - echo "Deleting directory: $dir" - rm -rf "$dir" - fi -done -for dir in "$DERIVED_DATA_PATH"Tuist-*; do - # Check if it is a directory before deleting - if [[ -d "$dir" ]]; then - echo "Deleting directory: $dir" - rm -rf "$dir" - fi -done diff --git a/make/tasks/workspace/generate-fixture.sh b/make/tasks/workspace/generate-fixture.sh deleted file mode 100755 index f285f127909..00000000000 --- a/make/tasks/workspace/generate-fixture.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -source $ROOT_DIR/make/utilities/setup.sh - -path=$(pwd) -projects="" -targets="" -sources="" - -# Function to display usage -usage() { - echo "Usage: $0 --path --projects --targets --sources " - exit 1 -} - -while [[ "$#" -gt 0 ]]; do - case $1 in - --path) path="$2"; shift ;; - --projects) projects="$2"; shift ;; - --targets) targets="$2"; shift ;; - --sources) sources="$2"; shift ;; - *) echo "Unknown parameter passed: $1"; usage ;; - esac - shift -done - -if [ -z "$projects" ] || [ -z "$targets" ] || [ -z "$sources" ]; then - usage -fi - -echo "$(format_section "Generating fixture")" - -echo "Path: $path" -echo "Projects: $projects" -echo "Targets: $targets" -echo "Sources: $sources" - -swift run --package-path $ROOT_DIR tuistfixturegenerator generate --path $path --projects $projects --targets $targets --sources $sources \ No newline at end of file diff --git a/make/tasks/workspace/lint-fix.sh b/make/tasks/workspace/lint-fix.sh deleted file mode 100755 index eed7fb362ca..00000000000 --- a/make/tasks/workspace/lint-fix.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swiftformat $ROOT_DIR -swiftlint lint --fix --quiet --config $ROOT_DIR/.swiftlint.yml $ROOT_DIR/Sources diff --git a/make/tasks/workspace/lint.sh b/make/tasks/workspace/lint.sh deleted file mode 100755 index 95594ea91b4..00000000000 --- a/make/tasks/workspace/lint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) - -swiftformat $ROOT_DIR --lint -swiftlint lint --quiet --config $ROOT_DIR/.swiftlint.yml $ROOT_DIR/Sources diff --git a/make/tasks/workspace/lint/lockfiles.sh b/make/tasks/workspace/lint/lockfiles.sh deleted file mode 100755 index f0f0d601c54..00000000000 --- a/make/tasks/workspace/lint/lockfiles.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../../utilities/root_dir.sh) - -assert_same_packages_count() { - spm_count=$(jq '.pins | length' "$spm_lockfile") - tuist_count=$(jq '.pins | length' "$tuist_lockfile") - - if [ "$spm_count" -ne "$tuist_count" ]; then - echo "The number of packages in the Package.resolved files don't match." - return 1 - fi - return 0 -} - -assert_same_versions() { - mismatched_packages=() - - for package_name in $(jq -r '.pins[] .identity' "$spm_lockfile"); do - if jq -e ".pins[] | select(.identity == \"$package_name\")" "$tuist_lockfile" > /dev/null; then - tuist_revision=$(jq -r ".pins[] | select(.identity == \"$package_name\") .state.revision" "$tuist_lockfile") - spm_revision=$(jq -r ".pins[] | select(.identity == \"$package_name\") .state.revision" "$spm_lockfile") - - if [ "$tuist_revision" != "$spm_revision" ]; then - mismatched_packages+=("$package_name") - fi - fi - done - - if [ "${#mismatched_packages[@]}" -ne 0 ]; then - echo "There's a mismatch between the revision of the following packages in the Package.resolved files: ${mismatched_packages[*]}" - return 1 - fi - - return 0 -} - -spm_lockfile="$ROOT_DIR/Package.resolved" -tuist_lockfile="$ROOT_DIR/Tuist/Package.resolved" - -assert_same_packages_count -status1=$? - -assert_same_versions -status2=$? - -if [ $status1 -ne 0 ] || [ $status2 -ne 0 ]; then - exit 1 -fi diff --git a/make/tasks/workspace/release/bundle.sh b/make/tasks/workspace/release/bundle.sh deleted file mode 100755 index 1e782577cd5..00000000000 --- a/make/tasks/workspace/release/bundle.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/bash - -set -e - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../../utilities/root_dir.sh) -source $ROOT_DIR/make/utilities/setup.sh -XCODE_PATH_SCRIPT_PATH=$SCRIPT_DIR/../../../utilities/xcode_path.sh -BUILD_DIRECTORY=$ROOT_DIR/build -# Xcode 15 has a bug that causes the /var/folders... temporary directory, which is a symlink to -# /private/var/folders to crash Xcode. -TMP_DIR=/private$(mktemp -d) -trap "rm -rf $TMP_DIR" EXIT # Ensures it gets deleted -XCODE_VERSION=$(cat $ROOT_DIR/.xcode-version) -LIBRARIES_XCODE_VERSION=$(cat $ROOT_DIR/.xcode-version-libraries) -BUILD_DIR=$ROOT_DIR/build - -echo "$(format_section "Building release into $BUILD_DIRECTORY")" - -XCODE_PATH=/Applications/Xcode-15.0.1.app -# TODO: -# Remove the hardcoded path. The GitHub hosted runners have the dev tools of 15.0.1 -# and causes the line below to output a non-existing path. Once they've installed 15.0.1 -# in the CI environments, the hardcoded path should not be necessary. -# That will happen when this PR is merged and deployed: https://github.com/actions/runner-images/pull/8601 -# XCODE_PATH=$($XCODE_PATH_SCRIPT_PATH --version $XCODE_VERSION) -XCODE_LIBRARIES_PATH=$($XCODE_PATH_SCRIPT_PATH --version $LIBRARIES_XCODE_VERSION) - -echo "Static executables will be built with $XCODE_PATH" -echo "Dynamic bundles will be built with $XCODE_PATH" - -rm -rf $ROOT_DIR/Tuist.xcodeproj -rm -rf $ROOT_DIR/Tuist.xcworkspace -rm -rf $BUILD_DIRECTORY -mkdir -p $BUILD_DIRECTORY - -build_fat_release_library() { - ( - cd $ROOT_DIR || exit 1 - DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -resolvePackageDependencies - DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -scheme $1 -configuration Release -destination platform=macosx BUILD_LIBRARY_FOR_DISTRIBUTION=YES ARCHS='arm64 x86_64' BUILD_DIR=$TMP_DIR clean build - - # We remove the PRODUCT.swiftmodule/Project directory because - # this directory contains objects that are not stable across Swift releases. - rm -rf $TMP_DIR/Release/$1.swiftmodule/Project - cp -r $TMP_DIR/Release/PackageFrameworks/$1.framework $BUILD_DIRECTORY/$1.framework - mkdir -p $BUILD_DIRECTORY/$1.framework/Modules - cp -r $TMP_DIR/Release/$1.swiftmodule $BUILD_DIRECTORY/$1.framework/Modules/$1.swiftmodule - cp -r $TMP_DIR/Release/$1.framework.dSYM $BUILD_DIRECTORY/$1.framework.dSYM - ) -} - -build_xcframework_library() { - ( - cd $ROOT_DIR || exit 1 - DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -resolvePackageDependencies - DEVELOPER_DIR=$XCODE_LIBRARIES_PATH xcrun xcodebuild -scheme $1 -configuration Release -destination platform=macosx BUILD_LIBRARY_FOR_DISTRIBUTION=YES ARCHS='arm64 x86_64' BUILD_DIR=$TMP_DIR clean build - - xcodebuild -create-xcframework \ - -framework $TMP_DIR/Release/PackageFrameworks/$1.framework \ - -output $BUILD_DIRECTORY/$1.xcframework - cp -r $TMP_DIR/Release/$1.framework.dSYM $BUILD_DIRECTORY/$1.xcframework.dSYM - ) -} - -build_fat_release_binary() { - ( - cd $ROOT_DIR || exit 1 - ARM64_TARGET=arm64-apple-macosx - X86_64_TARGET=x86_64-apple-macosx - - DEVELOPER_DIR=$XCODE_PATH swift build \ - --configuration release \ - --disable-sandbox \ - --product $1 \ - --package-path $2 \ - --build-path $TMP_DIR/$1 \ - --triple $ARM64_TARGET - - DEVELOPER_DIR=$XCODE_PATH swift build \ - --configuration release \ - --disable-sandbox \ - --product $1 \ - --package-path $2 \ - --build-path $TMP_DIR/$1 \ - --triple $X86_64_TARGET - - mkdir -p $3 - - DEVELOPER_DIR=$XCODE_PATH lipo -create \ - -output $3/$1 \ - $TMP_DIR/$1/$ARM64_TARGET/release/$1 \ - $TMP_DIR/$1/$X86_64_TARGET/release/$1 - ) -} - -echo "$(format_section "Building")" - -echo "$(format_subsection "Building ProjectDescription framework")" -build_fat_release_library "ProjectDescription" - -echo "$(format_subsection "Building ProjectAutomation framework")" -build_xcframework_library "ProjectAutomation" - -echo "$(format_subsection "Building tuist executable")" -build_fat_release_binary "tuist" $ROOT_DIR $BUILD_DIRECTORY - -echo "$(format_subsection "Building tuistenv executable")" -build_fat_release_binary "tuistenv" $ROOT_DIR $BUILD_DIRECTORY - -echo "$(format_section "Copying assets")" - -echo "$(format_subsection "Copying Tuist's templates")" -cp -r $ROOT_DIR/Templates $BUILD_DIRECTORY/Templates - -echo "$(format_subsection "Copy Swift libraries into the Tuist binary")" -swift stdlib-tool --copy --scan-executable $BUILD_DIRECTORY/tuist --platform macosx --destination $BUILD_DIRECTORY - -echo "$(format_section "Bundling")" - -( - cd $BUILD_DIRECTORY || exit 1 - echo "$(format_subsection "Bundling tuistenv.zip")" - zip -q -r --symlinks tuistenv.zip tuistenv - echo "$(format_subsection "Bundling tuist.zip")" - zip -q -r --symlinks tuist.zip tuist libswift_Concurrency.dylib ProjectAutomation.xcframework ProjectAutomation.xcframework.dSYM ProjectDescription.framework ProjectDescription.framework.dSYM Templates vendor - echo "$(format_subsection "Bundling ProjectDescription.framework.zip")" - zip -q -r --symlinks ProjectDescription.framework.zip ProjectDescription.framework ProjectDescription.framework.dSYM - echo "$(format_subsection "Bundling ProjectAutomation.xcframework.zip")" - zip -q -r --symlinks ProjectAutomation.xcframework.zip ProjectAutomation.xcframework ProjectAutomation.xcframework.dSYM - - rm -rf tuist tuistenv ProjectAutomation.xcframework ProjectAutomation.xcframework.dSYM ProjectDescription.framework ProjectDescription.framework.dSYM Templates vendor - - : > SHASUMS256.txt - : > SHASUMS512.txt - - for file in *; do - if [ -f "$file" ]; then - if [[ "$file" == "SHASUMS256.txt" || "$file" == "SHASUMS512.txt" ]]; then - continue - fi - echo "$(shasum -a 256 "$file" | awk '{print $1}') ./$file" >> SHASUMS256.txt - echo "$(shasum -a 512 "$file" | awk '{print $1}') ./$file" >> SHASUMS512.txt - fi - done -) diff --git a/make/utilities/derived_data_path.sh b/make/utilities/derived_data_path.sh deleted file mode 100755 index b6b914ed333..00000000000 --- a/make/utilities/derived_data_path.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" - -DERIVED_DATA_PATH=$(defaults read com.apple.dt.Xcode IDECustomDerivedDataLocation 2>/dev/null) - -# Check if the path is set -if [ -z "$DERIVED_DATA_PATH" ]; then - echo "$HOME/Library/Developer/Xcode/DerivedData/" -else - echo "$DERIVED_DATA_PATH" -fi \ No newline at end of file diff --git a/make/utilities/root_dir.sh b/make/utilities/root_dir.sh deleted file mode 100755 index e2e17984da2..00000000000 --- a/make/utilities/root_dir.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" -echo $ROOT_DIR \ No newline at end of file diff --git a/script/install b/script/install index 84145f1482e..a84b76a2b8b 100755 --- a/script/install +++ b/script/install @@ -20,8 +20,9 @@ warn() { printf "${tty_red}Warning${tty_reset}: %s\n" "$(chomp "$1")" } -# The line below extracts the version from the list of tags in the Tuist repository. -LATEST_VERSION=$(git ls-remote -t --sort=v:refname https://github.com/tuist/tuist.git | sed -ne '$s/.*tags\/\(.*\)/\1/p') +# The line below extracts the latest 3.x version from the list of tags in the Tuist repository. +# Tuist v3 is the last major to provide tuistenv. +LATEST_VERSION=$(git ls-remote -t --sort=v:refname https://github.com/tuist/tuist.git | grep 'tags/3\.' | sed -ne '$s/.*tags\/\(.*\)/\1/p') ohai "Downloading tuistenv..." [ -f /tmp/tuistenv.zip ] && rm /tmp/tuistenv.zip [ -f /tmp/tuistenv ] && rm /tmp/tuistenv @@ -56,4 +57,4 @@ rm -rf /tmp/tuistenv rm /tmp/tuistenv.zip ohai "tuistenv installed. Try running 'tuist'" -ohai "Check out the documentation at https://docs.tuist.io/" +ohai "Check out the documentation at https://docs.tuist.io/" \ No newline at end of file diff --git a/script/uninstall b/script/uninstall index 1a138402d23..8c7ea845e02 100755 --- a/script/uninstall +++ b/script/uninstall @@ -34,4 +34,4 @@ sudo_if_install_dir_not_writeable() { sudo_if_install_dir_not_writeable "rm -rf /usr/local/bin/tuist" rm -rf ~/.tuist -ohai "Tuist uninstalled" +ohai "Tuist uninstalled" \ No newline at end of file