├── .github └── workflows │ ├── conventional-pr.yml │ ├── deploy.yml │ ├── docs.yml │ ├── release.yml │ └── rosalind.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── .xcode-version ├── CHANGELOG.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── Rosalind │ ├── AppBundle.swift │ ├── AppBundleLoader.swift │ ├── AssetUtilController.swift │ ├── Extensions │ ├── FileHandle+Read.swift │ └── Sequence+Concurrency.swift │ ├── Rosalind.swift │ ├── RosalindReport.swift │ └── ShasumCalculator.swift ├── Tests └── RosalindTests │ ├── Extensions │ └── Snapshotting+RosalindReport.swift │ ├── RosalindAcceptanceTests.swift │ ├── RosalindTests.swift │ └── __Snapshots__ │ └── RosalindAcceptanceTests │ ├── ios_app.1 │ ├── ios_app_ipa.1 │ └── ios_app_xcarchive.1 ├── cliff.toml ├── docs ├── .vitepress │ ├── config.mjs │ └── icons.mjs ├── api │ ├── rosalind.md │ └── schema.md ├── index.md ├── package.json ├── public │ ├── favicon.ico │ └── logo.png └── quick-start │ └── add-dependency.md ├── fixtures └── ios_app │ ├── App.app │ ├── App │ ├── App.debug.dylib │ ├── Assets.car │ ├── Info.plist │ ├── PkgInfo │ ├── _CodeSignature │ │ └── CodeResources │ ├── __preview.dylib │ └── embedded.mobileprovision │ ├── App.ipa │ ├── App.xcarchive │ ├── Info.plist │ └── Products │ │ └── Applications │ │ └── App.app │ │ ├── App │ │ ├── Assets.car │ │ ├── Info.plist │ │ ├── PkgInfo │ │ ├── _CodeSignature │ │ └── CodeResources │ │ └── embedded.mobileprovision │ ├── App.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── App │ ├── AppApp.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── Image.imageset │ │ ├── Contents.json │ │ ├── Tuist-Filled.png │ │ ├── Tuist-Filled@2x.png │ │ └── Tuist-Filled@3x.png │ ├── ContentView.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── mise.toml ├── mise └── tasks │ ├── build │ ├── build-fixture.sh │ ├── build-linux │ ├── cache │ ├── docs │ ├── build │ ├── deploy │ ├── dev │ └── preview │ ├── install │ ├── lint │ ├── test │ └── test-linux ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── renovate.json /.github/workflows/conventional-pr.yml: -------------------------------------------------------------------------------- 1 | name: conventional-pr 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | - master 7 | types: 8 | - opened 9 | - edited 10 | - synchronize 11 | jobs: 12 | lint-pr: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: CondeNast/conventional-pull-request-action@v0.2.0 17 | with: 18 | commitTitleMatch: false 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | 7 | concurrency: 8 | group: deploy-${{ github.head_ref }} 9 | cancel-in-progress: true 10 | 11 | env: 12 | MISE_EXPERIMENTAL: 1 13 | 14 | jobs: 15 | docs: 16 | name: Docs 17 | runs-on: ubuntu-latest 18 | env: 19 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 20 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 23 | - uses: jdx/mise-action@v2 24 | with: 25 | experimental: true 26 | - run: mise run install 27 | - run: mise run docs:deploy -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: docs-${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | MISE_EXPERIMENTAL: 1 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | runs-on: 'ubuntu-latest' 20 | timeout-minutes: 15 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: jdx/mise-action@v2 24 | - name: Install dependencies 25 | run: mise run install 26 | - name: Build 27 | run: mise run docs:build -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: "The version to release" 11 | type: string 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: read 16 | statuses: write 17 | packages: write 18 | 19 | jobs: 20 | release: 21 | name: Release 22 | runs-on: "ubuntu-latest" 23 | timeout-minutes: 15 24 | if: "!startsWith(github.event.head_commit.message, '[Release]')" 25 | steps: 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 27 | with: 28 | fetch-depth: 0 29 | - uses: jdx/mise-action@v2 30 | with: 31 | experimental: true 32 | - name: Check if there are releasable changes 33 | id: is-releasable 34 | run: | 35 | bumped_output=$(git cliff --bump) 36 | changelog_content=$(cat CHANGELOG.md) 37 | 38 | bumped_hash=$(echo -n "$bumped_output" | shasum -a 256 | awk '{print $1}') 39 | changelog_hash=$(echo -n "$changelog_content" | shasum -a 256 | awk '{print $1}') 40 | 41 | if [ "$bumped_hash" != "$changelog_hash" ]; then 42 | echo "should-release=true" >> $GITHUB_ENV 43 | else 44 | echo "should-release=false" >> $GITHUB_ENV 45 | fi 46 | 47 | - name: Get next version 48 | id: next-version 49 | if: env.should-release == 'true' 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | run: echo "NEXT_VERSION=$(git cliff --bumped-version)" >> "$GITHUB_OUTPUT" 53 | - name: Get release notes 54 | id: release-notes 55 | if: env.should-release == 'true' 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: | 59 | echo "RELEASE_NOTES<> "$GITHUB_OUTPUT" 60 | git cliff --unreleased >> "$GITHUB_OUTPUT" 61 | echo "EOF" >> "$GITHUB_OUTPUT" 62 | - name: Update CHANGELOG.md 63 | if: env.should-release == 'true' 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | run: git cliff --bump -o CHANGELOG.md 67 | - name: Commit changes 68 | id: auto-commit-action 69 | uses: stefanzweifel/git-auto-commit-action@v5 70 | if: env.should-release == 'true' 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.TUIST_FILE_SYSTEM_RELEASE_TOKEN }} 73 | with: 74 | commit_options: "--allow-empty" 75 | tagging_message: ${{ steps.next-version.outputs.NEXT_VERSION }} 76 | skip_dirty_check: true 77 | commit_message: "[Release] Rosalind ${{ steps.next-version.outputs.NEXT_VERSION }}" 78 | - name: Create GitHub Release 79 | uses: softprops/action-gh-release@v2 80 | if: env.should-release == 'true' 81 | with: 82 | draft: false 83 | repository: tuist/Rosalind 84 | name: ${{ steps.next-version.outputs.NEXT_VERSION }} 85 | tag_name: ${{ steps.next-version.outputs.NEXT_VERSION }} 86 | body: ${{ steps.release-notes.outputs.RELEASE_NOTES }} 87 | target_commitish: ${{ steps.auto-commit-action.outputs.commit_hash }} 88 | -------------------------------------------------------------------------------- /.github/workflows/rosalind.yml: -------------------------------------------------------------------------------- 1 | name: Rosalind 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | env: 10 | TUIST_CONFIG_TOKEN: ${{ secrets.TUIST_CONFIG_CLOUD_TOKEN }} 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | concurrency: 14 | group: rosalind-${{ github.head_ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | name: "Release build on ${{ matrix.os }}" 20 | timeout-minutes: 10 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, macos-latest] 24 | swift: ["6.0"] 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: SwiftyLab/setup-swift@latest 29 | with: 30 | swift-version: ${{ matrix.swift }} 31 | - uses: jdx/mise-action@v2 32 | if: runner.os == 'Linux' || runner.os == 'macOS' 33 | with: 34 | experimental: true 35 | - name: Run 36 | if: runner.os == 'Linux' || runner.os == 'macOS' 37 | run: mise run build 38 | - name: Run 39 | if: runner.os == 'Windows' 40 | run: swift build --product Rosalind 41 | 42 | test: 43 | name: "Test on ${{ matrix.os }}" 44 | timeout-minutes: 10 45 | strategy: 46 | matrix: 47 | os: [macos-latest] 48 | swift: ["6.0"] 49 | runs-on: ${{ matrix.os }} 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Select Xcode 53 | run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app 54 | - uses: SwiftyLab/setup-swift@latest 55 | with: 56 | swift-version: ${{ matrix.swift }} 57 | - uses: jdx/mise-action@v2 58 | if: runner.os == 'Linux' || runner.os == 'macOS' 59 | with: 60 | experimental: true 61 | - name: Run 62 | if: runner.os == 'Linux' || runner.os == 'macOS' 63 | run: mise run test 64 | - name: Run 65 | if: runner.os == 'Windows' 66 | run: swift test 67 | 68 | lint: 69 | name: Lint 70 | timeout-minutes: 10 71 | runs-on: macos-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: jdx/mise-action@v2 75 | with: 76 | experimental: true 77 | - name: Run 78 | run: mise run lint 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,swift 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Swift ### 38 | # Xcode 39 | # 40 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 41 | 42 | ## User settings 43 | xcuserdata/ 44 | 45 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 46 | *.xcscmblueprint 47 | *.xccheckout 48 | 49 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 50 | build/ 51 | DerivedData/ 52 | *.moved-aside 53 | *.pbxuser 54 | !default.pbxuser 55 | *.mode1v3 56 | !default.mode1v3 57 | *.mode2v3 58 | !default.mode2v3 59 | *.perspectivev3 60 | !default.perspectivev3 61 | 62 | ## Obj-C/Swift specific 63 | *.hmap 64 | 65 | ## App packaging 66 | *.dSYM.zip 67 | *.dSYM 68 | 69 | ## Playgrounds 70 | timeline.xctimeline 71 | playground.xcworkspace 72 | 73 | # Swift Package Manager 74 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 75 | # Packages/ 76 | # Package.pins 77 | # Package.resolved 78 | # *.xcodeproj 79 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 80 | # hence it is not needed unless you have added a package configuration file to your project 81 | .swiftpm 82 | 83 | .build/ 84 | 85 | # CocoaPods 86 | # We recommend against adding the Pods directory to your .gitignore. However 87 | # you should judge for yourself, the pros and cons are mentioned at: 88 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 89 | # Pods/ 90 | # Add this line if you want to avoid checking in source code from the Xcode workspace 91 | # *.xcworkspace 92 | 93 | # Carthage 94 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 95 | # Carthage/Checkouts 96 | 97 | Carthage/Build/ 98 | 99 | # Accio dependency management 100 | Dependencies/ 101 | .accio/ 102 | 103 | # fastlane 104 | # It is recommended to not store the screenshots in the git repo. 105 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 106 | # For more information about the recommended setup visit: 107 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 108 | 109 | fastlane/report.xml 110 | fastlane/Preview.html 111 | fastlane/screenshots/**/*.png 112 | fastlane/test_output 113 | 114 | # Code Injection 115 | # After new code Injection tools there's a generated folder /iOSInjectionProject 116 | # https://github.com/johnno1962/injectionforxcode 117 | 118 | iOSInjectionProject/ 119 | 120 | # End of https://www.toptal.com/developers/gitignore/api/macos,swift 121 | 122 | ### Tuist derived files ### 123 | graph.dot 124 | Derived/ 125 | 126 | ### Tuist managed dependencies ### 127 | Tuist/.build 128 | 129 | # Created by https://www.toptal.com/developers/gitignore/api/node 130 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 131 | 132 | ### Node ### 133 | # Logs 134 | logs 135 | *.log 136 | npm-debug.log* 137 | yarn-debug.log* 138 | yarn-error.log* 139 | lerna-debug.log* 140 | .pnpm-debug.log* 141 | 142 | # Diagnostic reports (https://nodejs.org/api/report.html) 143 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 144 | 145 | # Runtime data 146 | pids 147 | *.pid 148 | *.seed 149 | *.pid.lock 150 | 151 | # Directory for instrumented libs generated by jscoverage/JSCover 152 | lib-cov 153 | 154 | # Coverage directory used by tools like istanbul 155 | coverage 156 | *.lcov 157 | 158 | # nyc test coverage 159 | .nyc_output 160 | 161 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 162 | .grunt 163 | 164 | # Bower dependency directory (https://bower.io/) 165 | bower_components 166 | 167 | # node-waf configuration 168 | .lock-wscript 169 | 170 | # Compiled binary addons (https://nodejs.org/api/addons.html) 171 | build/Release 172 | 173 | # Dependency directories 174 | node_modules/ 175 | jspm_packages/ 176 | 177 | # Snowpack dependency directory (https://snowpack.dev/) 178 | web_modules/ 179 | 180 | # TypeScript cache 181 | *.tsbuildinfo 182 | 183 | # Optional npm cache directory 184 | .npm 185 | 186 | # Optional eslint cache 187 | .eslintcache 188 | 189 | # Optional stylelint cache 190 | .stylelintcache 191 | 192 | # Microbundle cache 193 | .rpt2_cache/ 194 | .rts2_cache_cjs/ 195 | .rts2_cache_es/ 196 | .rts2_cache_umd/ 197 | 198 | # Optional REPL history 199 | .node_repl_history 200 | 201 | # Output of 'npm pack' 202 | *.tgz 203 | 204 | # Yarn Integrity file 205 | .yarn-integrity 206 | 207 | # dotenv environment variable files 208 | .env 209 | .env.development.local 210 | .env.test.local 211 | .env.production.local 212 | .env.local 213 | 214 | # parcel-bundler cache (https://parceljs.org/) 215 | .cache 216 | .parcel-cache 217 | 218 | # Next.js build output 219 | .next 220 | out 221 | 222 | # Nuxt.js build / generate output 223 | .nuxt 224 | dist 225 | 226 | # Gatsby files 227 | .cache/ 228 | # Comment in the public line in if your project uses Gatsby and not Next.js 229 | # https://nextjs.org/blog/next-9-1#public-directory-support 230 | # public 231 | 232 | # vuepress build output 233 | .vuepress/dist 234 | 235 | # vuepress v2.x temp and cache directory 236 | .temp 237 | 238 | # Docusaurus cache and generated files 239 | .docusaurus 240 | 241 | # Serverless directories 242 | .serverless/ 243 | 244 | # FuseBox cache 245 | .fusebox/ 246 | 247 | # DynamoDB Local files 248 | .dynamodb/ 249 | 250 | # TernJS port file 251 | .tern-port 252 | 253 | # Stores VSCode versions used for testing VSCode extensions 254 | .vscode-test 255 | 256 | # yarn v2 257 | .yarn/cache 258 | .yarn/unplugged 259 | .yarn/build-state.yml 260 | .yarn/install-state.gz 261 | .pnp.* 262 | 263 | ### Node Patch ### 264 | # Serverless Webpack directories 265 | .webpack/ 266 | 267 | # Optional stylelint cache 268 | 269 | # SvelteKit build / generate output 270 | .svelte-kit 271 | 272 | # End of https://www.toptal.com/developers/gitignore/api/node 273 | 274 | docs/.vitepress/cache 275 | 276 | .swiftpm/**/*.xcworkspace/ 277 | .swiftpm/**/*.xcodeproj/ 278 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --symlinks ignore 4 | --disable hoistAwait 5 | --disable hoistTry 6 | --swiftversion 5.7 7 | 8 | # format options 9 | 10 | --allman false 11 | --binarygrouping 4,8 12 | --closingparen balanced 13 | --commas always 14 | --comments indent 15 | --decimalgrouping 3,6 16 | --elseposition same-line 17 | --empty void 18 | --exponentcase lowercase 19 | --exponentgrouping disabled 20 | --extensionacl on-declarations 21 | --fractiongrouping disabled 22 | --header strip 23 | --hexgrouping 4,8 24 | --hexliteralcase uppercase 25 | --ifdef indent 26 | --indent 4 27 | --indentcase false 28 | --importgrouping testable-bottom 29 | --linebreaks lf 30 | --octalgrouping 4,8 31 | --operatorfunc spaced 32 | --patternlet hoist 33 | --ranges spaced 34 | --self remove 35 | --semicolons inline 36 | --stripunusedargs always 37 | --trimwhitespace always 38 | --maxwidth 130 39 | --wraparguments before-first 40 | --wrapcollections before-first 41 | --wrapconditions after-first 42 | --wrapparameters before-first -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - trailing_comma 4 | - nesting 5 | - cyclomatic_complexity 6 | - file_length 7 | - todo 8 | - function_parameter_count 9 | - opening_brace 10 | - line_length 11 | identifier_name: 12 | min_length: 13 | error: 1 14 | warning: 1 15 | max_length: 16 | warning: 60 17 | error: 80 18 | inclusive_language: 19 | override_allowed_terms: 20 | - masterKey 21 | type_name: 22 | min_length: 23 | error: 1 24 | warning: 1 -------------------------------------------------------------------------------- /.xcode-version: -------------------------------------------------------------------------------- 1 | 15.4 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.5.26] - 2025-06-01 9 | ### Details 10 | #### Chore 11 | - Update dependency p-x9/machokit to from: "0.34.0" by @renovate[bot] in [#140](https://github.com/tuist/Rosalind/pull/140) 12 | 13 | ## [0.5.25] - 2025-05-27 14 | ### Details 15 | #### Chore 16 | - Update dependency esbuild to v0.25.5 by @renovate[bot] in [#139](https://github.com/tuist/Rosalind/pull/139) 17 | 18 | ## [0.5.24] - 2025-05-26 19 | ### Details 20 | #### Chore 21 | - Update dependency tuist/filesystem to from: "0.10.0" by @renovate[bot] in [#138](https://github.com/tuist/Rosalind/pull/138) 22 | 23 | ## [0.5.23] - 2025-05-26 24 | ### Details 25 | #### Chore 26 | - Lock file maintenance by @renovate[bot] in [#137](https://github.com/tuist/Rosalind/pull/137) 27 | 28 | ## [0.5.22] - 2025-05-23 29 | ### Details 30 | #### Chore 31 | - Update dependency pointfreeco/swift-snapshot-testing to from: "1.18.4" by @renovate[bot] in [#136](https://github.com/tuist/Rosalind/pull/136) 32 | 33 | ## [0.5.21] - 2025-05-22 34 | ### Details 35 | #### Chore 36 | - Update node.js to v22.16.0 by @renovate[bot] in [#135](https://github.com/tuist/Rosalind/pull/135) 37 | 38 | ## [0.5.20] - 2025-05-21 39 | ### Details 40 | #### Chore 41 | - Update dependency p-x9/machokit to from: "0.33.0" by @renovate[bot] in [#134](https://github.com/tuist/Rosalind/pull/134) 42 | 43 | ## [0.5.19] - 2025-05-20 44 | ### Details 45 | #### Chore 46 | - Update dependency tuist/filesystem to from: "0.9.2" by @renovate[bot] in [#133](https://github.com/tuist/Rosalind/pull/133) 47 | 48 | ## [0.5.18] - 2025-05-19 49 | ### Details 50 | #### Chore 51 | - Lock file maintenance by @renovate[bot] in [#132](https://github.com/tuist/Rosalind/pull/132) 52 | 53 | ## [0.5.17] - 2025-05-18 54 | ### Details 55 | #### Chore 56 | - Update dependency tuist/filesystem to from: "0.9.1" by @renovate[bot] in [#131](https://github.com/tuist/Rosalind/pull/131) 57 | 58 | ## [0.5.16] - 2025-05-15 59 | ### Details 60 | #### Chore 61 | - Update dependency tuist/filesystem to from: "0.9.0" by @renovate[bot] in [#130](https://github.com/tuist/Rosalind/pull/130) 62 | 63 | ## [0.5.15] - 2025-05-15 64 | ### Details 65 | #### Chore 66 | - Update dependency wrangler to v4 by @renovate[bot] 67 | 68 | ## [0.5.14] - 2025-05-15 69 | ### Details 70 | #### Chore 71 | - Update node.js to v22.15.1 by @renovate[bot] in [#129](https://github.com/tuist/Rosalind/pull/129) 72 | 73 | ## [0.5.13] - 2025-05-14 74 | ### Details 75 | #### Fix 76 | - Allow older versions of FileSystem for consumers by @fortmarek in [#127](https://github.com/tuist/Rosalind/pull/127) 77 | 78 | ## [0.5.12] - 2025-05-14 79 | ### Details 80 | #### Chore 81 | - Update dependency tuist/filesystem to from: "0.8.0" by @renovate[bot] in [#126](https://github.com/tuist/Rosalind/pull/126) 82 | 83 | ## [0.5.11] - 2025-05-13 84 | ### Details 85 | #### Chore 86 | - Update dependency pnpm to v10.11.0 by @renovate[bot] in [#125](https://github.com/tuist/Rosalind/pull/125) 87 | 88 | ## [0.5.10] - 2025-05-12 89 | ### Details 90 | #### Chore 91 | - Update dependency tuist/filesystem to from: "0.7.18" by @renovate[bot] in [#124](https://github.com/tuist/Rosalind/pull/124) 92 | 93 | ## [0.5.9] - 2025-05-12 94 | ### Details 95 | #### Chore 96 | - Lock file maintenance by @renovate[bot] in [#123](https://github.com/tuist/Rosalind/pull/123) 97 | 98 | ## [0.5.8] - 2025-05-07 99 | ### Details 100 | #### Chore 101 | - Update dependency kolos65/mockable to from: "0.3.2" by @renovate[bot] in [#122](https://github.com/tuist/Rosalind/pull/122) 102 | 103 | ## [0.5.7] - 2025-05-06 104 | ### Details 105 | #### Chore 106 | - Update dependency p-x9/machokit to from: "0.32.0" by @renovate[bot] in [#121](https://github.com/tuist/Rosalind/pull/121) 107 | 108 | ## [0.5.6] - 2025-05-06 109 | ### Details 110 | #### Chore 111 | - Update dependency esbuild to v0.25.4 by @renovate[bot] in [#120](https://github.com/tuist/Rosalind/pull/120) 112 | 113 | ## [0.5.5] - 2025-05-05 114 | ### Details 115 | #### Chore 116 | - Lock file maintenance by @renovate[bot] in [#119](https://github.com/tuist/Rosalind/pull/119) 117 | 118 | ## [0.5.4] - 2025-04-28 119 | ### Details 120 | #### Chore 121 | - Update dependency pnpm to v10.10.0 by @renovate[bot] in [#118](https://github.com/tuist/Rosalind/pull/118) 122 | 123 | ## [0.5.3] - 2025-04-26 124 | ### Details 125 | #### Chore 126 | - Update dependency tuist/filesystem to from: "0.7.16" by @renovate[bot] in [#117](https://github.com/tuist/Rosalind/pull/117) 127 | 128 | ## [0.5.2] - 2025-04-25 129 | ### Details 130 | #### Chore 131 | - Update dependency p-x9/machokit to from: "0.31.0" by @renovate[bot] in [#116](https://github.com/tuist/Rosalind/pull/116) 132 | 133 | ## [0.5.1] - 2025-04-25 134 | ### Details 135 | #### Fix 136 | - Wrong asset path by @fortmarek in [#115](https://github.com/tuist/Rosalind/pull/115) 137 | 138 | ## [0.5.0] - 2025-04-25 139 | ### Details 140 | #### Feat 141 | - Support for recognizing car as asset and parsing its contents by @fortmarek in [#114](https://github.com/tuist/Rosalind/pull/114) 142 | 143 | ## [0.4.0] - 2025-04-24 144 | ### Details 145 | #### Feat 146 | - Add distinction between download and install size by @fortmarek in [#113](https://github.com/tuist/Rosalind/pull/113) 147 | 148 | ## [0.3.2] - 2025-04-24 149 | ### Details 150 | #### Chore 151 | - Update node.js to v22.15.0 by @renovate[bot] in [#112](https://github.com/tuist/Rosalind/pull/112) 152 | 153 | ## [0.3.1] - 2025-04-23 154 | ### Details 155 | #### Chore 156 | - Update dependency esbuild to v0.25.3 by @renovate[bot] in [#111](https://github.com/tuist/Rosalind/pull/111) 157 | 158 | ## [0.3.0] - 2025-04-23 159 | ### Details 160 | #### Feat 161 | - Add app bundle analysis by @fortmarek in [#106](https://github.com/tuist/Rosalind/pull/106) 162 | 163 | ## [0.2.42] - 2025-04-23 164 | ### Details 165 | #### Chore 166 | - Update dependency tuist/filesystem to from: "0.7.14" by @renovate[bot] in [#110](https://github.com/tuist/Rosalind/pull/110) 167 | 168 | ## [0.2.41] - 2025-04-21 169 | ### Details 170 | #### Chore 171 | - Update dependency pnpm to v10.9.0 by @renovate[bot] in [#109](https://github.com/tuist/Rosalind/pull/109) 172 | 173 | ## [0.2.40] - 2025-04-21 174 | ### Details 175 | #### Chore 176 | - Lock file maintenance by @renovate[bot] in [#108](https://github.com/tuist/Rosalind/pull/108) 177 | 178 | ## [0.2.39] - 2025-04-17 179 | ### Details 180 | #### Chore 181 | - Update dependency tuist/filesystem to from: "0.7.13" by @renovate[bot] in [#107](https://github.com/tuist/Rosalind/pull/107) 182 | 183 | ## [0.2.38] - 2025-04-15 184 | ### Details 185 | #### Chore 186 | - Update dependency tuist/filesystem to from: "0.7.12" by @renovate[bot] in [#105](https://github.com/tuist/Rosalind/pull/105) 187 | 188 | ## [0.2.37] - 2025-04-14 189 | ### Details 190 | #### Chore 191 | - Update dependency pnpm to v10.8.1 by @renovate[bot] in [#104](https://github.com/tuist/Rosalind/pull/104) 192 | 193 | ## [0.2.36] - 2025-04-14 194 | ### Details 195 | #### Chore 196 | - Lock file maintenance by @renovate[bot] in [#103](https://github.com/tuist/Rosalind/pull/103) 197 | 198 | ## [0.2.35] - 2025-04-08 199 | ### Details 200 | #### Chore 201 | - Update dependency pnpm to v10.8.0 by @renovate[bot] in [#102](https://github.com/tuist/Rosalind/pull/102) 202 | 203 | ## [0.2.34] - 2025-04-07 204 | ### Details 205 | #### Chore 206 | - Update dependency apple/swift-crypto to from: "3.12.3" by @renovate[bot] in [#101](https://github.com/tuist/Rosalind/pull/101) 207 | 208 | ## [0.2.33] - 2025-04-07 209 | ### Details 210 | #### Chore 211 | - Lock file maintenance by @renovate[bot] in [#100](https://github.com/tuist/Rosalind/pull/100) 212 | 213 | ## [0.2.32] - 2025-04-05 214 | ### Details 215 | #### Chore 216 | - Update dependency tuist/filesystem to from: "0.7.10" by @renovate[bot] in [#99](https://github.com/tuist/Rosalind/pull/99) 217 | 218 | ## [0.2.31] - 2025-04-02 219 | ### Details 220 | #### Chore 221 | - Update dependency pointfreeco/swift-snapshot-testing to from: "1.18.3" by @renovate[bot] in [#98](https://github.com/tuist/Rosalind/pull/98) 222 | 223 | ## [0.2.30] - 2025-04-01 224 | ### Details 225 | #### Chore 226 | - Update dependency pnpm to v10.7.1 by @renovate[bot] in [#97](https://github.com/tuist/Rosalind/pull/97) 227 | 228 | ## [0.2.29] - 2025-04-01 229 | ### Details 230 | #### Chore 231 | - Update dependency pointfreeco/swift-snapshot-testing to from: "1.18.2" by @renovate[bot] in [#96](https://github.com/tuist/Rosalind/pull/96) 232 | 233 | ## [0.2.28] - 2025-03-31 234 | ### Details 235 | #### Chore 236 | - Lock file maintenance by @renovate[bot] in [#95](https://github.com/tuist/Rosalind/pull/95) 237 | 238 | ## [0.2.27] - 2025-03-31 239 | ### Details 240 | #### Chore 241 | - Update dependency wrangler to v3.114.3 by @renovate[bot] in [#93](https://github.com/tuist/Rosalind/pull/93) 242 | 243 | ## [0.2.26] - 2025-03-30 244 | ### Details 245 | #### Chore 246 | - Update dependency esbuild to v0.25.2 by @renovate[bot] in [#94](https://github.com/tuist/Rosalind/pull/94) 247 | - Update dependency pnpm to v10.7.0 by @renovate[bot] in [#92](https://github.com/tuist/Rosalind/pull/92) 248 | 249 | ## [0.2.25] - 2025-03-26 250 | ### Details 251 | #### Chore 252 | - Add release pipeline by @fortmarek 253 | - Update dependency tuist/filesystem to from: "0.7.9" by @renovate[bot] in [#90](https://github.com/tuist/Rosalind/pull/90) 254 | - Lock file maintenance by @renovate[bot] in [#89](https://github.com/tuist/Rosalind/pull/89) 255 | - Update dependency tuist/filesystem to from: "0.7.8" by @renovate[bot] in [#88](https://github.com/tuist/Rosalind/pull/88) 256 | - Update dependency pnpm to v10.6.5 by @renovate[bot] in [#87](https://github.com/tuist/Rosalind/pull/87) 257 | - Update dependency wrangler to v3.114.2 by @renovate[bot] in [#86](https://github.com/tuist/Rosalind/pull/86) 258 | - Update dependency pnpm to v10.6.4 by @renovate[bot] in [#85](https://github.com/tuist/Rosalind/pull/85) 259 | - Lock file maintenance by @renovate[bot] in [#84](https://github.com/tuist/Rosalind/pull/84) 260 | - Update dependency pnpm to v10.6.3 by @renovate[bot] in [#82](https://github.com/tuist/Rosalind/pull/82) 261 | - Update dependency apple/swift-crypto to from: "3.12.2" by @renovate[bot] in [#81](https://github.com/tuist/Rosalind/pull/81) 262 | - Update dependency wrangler to v3.114.1 by @renovate[bot] in [#80](https://github.com/tuist/Rosalind/pull/80) 263 | - Update dependency apple/swift-crypto to from: "3.11.2" by @renovate[bot] in [#79](https://github.com/tuist/Rosalind/pull/79) 264 | - Update dependency pnpm to v10.6.2 by @renovate[bot] in [#78](https://github.com/tuist/Rosalind/pull/78) 265 | - Update dependency esbuild to v0.25.1 by @renovate[bot] in [#77](https://github.com/tuist/Rosalind/pull/77) 266 | - Lock file maintenance by @renovate[bot] in [#76](https://github.com/tuist/Rosalind/pull/76) 267 | - Update dependency pnpm to v10.6.1 by @renovate[bot] in [#75](https://github.com/tuist/Rosalind/pull/75) 268 | - Update dependency wrangler to v3.114.0 by @renovate[bot] in [#74](https://github.com/tuist/Rosalind/pull/74) 269 | - Update dependency pnpm to v10.6.0 by @renovate[bot] in [#73](https://github.com/tuist/Rosalind/pull/73) 270 | - Update dependency wrangler to v3.113.0 by @renovate[bot] 271 | - Update dependency wrangler to v3.112.0 by @renovate[bot] 272 | - Update node.js to v22.14.0 by @renovate[bot] 273 | - Update dependency tuist/filesystem to from: "0.7.7" by @renovate[bot] 274 | - Update dependency tuist/command to from: "0.13.0" by @renovate[bot] 275 | - Lock file maintenance by @renovate[bot] 276 | - Update dependency pnpm to v10 by @renovate[bot] 277 | - Update dependency wrangler to v3.111.0 by @renovate[bot] 278 | - Update dependency wrangler to v3.105.1 by @renovate[bot] 279 | - Update dependency tuist/command to from: "0.12.0" by @renovate[bot] 280 | - Update dependency tuist/filesystem to from: "0.7.2" by @renovate[bot] 281 | - Update dependency vitepress to v1.6.3 by @renovate[bot] 282 | - Update dependency pnpm to v9.15.4 by @renovate[bot] 283 | - Update node.js to v22.13.1 by @renovate[bot] 284 | - Update dependency tuist to v4.40.0 by @renovate[bot] 285 | - Lock file maintenance by @renovate[bot] 286 | - Update dependency wrangler to v3.83.0 by @renovate[bot] in [#49](https://github.com/tuist/Rosalind/pull/49) 287 | - Update dependency tuist/filesystem to from: "0.4.8" by @renovate[bot] in [#48](https://github.com/tuist/Rosalind/pull/48) 288 | - Update dependency tuist/filesystem to from: "0.4.7" by @renovate[bot] in [#47](https://github.com/tuist/Rosalind/pull/47) 289 | - Update actions/checkout digest to 11bd719 by @renovate[bot] in [#46](https://github.com/tuist/Rosalind/pull/46) 290 | - Update dependency tuist/filesystem to from: "0.4.5" by @renovate[bot] in [#45](https://github.com/tuist/Rosalind/pull/45) 291 | - Update dependency tuist to v4.31.0 by @renovate[bot] in [#44](https://github.com/tuist/Rosalind/pull/44) 292 | - Update dependency wrangler to v3.82.0 by @renovate[bot] in [#43](https://github.com/tuist/Rosalind/pull/43) 293 | - Lock file maintenance by @renovate[bot] in [#42](https://github.com/tuist/Rosalind/pull/42) 294 | - Update dependency wrangler to v3.81.0 by @renovate[bot] in [#41](https://github.com/tuist/Rosalind/pull/41) 295 | - Update dependency tuist/path to from: "0.3.8" by @renovate[bot] 296 | - Update dependency node to v22.10.0 by @renovate[bot] 297 | - Lock file maintenance by @renovate[bot] 298 | - Update dependency tuist/filesystem to from: "0.4.4" by @renovate[bot] 299 | - Update dependency tuist to v4.30.0 by @renovate[bot] 300 | - Update actions/checkout digest to eef6144 by @renovate[bot] 301 | - Update dependency tuist/command to from: "0.9.32" by @renovate[bot] 302 | - Update dependency wrangler to v3.80.3 by @renovate[bot] 303 | 304 | ## [0.2.24] - 2024-08-28 305 | ### Details 306 | #### Chore 307 | - Update dependency tuist/path to from: "0.3.4" by @renovate[bot] in [#31](https://github.com/tuist/Rosalind/pull/31) 308 | 309 | ## [0.2.23] - 2024-08-27 310 | ### Details 311 | #### Chore 312 | - Update dependency wrangler to v3.72.3 by @renovate[bot] in [#30](https://github.com/tuist/Rosalind/pull/30) 313 | 314 | ## [0.2.22] - 2024-08-27 315 | ### Details 316 | #### Chore 317 | - Update dependency tuist to v4.25.0 by @renovate[bot] in [#29](https://github.com/tuist/Rosalind/pull/29) 318 | 319 | ## [0.2.21] - 2024-08-26 320 | ### Details 321 | #### Chore 322 | - Lock file maintenance by @renovate[bot] in [#28](https://github.com/tuist/Rosalind/pull/28) 323 | 324 | ## [0.2.20] - 2024-08-24 325 | ### Details 326 | #### Chore 327 | - Update dependency vitepress to v1.3.4 by @renovate[bot] in [#27](https://github.com/tuist/Rosalind/pull/27) 328 | 329 | ## [0.2.19] - 2024-08-22 330 | ### Details 331 | #### Chore 332 | - Update dependency wrangler to v3.72.2 by @renovate[bot] in [#26](https://github.com/tuist/Rosalind/pull/26) 333 | 334 | ## [0.2.18] - 2024-08-22 335 | ### Details 336 | #### Chore 337 | - Update dependency node to v22.7.0 by @renovate[bot] in [#25](https://github.com/tuist/Rosalind/pull/25) 338 | 339 | ## [0.2.17] - 2024-08-21 340 | ### Details 341 | #### Chore 342 | - Update dependency tuist/command to from: "0.8.0" by @renovate[bot] in [#24](https://github.com/tuist/Rosalind/pull/24) 343 | 344 | ## [0.2.16] - 2024-08-20 345 | ### Details 346 | #### Chore 347 | - Update dependency wrangler to v3.72.1 by @renovate[bot] in [#23](https://github.com/tuist/Rosalind/pull/23) 348 | 349 | ## [0.2.15] - 2024-08-20 350 | ### Details 351 | #### Chore 352 | - Update dependency tuist/path to from: "0.3.3" by @renovate[bot] in [#21](https://github.com/tuist/Rosalind/pull/21) 353 | 354 | ## [0.2.14] - 2024-08-20 355 | ### Details 356 | #### Chore 357 | - Update dependency tuist/command to from: "0.7.8" by @renovate[bot] in [#22](https://github.com/tuist/Rosalind/pull/22) 358 | 359 | ## [0.2.13] - 2024-08-20 360 | ### Details 361 | #### Chore 362 | - Update dependency tuist/command to from: "0.7.7" by @renovate[bot] in [#20](https://github.com/tuist/Rosalind/pull/20) 363 | 364 | ## [0.2.12] - 2024-08-19 365 | ### Details 366 | #### Chore 367 | - Update dependency tuist to v4.24.0 by @renovate[bot] in [#19](https://github.com/tuist/Rosalind/pull/19) 368 | 369 | ## [0.2.11] - 2024-08-19 370 | ### Details 371 | #### Chore 372 | - Update dependency tuist/command to from: "0.7.6" by @renovate[bot] in [#18](https://github.com/tuist/Rosalind/pull/18) 373 | 374 | ## [0.2.10] - 2024-08-19 375 | ### Details 376 | #### Chore 377 | - Lock file maintenance by @renovate[bot] in [#17](https://github.com/tuist/Rosalind/pull/17) 378 | 379 | ## [0.2.9] - 2024-08-17 380 | ### Details 381 | #### Chore 382 | - Update dependency tuist/command to from: "0.7.5" by @renovate[bot] in [#16](https://github.com/tuist/Rosalind/pull/16) 383 | 384 | ## [0.2.8] - 2024-08-17 385 | ### Details 386 | #### Chore 387 | - Update dependency vitepress to v1.3.3 by @renovate[bot] in [#15](https://github.com/tuist/Rosalind/pull/15) 388 | 389 | ## [0.2.7] - 2024-08-16 390 | ### Details 391 | #### Chore 392 | - Update dependency wrangler to v3.72.0 by @renovate[bot] in [#14](https://github.com/tuist/Rosalind/pull/14) 393 | 394 | ## [0.2.6] - 2024-08-16 395 | ### Details 396 | #### Chore 397 | - Update dependency tuist/command to from: "0.7.4" by @renovate[bot] in [#13](https://github.com/tuist/Rosalind/pull/13) 398 | 399 | ## [0.2.5] - 2024-08-15 400 | ### Details 401 | #### Chore 402 | - Update dependency tuist/command to from: "0.7.3" by @renovate[bot] in [#12](https://github.com/tuist/Rosalind/pull/12) 403 | 404 | ## [0.2.4] - 2024-08-15 405 | ### Details 406 | #### Chore 407 | - Update dependency tuist/command to from: "0.7.2" by @renovate[bot] in [#11](https://github.com/tuist/Rosalind/pull/11) 408 | 409 | ## [0.2.3] - 2024-08-14 410 | ### Details 411 | #### Chore 412 | - Update dependency tuist/command to from: "0.7.1" by @renovate[bot] in [#10](https://github.com/tuist/Rosalind/pull/10) 413 | 414 | ## [0.2.2] - 2024-08-13 415 | ### Details 416 | #### Chore 417 | - Update dependency wrangler to v3.71.0 by @renovate[bot] in [#9](https://github.com/tuist/Rosalind/pull/9) 418 | 419 | ## [0.2.1] - 2024-08-13 420 | ### Details 421 | #### Chore 422 | - Update dependency tuist/command to from: "0.7.0" by @renovate[bot] 423 | 424 | ## [0.2.0] - 2024-08-13 425 | ### Details 426 | #### Feat 427 | - Add documentation site by @pepicrft 428 | 429 | ## [0.1.0] - 2024-08-12 430 | ### Details 431 | #### Chore 432 | - Update dependency tuist/command to from: "0.6.3" by @renovate[bot] in [#5](https://github.com/tuist/Rosalind/pull/5) 433 | - Update dependency tuist to v4.23.0 by @renovate[bot] 434 | - Update dependency tuist/path to from: "0.3.2" by @renovate[bot] 435 | 436 | [0.5.26]: https://github.com/tuist/Rosalind/compare/0.5.25..0.5.26 437 | [0.5.25]: https://github.com/tuist/Rosalind/compare/0.5.24..0.5.25 438 | [0.5.24]: https://github.com/tuist/Rosalind/compare/0.5.23..0.5.24 439 | [0.5.23]: https://github.com/tuist/Rosalind/compare/0.5.22..0.5.23 440 | [0.5.22]: https://github.com/tuist/Rosalind/compare/0.5.21..0.5.22 441 | [0.5.21]: https://github.com/tuist/Rosalind/compare/0.5.20..0.5.21 442 | [0.5.20]: https://github.com/tuist/Rosalind/compare/0.5.19..0.5.20 443 | [0.5.19]: https://github.com/tuist/Rosalind/compare/0.5.18..0.5.19 444 | [0.5.18]: https://github.com/tuist/Rosalind/compare/0.5.17..0.5.18 445 | [0.5.17]: https://github.com/tuist/Rosalind/compare/0.5.16..0.5.17 446 | [0.5.16]: https://github.com/tuist/Rosalind/compare/0.5.15..0.5.16 447 | [0.5.15]: https://github.com/tuist/Rosalind/compare/0.5.14..0.5.15 448 | [0.5.14]: https://github.com/tuist/Rosalind/compare/0.5.13..0.5.14 449 | [0.5.13]: https://github.com/tuist/Rosalind/compare/0.5.12..0.5.13 450 | [0.5.12]: https://github.com/tuist/Rosalind/compare/0.5.11..0.5.12 451 | [0.5.11]: https://github.com/tuist/Rosalind/compare/0.5.10..0.5.11 452 | [0.5.10]: https://github.com/tuist/Rosalind/compare/0.5.9..0.5.10 453 | [0.5.9]: https://github.com/tuist/Rosalind/compare/0.5.8..0.5.9 454 | [0.5.8]: https://github.com/tuist/Rosalind/compare/0.5.7..0.5.8 455 | [0.5.7]: https://github.com/tuist/Rosalind/compare/0.5.6..0.5.7 456 | [0.5.6]: https://github.com/tuist/Rosalind/compare/0.5.5..0.5.6 457 | [0.5.5]: https://github.com/tuist/Rosalind/compare/0.5.4..0.5.5 458 | [0.5.4]: https://github.com/tuist/Rosalind/compare/0.5.3..0.5.4 459 | [0.5.3]: https://github.com/tuist/Rosalind/compare/0.5.2..0.5.3 460 | [0.5.2]: https://github.com/tuist/Rosalind/compare/0.5.1..0.5.2 461 | [0.5.1]: https://github.com/tuist/Rosalind/compare/0.5.0..0.5.1 462 | [0.5.0]: https://github.com/tuist/Rosalind/compare/0.4.0..0.5.0 463 | [0.4.0]: https://github.com/tuist/Rosalind/compare/0.3.2..0.4.0 464 | [0.3.2]: https://github.com/tuist/Rosalind/compare/0.3.1..0.3.2 465 | [0.3.1]: https://github.com/tuist/Rosalind/compare/0.3.0..0.3.1 466 | [0.3.0]: https://github.com/tuist/Rosalind/compare/0.2.42..0.3.0 467 | [0.2.42]: https://github.com/tuist/Rosalind/compare/0.2.41..0.2.42 468 | [0.2.41]: https://github.com/tuist/Rosalind/compare/0.2.40..0.2.41 469 | [0.2.40]: https://github.com/tuist/Rosalind/compare/0.2.39..0.2.40 470 | [0.2.39]: https://github.com/tuist/Rosalind/compare/0.2.38..0.2.39 471 | [0.2.38]: https://github.com/tuist/Rosalind/compare/0.2.37..0.2.38 472 | [0.2.37]: https://github.com/tuist/Rosalind/compare/0.2.36..0.2.37 473 | [0.2.36]: https://github.com/tuist/Rosalind/compare/0.2.35..0.2.36 474 | [0.2.35]: https://github.com/tuist/Rosalind/compare/0.2.34..0.2.35 475 | [0.2.34]: https://github.com/tuist/Rosalind/compare/0.2.33..0.2.34 476 | [0.2.33]: https://github.com/tuist/Rosalind/compare/0.2.32..0.2.33 477 | [0.2.32]: https://github.com/tuist/Rosalind/compare/0.2.31..0.2.32 478 | [0.2.31]: https://github.com/tuist/Rosalind/compare/0.2.30..0.2.31 479 | [0.2.30]: https://github.com/tuist/Rosalind/compare/0.2.29..0.2.30 480 | [0.2.29]: https://github.com/tuist/Rosalind/compare/0.2.28..0.2.29 481 | [0.2.28]: https://github.com/tuist/Rosalind/compare/0.2.27..0.2.28 482 | [0.2.27]: https://github.com/tuist/Rosalind/compare/0.2.26..0.2.27 483 | [0.2.26]: https://github.com/tuist/Rosalind/compare/0.2.25..0.2.26 484 | [0.2.25]: https://github.com/tuist/Rosalind/compare/0.2.24..0.2.25 485 | [0.2.24]: https://github.com/tuist/Rosalind/compare/0.2.23..0.2.24 486 | [0.2.23]: https://github.com/tuist/Rosalind/compare/0.2.22..0.2.23 487 | [0.2.22]: https://github.com/tuist/Rosalind/compare/0.2.21..0.2.22 488 | [0.2.21]: https://github.com/tuist/Rosalind/compare/0.2.20..0.2.21 489 | [0.2.20]: https://github.com/tuist/Rosalind/compare/0.2.19..0.2.20 490 | [0.2.19]: https://github.com/tuist/Rosalind/compare/0.2.18..0.2.19 491 | [0.2.18]: https://github.com/tuist/Rosalind/compare/0.2.17..0.2.18 492 | [0.2.17]: https://github.com/tuist/Rosalind/compare/0.2.16..0.2.17 493 | [0.2.16]: https://github.com/tuist/Rosalind/compare/0.2.15..0.2.16 494 | [0.2.15]: https://github.com/tuist/Rosalind/compare/0.2.14..0.2.15 495 | [0.2.14]: https://github.com/tuist/Rosalind/compare/0.2.13..0.2.14 496 | [0.2.13]: https://github.com/tuist/Rosalind/compare/0.2.12..0.2.13 497 | [0.2.12]: https://github.com/tuist/Rosalind/compare/0.2.11..0.2.12 498 | [0.2.11]: https://github.com/tuist/Rosalind/compare/0.2.10..0.2.11 499 | [0.2.10]: https://github.com/tuist/Rosalind/compare/0.2.9..0.2.10 500 | [0.2.9]: https://github.com/tuist/Rosalind/compare/0.2.8..0.2.9 501 | [0.2.8]: https://github.com/tuist/Rosalind/compare/0.2.7..0.2.8 502 | [0.2.7]: https://github.com/tuist/Rosalind/compare/0.2.6..0.2.7 503 | [0.2.6]: https://github.com/tuist/Rosalind/compare/0.2.5..0.2.6 504 | [0.2.5]: https://github.com/tuist/Rosalind/compare/0.2.4..0.2.5 505 | [0.2.4]: https://github.com/tuist/Rosalind/compare/0.2.3..0.2.4 506 | [0.2.3]: https://github.com/tuist/Rosalind/compare/0.2.2..0.2.3 507 | [0.2.2]: https://github.com/tuist/Rosalind/compare/0.2.1..0.2.2 508 | [0.2.1]: https://github.com/tuist/Rosalind/compare/0.2.0..0.2.1 509 | [0.2.0]: https://github.com/tuist/Rosalind/compare/0.1.0..0.2.0 510 | 511 | 512 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tuist GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "da51f0ba7aaad5af40c6980939ca98e9ea88b7c00c86f39b0c15ec7a0c90df3e", 3 | "pins" : [ 4 | { 5 | "identity" : "command", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/tuist/Command.git", 8 | "state" : { 9 | "revision" : "079a7803b581d3022469b3a331bccd51d48d2fc0", 10 | "version" : "0.13.0" 11 | } 12 | }, 13 | { 14 | "identity" : "filesystem", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/tuist/FileSystem.git", 17 | "state" : { 18 | "revision" : "0bb391ac255489bfc31f36f6d9f6b130db4e242e", 19 | "version" : "0.8.0" 20 | } 21 | }, 22 | { 23 | "identity" : "machokit", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/p-x9/MachOKit", 26 | "state" : { 27 | "revision" : "b4b752d9d2cf3975ee05620c66284a3414fcf56e", 28 | "version" : "0.32.0" 29 | } 30 | }, 31 | { 32 | "identity" : "mockable", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/Kolos65/Mockable", 35 | "state" : { 36 | "revision" : "118a0b8934e585b80952586db30bcb72aef45a74", 37 | "version" : "0.3.2" 38 | } 39 | }, 40 | { 41 | "identity" : "path", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/tuist/Path.git", 44 | "state" : { 45 | "revision" : "7c74ac435e03a927c3a73134c48b61e60221abcb", 46 | "version" : "0.3.8" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-asn1", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-asn1.git", 53 | "state" : { 54 | "revision" : "ae33e5941bb88d88538d0a6b19ca0b01e6c76dcf", 55 | "version" : "1.3.1" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-atomics", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-atomics.git", 62 | "state" : { 63 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 64 | "version" : "1.2.0" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-collections", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/apple/swift-collections.git", 71 | "state" : { 72 | "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", 73 | "version" : "1.1.2" 74 | } 75 | }, 76 | { 77 | "identity" : "swift-crypto", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/apple/swift-crypto.git", 80 | "state" : { 81 | "revision" : "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed", 82 | "version" : "3.12.3" 83 | } 84 | }, 85 | { 86 | "identity" : "swift-custom-dump", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 89 | "state" : { 90 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 91 | "version" : "1.3.3" 92 | } 93 | }, 94 | { 95 | "identity" : "swift-fileio", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/p-x9/swift-fileio.git", 98 | "state" : { 99 | "revision" : "23349fe1eb23c6ca2876d461a46ff60c0fa92f9c", 100 | "version" : "0.9.0" 101 | } 102 | }, 103 | { 104 | "identity" : "swift-log", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/apple/swift-log", 107 | "state" : { 108 | "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", 109 | "version" : "1.6.3" 110 | } 111 | }, 112 | { 113 | "identity" : "swift-nio", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/apple/swift-nio", 116 | "state" : { 117 | "revision" : "34d486b01cd891297ac615e40d5999536a1e138d", 118 | "version" : "2.83.0" 119 | } 120 | }, 121 | { 122 | "identity" : "swift-snapshot-testing", 123 | "kind" : "remoteSourceControl", 124 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 125 | "state" : { 126 | "revision" : "1be8144023c367c5de701a6313ed29a3a10bf59b", 127 | "version" : "1.18.3" 128 | } 129 | }, 130 | { 131 | "identity" : "swift-syntax", 132 | "kind" : "remoteSourceControl", 133 | "location" : "https://github.com/swiftlang/swift-syntax.git", 134 | "state" : { 135 | "revision" : "0687f71944021d616d34d922343dcef086855920", 136 | "version" : "600.0.1" 137 | } 138 | }, 139 | { 140 | "identity" : "swift-system", 141 | "kind" : "remoteSourceControl", 142 | "location" : "https://github.com/apple/swift-system.git", 143 | "state" : { 144 | "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", 145 | "version" : "1.4.2" 146 | } 147 | }, 148 | { 149 | "identity" : "xctest-dynamic-overlay", 150 | "kind" : "remoteSourceControl", 151 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 152 | "state" : { 153 | "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", 154 | "version" : "1.5.2" 155 | } 156 | }, 157 | { 158 | "identity" : "zipfoundation", 159 | "kind" : "remoteSourceControl", 160 | "location" : "https://github.com/tuist/ZIPFoundation", 161 | "state" : { 162 | "revision" : "e9b1917bd4d7d050e0ff4ec157b5d6e253c84385", 163 | "version" : "0.9.20" 164 | } 165 | } 166 | ], 167 | "version" : 3 168 | } 169 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Rosalind", 8 | platforms: [.macOS("14.0")], 9 | products: [ 10 | .library( 11 | name: "Rosalind", 12 | type: .static, 13 | targets: ["Rosalind"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/tuist/Path.git", .upToNextMajor(from: "0.3.8")), 18 | .package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.10.0")), 19 | .package(url: "https://github.com/tuist/Command.git", .upToNextMajor(from: "0.13.0")), 20 | .package( 21 | url: "https://github.com/pointfreeco/swift-snapshot-testing", 22 | .upToNextMajor(from: "1.18.4") 23 | ), 24 | // To our surprise (note the irony), CryptoSwift is an AppleOS-only framework, therefore 25 | // crypto capabilities need to be imported using a package. 26 | .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMajor(from: "3.12.3")), 27 | .package(url: "https://github.com/p-x9/MachOKit", .upToNextMajor(from: "0.34.0")), 28 | .package(url: "https://github.com/Kolos65/Mockable", .upToNextMajor(from: "0.3.2")), 29 | ], 30 | targets: [ 31 | .target( 32 | name: "Rosalind", 33 | dependencies: [ 34 | .product(name: "Crypto", package: "swift-crypto"), 35 | .product(name: "Path", package: "Path"), 36 | .product(name: "FileSystem", package: "FileSystem"), 37 | .product(name: "Command", package: "Command"), 38 | .product(name: "MachOKit", package: "MachOKit"), 39 | ], 40 | swiftSettings: [ 41 | .enableExperimentalFeature("StrictConcurrency"), 42 | .define("MOCKING", .when(configuration: .debug)), 43 | ] 44 | ), 45 | .testTarget( 46 | name: "RosalindTests", 47 | dependencies: [ 48 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), 49 | .product(name: "Mockable", package: "Mockable"), 50 | "Rosalind", 51 | ] 52 | ), 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rosalind 2 | 3 | [Rosalind Franklin](https://en.wikipedia.org/wiki/Rosalind_Franklin) was a pioneering scientist whose X-ray crystallography work revealed the fundamental structure of DNA, transforming our understanding of life itself. In a similar way, modern applications across platforms—iOS, Android, and React Native—have complex internal architectures that, when properly understood, can be optimized for performance, efficiency, and user experience. 4 | 5 | Our tool, Rosalind, analyzes application bundles to uncover these hidden structures, providing developers with clear, actionable insights about their code, dependencies, resources, and overall composition. By making the invisible visible, Rosalind empowers development teams to make informed decisions when refining their applications. 6 | 7 | > [!NOTE] 8 | > Inspired by Franklin's commitment to scientific discovery, we've made Rosalind open-source under the MIT license. We believe that understanding application architecture should be accessible to all developers, regardless of platform or team size, fostering a more collaborative and innovative development community. 9 | 10 | > [!WARNING] 11 | > While Rosalind can be built on Linux systems, it currently can only be run on macOS due to a runtime dependency on the macOS-only `assetutil` CLI tool. This is something we'd like to address in the future. 12 | 13 | Rosalind currently provides comprehensive analysis of Xcode-built application artifacts, with planned support for Android and React Native platforms in our development roadmap. Our vision is to offer a unified approach to understanding the DNA of your applications across all major app development ecosystems. 14 | 15 | ## Development 16 | 17 | ### Set up 18 | 19 | 1. Clone the repository: `git clone https://github.com/tuist/rosalind`. 20 | 2. Install system dependencies: `mise install`. 21 | 3. Install project dependencies: `mise run install`. 22 | 4. Build the project: `mise run build`. 23 | -------------------------------------------------------------------------------- /Sources/Rosalind/AppBundle.swift: -------------------------------------------------------------------------------- 1 | import Path 2 | 3 | struct AppBundle: Equatable { 4 | /// Path to the app bundle 5 | let path: AbsolutePath 6 | 7 | /// The app's Info.plist 8 | let infoPlist: InfoPlist 9 | 10 | struct InfoPlist: Decodable, Equatable { 11 | /// App version number (e.g. 10.3) 12 | let version: String 13 | 14 | /// Name of the app 15 | let name: String 16 | 17 | /// Bundle ID 18 | let bundleId: String 19 | 20 | /// Minimum OS version 21 | let minimumOSVersion: String 22 | 23 | /// Supported destination platforms. 24 | let supportedPlatforms: [String] 25 | 26 | init( 27 | version: String, 28 | name: String, 29 | bundleId: String, 30 | minimumOSVersion: String, 31 | supportedPlatforms: [String] 32 | ) { 33 | self.version = version 34 | self.name = name 35 | self.bundleId = bundleId 36 | self.minimumOSVersion = minimumOSVersion 37 | self.supportedPlatforms = supportedPlatforms 38 | } 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case version = "CFBundleShortVersionString" 42 | case name = "CFBundleName" 43 | case bundleId = "CFBundleIdentifier" 44 | case minimumOSVersion = "MinimumOSVersion" 45 | case supportedPlatforms = "CFBundleSupportedPlatforms" 46 | } 47 | 48 | init(from decoder: any Decoder) throws { 49 | let container: KeyedDecodingContainer = try decoder 50 | .container(keyedBy: AppBundle.InfoPlist.CodingKeys.self) 51 | version = try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.version) 52 | name = try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.name) 53 | bundleId = try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.bundleId) 54 | minimumOSVersion = try container.decode(String.self, forKey: AppBundle.InfoPlist.CodingKeys.minimumOSVersion) 55 | supportedPlatforms = try container.decode([String].self, forKey: AppBundle.InfoPlist.CodingKeys.supportedPlatforms) 56 | } 57 | } 58 | } 59 | 60 | #if DEBUG 61 | extension AppBundle { 62 | static func test( 63 | // swiftlint:disable:next force_try 64 | path: AbsolutePath = try! AbsolutePath(validating: "/App.app"), 65 | infoPlist: AppBundle.InfoPlist = .test() 66 | ) -> AppBundle { 67 | AppBundle( 68 | path: path, 69 | infoPlist: infoPlist 70 | ) 71 | } 72 | } 73 | 74 | extension AppBundle.InfoPlist { 75 | static func test( 76 | version: String = "1.0", 77 | name: String = "App", 78 | bundleId: String = "com.App", 79 | minimumOSVersion: String = "18.0", 80 | supportedPlatforms: [String] = ["iPhoneOS"] 81 | ) -> AppBundle.InfoPlist { 82 | AppBundle.InfoPlist( 83 | version: version, 84 | name: name, 85 | bundleId: bundleId, 86 | minimumOSVersion: minimumOSVersion, 87 | supportedPlatforms: supportedPlatforms 88 | ) 89 | } 90 | } 91 | #endif 92 | -------------------------------------------------------------------------------- /Sources/Rosalind/AppBundleLoader.swift: -------------------------------------------------------------------------------- 1 | @preconcurrency import FileSystem 2 | import Foundation 3 | import Mockable 4 | import Path 5 | 6 | enum AppBundleLoaderError: LocalizedError, Equatable { 7 | case missingInfoPlist(AbsolutePath) 8 | case failedDecodingInfoPlist(AbsolutePath, String) 9 | 10 | var errorDescription: String? { 11 | switch self { 12 | case let .missingInfoPlist(path): 13 | return "Expected Info.plist at \(path) was not found. Make sure it exists." 14 | case let .failedDecodingInfoPlist(path, reason): 15 | return "Failed decoding Info.plist at \(path) due to: \(reason)" 16 | } 17 | } 18 | } 19 | 20 | @Mockable 21 | protocol AppBundleLoading: Sendable { 22 | func load(_ appBundle: AbsolutePath) async throws -> AppBundle 23 | } 24 | 25 | struct AppBundleLoader: AppBundleLoading { 26 | private let fileSystem: FileSysteming 27 | 28 | init( 29 | fileSystem: FileSysteming = FileSystem() 30 | ) { 31 | self.fileSystem = fileSystem 32 | } 33 | 34 | func load(_ appBundle: AbsolutePath) async throws -> AppBundle { 35 | let infoPlistPath = appBundle.appending(component: "Info.plist") 36 | 37 | if try await !fileSystem.exists(infoPlistPath) { 38 | throw AppBundleLoaderError.missingInfoPlist(infoPlistPath) 39 | } 40 | 41 | let data = try Data(contentsOf: URL(fileURLWithPath: infoPlistPath.pathString)) 42 | let decoder = PropertyListDecoder() 43 | 44 | let infoPlist: AppBundle.InfoPlist 45 | do { 46 | infoPlist = try decoder.decode(AppBundle.InfoPlist.self, from: data) 47 | } catch { 48 | throw AppBundleLoaderError.failedDecodingInfoPlist(infoPlistPath, error.localizedDescription) 49 | } 50 | 51 | return AppBundle( 52 | path: appBundle, 53 | infoPlist: infoPlist 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Rosalind/AssetUtilController.swift: -------------------------------------------------------------------------------- 1 | import Command 2 | import Foundation 3 | import Mockable 4 | import Path 5 | 6 | enum AssetUtilControllerError: LocalizedError { 7 | case parsingFailed(AbsolutePath) 8 | 9 | var errorDescription: String? { 10 | switch self { 11 | case let .parsingFailed(path): 12 | return "Parsing of \(path.pathString) failed. Make sure the file is valid." 13 | } 14 | } 15 | } 16 | 17 | struct AssetInfo: Decodable { 18 | enum CodingKeys: String, CodingKey { 19 | case sizeOnDisk = "SizeOnDisk" 20 | case sha1Digest = "SHA1Digest" 21 | case renditionName = "RenditionName" 22 | } 23 | 24 | // All properties are optional because [AssetInfo] is a hetergoneous array 25 | let sizeOnDisk: Int? 26 | let sha1Digest: String? 27 | let renditionName: String? 28 | } 29 | 30 | @Mockable 31 | protocol AssetUtilControlling: Sendable { 32 | func info(at path: AbsolutePath) async throws -> [AssetInfo] 33 | } 34 | 35 | struct AssetUtilController: AssetUtilControlling { 36 | private let commandRunner: CommandRunning 37 | private let jsonDecoder = JSONDecoder() 38 | 39 | init(commandRunner: CommandRunning = CommandRunner()) { 40 | self.commandRunner = commandRunner 41 | } 42 | 43 | func info(at path: AbsolutePath) async throws -> [AssetInfo] { 44 | guard let data = try await commandRunner.run(arguments: ["/usr/bin/xcrun", "assetutil", "--info", path.pathString]) 45 | .concatenatedString() 46 | .data(using: .utf8) 47 | else { 48 | throw AssetUtilControllerError.parsingFailed(path) 49 | } 50 | 51 | return try jsonDecoder.decode([AssetInfo].self, from: data) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Rosalind/Extensions/FileHandle+Read.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FileHandle { 4 | @_spi(Support) 5 | public func read( 6 | offset: UInt64, 7 | swapHandler: ((inout Data) -> Void)? = nil 8 | ) -> Element? { 9 | seek(toFileOffset: offset) 10 | var data = readData( 11 | ofLength: MemoryLayout.size 12 | ) 13 | guard data.count >= MemoryLayout.size else { return nil } 14 | if let swapHandler { swapHandler(&data) } 15 | return data.withUnsafeBytes { 16 | $0.load(as: Element.self) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Rosalind/Extensions/Sequence+Concurrency.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Sequence { 4 | func asyncMap(_ transform: @Sendable @escaping (Element) async throws -> T) async throws -> [T] 5 | where Element: Sendable 6 | { 7 | let elements = Array(self) 8 | 9 | if elements.isEmpty { 10 | return [] 11 | } 12 | var results = [T?](repeating: nil, count: elements.count) 13 | 14 | try await withThrowingTaskGroup(of: (Int, T).self) { group in 15 | for (index, element) in elements.enumerated() { 16 | group.addTask { 17 | let result = try await transform(element) 18 | return (index, result) 19 | } 20 | } 21 | for try await (index, result) in group { 22 | results[index] = result 23 | } 24 | } 25 | return results.compactMap { $0 } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Rosalind/Rosalind.swift: -------------------------------------------------------------------------------- 1 | import Command 2 | @preconcurrency import FileSystem 3 | import Foundation 4 | import MachOKit 5 | import Path 6 | 7 | enum RosalindError: LocalizedError, Equatable { 8 | case notFound(AbsolutePath) 9 | case appNotFound(AbsolutePath) 10 | case notSupported(AbsolutePath) 11 | 12 | var errorDescription: String? { 13 | switch self { 14 | case let .notFound(path): 15 | return "File not found at path \(path.pathString)" 16 | case let .appNotFound(path): 17 | return "No app found at \(path). Make sure the passed app bundle is valid." 18 | case let .notSupported(path): 19 | return "The app bundle \(path) is not supported. Only `.xcarchive`, `.ipa`, and `.app` bundles are supported." 20 | } 21 | } 22 | } 23 | 24 | public protocol Rosalindable: Sendable { 25 | func analyzeAppBundle(at path: AbsolutePath) async throws -> AppBundleReport 26 | } 27 | 28 | enum FileSystemArtifact { 29 | case file(AbsolutePath) 30 | case directory(AbsolutePath) 31 | 32 | var path: AbsolutePath { 33 | switch self { 34 | case let .file(path): return path 35 | case let .directory(path): return path 36 | } 37 | } 38 | 39 | var isFile: Bool { 40 | switch self { 41 | case .file: 42 | return true 43 | case .directory: 44 | return false 45 | } 46 | } 47 | 48 | var isDirectory: Bool { 49 | switch self { 50 | case .file: 51 | return false 52 | case .directory: 53 | return true 54 | } 55 | } 56 | } 57 | 58 | /// Rosalind is the main interface to analyzing app artifacts. 59 | /// Once instantiated, you can invoke the function `analyze` passing an absolute path to the artifact, 60 | /// and you'll get a `Codable` report back. 61 | public struct Rosalind: Rosalindable { 62 | private let fileSystem: FileSysteming 63 | private let appBundleLoader: AppBundleLoading 64 | private let shasumCalculator: ShasumCalculating 65 | private let assetUtilController: AssetUtilControlling 66 | 67 | /// The default constructor of Rosalind. 68 | public init() { 69 | self.init( 70 | fileSystem: FileSystem(), 71 | appBundleLoader: AppBundleLoader(), 72 | shasumCalculator: ShasumCalculator(), 73 | assetUtilController: AssetUtilController() 74 | ) 75 | } 76 | 77 | init( 78 | fileSystem: FileSysteming, 79 | appBundleLoader: AppBundleLoading, 80 | shasumCalculator: ShasumCalculating, 81 | assetUtilController: AssetUtilControlling 82 | ) { 83 | self.fileSystem = fileSystem 84 | self.appBundleLoader = appBundleLoader 85 | self.shasumCalculator = shasumCalculator 86 | self.assetUtilController = assetUtilController 87 | } 88 | 89 | /// Given the absolute path to an artifact that's result of a compilation, for example a .app bundle, 90 | /// Rosalind analyzes it and returns a report. 91 | /// - Parameter path: Absolute path to the artifact. If it doesn't exist, Rosalind throws. 92 | /// - Returns: A `RosalindReport` instance that captures the analysis. 93 | public func analyzeAppBundle(at path: AbsolutePath) async throws -> AppBundleReport { 94 | guard try await fileSystem.exists(path) else { throw RosalindError.notFound(path) } 95 | return try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 96 | let appBundlePath = try await appBundlePath(path, temporaryDirectory: temporaryDirectory) 97 | let artifactPath = try await pathToArtifact(appBundlePath) 98 | let artifact = try await traverse( 99 | artifact: artifactPath, 100 | baseArtifact: artifactPath 101 | ) 102 | let appBundle = try await appBundleLoader.load(appBundlePath) 103 | 104 | let downloadSize: Int? 105 | switch path.extension { 106 | case "ipa": 107 | downloadSize = try fileSize(at: path) 108 | default: 109 | downloadSize = nil 110 | } 111 | 112 | return AppBundleReport( 113 | bundleId: appBundle.infoPlist.bundleId, 114 | name: appBundle.infoPlist.name, 115 | installSize: artifact.size, 116 | downloadSize: downloadSize, 117 | platforms: appBundle.infoPlist.supportedPlatforms, 118 | version: appBundle.infoPlist.version, 119 | artifacts: artifact.children ?? [] 120 | ) 121 | } 122 | } 123 | 124 | private func appBundlePath( 125 | _ path: AbsolutePath, 126 | temporaryDirectory: AbsolutePath 127 | ) async throws -> AbsolutePath { 128 | switch path.extension { 129 | case "xcarchive": 130 | guard let appPath = try await fileSystem.glob( 131 | directory: path.appending(components: "Products", "Applications"), 132 | include: ["*.app"] 133 | ) 134 | .collect() 135 | .first else { 136 | throw RosalindError.appNotFound(path) 137 | } 138 | return appPath 139 | case "ipa": 140 | let unzippedPath = temporaryDirectory.appending(component: "App") 141 | try await fileSystem.unzip(path, to: unzippedPath) 142 | guard let appPath = try await fileSystem.glob( 143 | directory: unzippedPath.appending(component: "Payload"), 144 | include: ["*.app"] 145 | ) 146 | .collect() 147 | .first else { 148 | throw RosalindError.appNotFound(path) 149 | } 150 | return appPath 151 | case "app": 152 | return path 153 | default: 154 | throw RosalindError.notSupported(path) 155 | } 156 | } 157 | 158 | private func traverse(artifact: FileSystemArtifact, baseArtifact: FileSystemArtifact) async throws -> AppBundleArtifact { 159 | let children: [AppBundleArtifact]? 160 | let artifactType = try artifactType(for: artifact) 161 | switch artifactType { 162 | case .asset: 163 | let infos = try await assetUtilController.info(at: artifact.path) 164 | children = try infos.compactMap { info -> AppBundleArtifact? in 165 | guard let sizeOnDisk = info.sizeOnDisk, 166 | let sha1Digest = info.sha1Digest, 167 | let renditionName = info.renditionName 168 | else { return nil } 169 | 170 | return AppBundleArtifact( 171 | artifactType: .asset, 172 | path: try RelativePath(validating: baseArtifact.path.basename) 173 | .appending(artifact.path.appending(component: renditionName).relative(to: baseArtifact.path)).pathString, 174 | size: sizeOnDisk, 175 | shasum: sha1Digest.lowercased(), 176 | children: nil 177 | ) 178 | } 179 | case .directory: 180 | children = try await fileSystem.glob(directory: artifact.path, include: ["*"]).collect().sorted() 181 | .asyncMap { 182 | try await traverse(artifact: pathToArtifact($0), baseArtifact: baseArtifact) 183 | } 184 | case .file, .binary, .localization, .font: 185 | children = nil 186 | } 187 | 188 | let size = try await size(artifact: artifact, children: children ?? []) 189 | let shasum = try await shasum(artifact: artifact, children: children ?? []) 190 | return AppBundleArtifact( 191 | artifactType: artifactType, 192 | path: try RelativePath(validating: baseArtifact.path.basename) 193 | .appending(artifact.path.relative(to: baseArtifact.path)).pathString, 194 | size: size, 195 | shasum: shasum, 196 | children: children 197 | ) 198 | } 199 | 200 | private func artifactType(for artifact: FileSystemArtifact) throws -> AppBundleArtifact.ArtifactType { 201 | switch artifact.path.extension { 202 | case "otf", "ttc", "ttf", "woff": return .font 203 | case "strings", "xcstrings": return .localization 204 | case "car": return .asset 205 | default: 206 | if artifact.isDirectory { 207 | return .directory 208 | } else { 209 | let fileURL = URL(fileURLWithPath: artifact.path.pathString) 210 | let fileHandle = try FileHandle(forReadingFrom: fileURL) 211 | defer { try? fileHandle.close() } 212 | 213 | if let magicRaw: UInt32 = fileHandle.read(offset: 0), 214 | Magic(rawValue: magicRaw) != nil 215 | { 216 | return .binary 217 | } else { 218 | return .file 219 | } 220 | } 221 | } 222 | } 223 | 224 | private func shasum(artifact: FileSystemArtifact, children: [AppBundleArtifact]) async throws -> String { 225 | if artifact.isDirectory { 226 | return try await shasumCalculator.calculate(childrenShasums: children.map(\.shasum).sorted()) 227 | } else { 228 | return try await shasumCalculator.calculate(filePath: artifact.path) 229 | } 230 | } 231 | 232 | private func pathToArtifact(_ path: AbsolutePath) async throws -> FileSystemArtifact { 233 | (try await fileSystem.exists(path, isDirectory: true)) ? .directory(path) : .file(path) 234 | } 235 | 236 | private func size(artifact: FileSystemArtifact, children: [AppBundleArtifact]) async throws -> Int { 237 | if artifact.isDirectory { 238 | return children.map(\.size).reduce(0, +) 239 | } else { 240 | return try fileSize(at: artifact.path) 241 | } 242 | } 243 | 244 | private func fileSize(at path: AbsolutePath) throws -> Int { 245 | ((try FileManager.default.attributesOfItem(atPath: path.pathString))[.size] as? Int) ?? 0 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Sources/Rosalind/RosalindReport.swift: -------------------------------------------------------------------------------- 1 | // A Rosalind report of an app bundle such as `.ipa`. 2 | public struct AppBundleReport: Sendable, Codable, Equatable { 3 | /// App's Bundle ID 4 | public let bundleId: String 5 | /// The app name 6 | public let name: String 7 | /// The app install size in bytes. This is the size of the `.app` bundle and represents the value that will be installed on 8 | /// the device. 9 | public let installSize: Int 10 | /// The app download size in bytes. Only available for `.ipa`. It represents the compressed size that the users will end up 11 | /// downloading over the network. 12 | public let downloadSize: Int? 13 | /// List of supported platforms, such as `iPhoneSimulator`. List of possible values is the same as for 14 | /// `CFBundleSupportedPlatforms`. 15 | public let platforms: [String] 16 | /// The app version. 17 | public let version: String 18 | /// List of app-specific artifacts, such as fonts or binaries. 19 | public let artifacts: [AppBundleArtifact] 20 | 21 | public init( 22 | bundleId: String, 23 | name: String, 24 | installSize: Int, 25 | downloadSize: Int?, 26 | platforms: [String], 27 | version: String, 28 | artifacts: [AppBundleArtifact] 29 | ) { 30 | self.bundleId = bundleId 31 | self.name = name 32 | self.installSize = installSize 33 | self.downloadSize = downloadSize 34 | self.platforms = platforms 35 | self.version = version 36 | self.artifacts = artifacts 37 | } 38 | } 39 | 40 | public struct AppBundleArtifact: Sendable, Codable, Equatable { 41 | public enum ArtifactType: String, Sendable, Codable, Equatable { 42 | /// A generic directory artifact type. 43 | case directory 44 | /// A generic file artifact type when the file is not recognized as something more specific, such as `.font`. 45 | case file 46 | /// A font artifact. A font is considered any file with one of the following extensions: `.otf`, `.ttc`, `.ttf`, `.woff`. 47 | case font 48 | /// A binary recognized by the file's header which has to be either `MH_*` or `FAT_*`. 49 | case binary 50 | /// A localization file is any file that has one of the following extensions: `.strings` or `.xcstrings`. 51 | case localization 52 | /// An asset – either a `.car` file or a file inside the `.car` obtained using the `assetutils`. 53 | case asset 54 | } 55 | 56 | /// The type of the artifact, such as `.font`. 57 | public let artifactType: ArtifactType 58 | public let path: String 59 | public let size: Int 60 | public let shasum: String 61 | public let children: [AppBundleArtifact]? 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Rosalind/ShasumCalculator.swift: -------------------------------------------------------------------------------- 1 | import Command 2 | import Crypto 3 | @preconcurrency import FileSystem 4 | import Foundation 5 | import Mockable 6 | import Path 7 | 8 | @Mockable 9 | protocol ShasumCalculating: Sendable { 10 | func calculate(childrenShasums: [String]) async throws -> String 11 | func calculate(filePath: AbsolutePath) async throws -> String 12 | } 13 | 14 | struct ShasumCalculator: ShasumCalculating { 15 | private let fileSystem: FileSysteming 16 | private let commandRunner: CommandRunning 17 | 18 | init(fileSystem: FileSysteming = FileSystem(), commandRunner: CommandRunning = CommandRunner()) { 19 | self.fileSystem = fileSystem 20 | self.commandRunner = commandRunner 21 | } 22 | 23 | func calculate(childrenShasums: [String]) async throws -> String { 24 | let digest = SHA256.hash(data: childrenShasums.joined().data(using: .utf8)!) 25 | return digest.compactMap { String(format: "%02x", $0) }.joined() 26 | } 27 | 28 | func calculate(filePath: Path.AbsolutePath) async throws -> String { 29 | let fileHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: filePath.pathString)) 30 | defer { try? fileHandle.close() } 31 | 32 | var hasher = SHA256() 33 | let chunkSize = 1024 * 1024 34 | 35 | var shouldContinue = true 36 | while shouldContinue { 37 | guard let data = try? fileHandle.read(upToCount: chunkSize), !data.isEmpty else { 38 | shouldContinue = false 39 | continue 40 | } 41 | hasher.update(data: data) 42 | } 43 | 44 | let digest = hasher.finalize() 45 | return digest.compactMap { String(format: "%02x", $0) }.joined() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/RosalindTests/Extensions/Snapshotting+RosalindReport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Rosalind 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | extension Diffing { 7 | fileprivate static func rosalind() -> Diffing { 8 | let jsonEncoder = JSONEncoder() 9 | let jsonDecoder = JSONDecoder() 10 | jsonEncoder.outputFormatting = [.prettyPrinted, .sortedKeys] 11 | 12 | return Diffing.init(toData: { value in 13 | try! jsonEncoder.encode(value) 14 | }, fromData: { data in 15 | try! jsonDecoder.decode(AppBundleReport.self, from: data) 16 | }, diff: { (lhs: AppBundleReport, rhs: AppBundleReport) -> (String, [XCTAttachment])? in 17 | if lhs == rhs { 18 | return nil 19 | } else { 20 | return Snapshotting.json.diffing.diff( 21 | try! String(decoding: jsonEncoder.encode(lhs), as: UTF8.self), 22 | try! String(decoding: jsonEncoder.encode(rhs), as: UTF8.self) 23 | ) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | extension Snapshotting { 30 | static func rosalind() -> Snapshotting { 31 | Snapshotting.init(pathExtension: nil, diffing: .rosalind()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/RosalindTests/RosalindAcceptanceTests.swift: -------------------------------------------------------------------------------- 1 | import Command 2 | import FileSystem 3 | import Foundation 4 | import Path 5 | import Rosalind 6 | import SnapshotTesting 7 | import Testing 8 | 9 | struct RosalindAcceptanceTests { 10 | private let fileSystem = FileSystem() 11 | private let subject = Rosalind() 12 | 13 | // We run `assetutils` as part of these acceptance tests, so these won't run on Linux 14 | #if os(macOS) 15 | @Test func ios_app() async throws { 16 | try await withFixtureInTemporaryDirectory("ios_app") { _, fixtureDirectory in 17 | // When 18 | let got = try await subject 19 | .analyzeAppBundle( 20 | at: fixtureDirectory.appending(component: "App.app") 21 | ) 22 | 23 | // Then 24 | assertSnapshot( 25 | of: got, 26 | as: .rosalind() 27 | ) 28 | } 29 | } 30 | 31 | @Test func ios_app_xcarchive() async throws { 32 | try await withFixtureInTemporaryDirectory("ios_app") { _, fixtureDirectory in 33 | // When 34 | let got = try await subject 35 | .analyzeAppBundle( 36 | at: fixtureDirectory.appending(component: "App.xcarchive") 37 | ) 38 | 39 | // Then 40 | assertSnapshot( 41 | of: got, 42 | as: .rosalind() 43 | ) 44 | } 45 | } 46 | 47 | @Test func ios_app_ipa() async throws { 48 | try await withFixtureInTemporaryDirectory("ios_app") { _, fixtureDirectory in 49 | // When 50 | let got = try await subject 51 | .analyzeAppBundle( 52 | at: fixtureDirectory.appending(component: "App.ipa") 53 | ) 54 | 55 | // Then 56 | assertSnapshot( 57 | of: got, 58 | as: .rosalind() 59 | ) 60 | } 61 | } 62 | #endif 63 | 64 | private func withFixtureInTemporaryDirectory( 65 | _ fixturePath: String, 66 | callback: (AbsolutePath, AbsolutePath) async throws -> Void 67 | ) async throws { 68 | try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 69 | let sourceFixtureDirectory = try AbsolutePath(validating: "\(#file)").parentDirectory.parentDirectory.parentDirectory 70 | .appending(component: "fixtures") 71 | .appending(try RelativePath(validating: fixturePath)) 72 | let targetFixtureDirectory = temporaryDirectory.appending(component: sourceFixtureDirectory.basename) 73 | try await fileSystem.copy(sourceFixtureDirectory, to: targetFixtureDirectory) 74 | try await callback(temporaryDirectory, targetFixtureDirectory) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/RosalindTests/RosalindTests.swift: -------------------------------------------------------------------------------- 1 | import FileSystem 2 | import Foundation 3 | import Mockable 4 | import Testing 5 | 6 | @testable import Rosalind 7 | 8 | struct RosalindTests { 9 | private let fileSystem = FileSystem() 10 | private let appBundleLoader = MockAppBundleLoading() 11 | private let shasumCalculator = MockShasumCalculating() 12 | private let assetUtilController = MockAssetUtilControlling() 13 | private let subject: Rosalind 14 | 15 | init() { 16 | given(shasumCalculator) 17 | .calculate(filePath: .any) 18 | .willProduce { $0.basename } 19 | given(shasumCalculator) 20 | .calculate(childrenShasums: .any) 21 | .willProduce { $0.joined(separator: "-") } 22 | subject = Rosalind( 23 | fileSystem: fileSystem, 24 | appBundleLoader: appBundleLoader, 25 | shasumCalculator: shasumCalculator, 26 | assetUtilController: assetUtilController 27 | ) 28 | } 29 | 30 | @Test func appBundleDoesNotExist() async throws { 31 | try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 32 | // Given 33 | let appBundlePath = temporaryDirectory.appending(component: "App.app") 34 | // When / Then 35 | await #expect( 36 | throws: RosalindError.notFound(appBundlePath) 37 | ) { 38 | try await subject.analyzeAppBundle(at: appBundlePath) 39 | } 40 | } 41 | } 42 | 43 | @Test func appBundle() async throws { 44 | try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 45 | // Given 46 | let appBundlePath = temporaryDirectory.appending(component: "App.app") 47 | try await fileSystem.makeDirectory(at: appBundlePath) 48 | try await fileSystem.writeText("font-binary", at: appBundlePath.appending(component: "Font.ttf")) 49 | given(appBundleLoader) 50 | .load(.any) 51 | .willReturn( 52 | .test( 53 | infoPlist: .test( 54 | name: "App", 55 | bundleId: "com.App", 56 | supportedPlatforms: ["iPhoneOS"] 57 | ) 58 | ) 59 | ) 60 | try await fileSystem.makeDirectory(at: appBundlePath.appending(component: "en.lproj")) 61 | try await fileSystem.writeText("app = App;", at: appBundlePath.appending(components: "en.lproj", "App.strings")) 62 | 63 | // When 64 | let got = try await subject.analyzeAppBundle(at: appBundlePath) 65 | 66 | // Then 67 | #expect( 68 | got == AppBundleReport( 69 | bundleId: "com.App", 70 | name: "App", 71 | installSize: 21, 72 | downloadSize: nil, 73 | platforms: ["iPhoneOS"], 74 | version: "1.0", 75 | artifacts: [ 76 | AppBundleArtifact( 77 | artifactType: .font, 78 | path: "App.app/Font.ttf", 79 | size: 11, 80 | shasum: "Font.ttf", 81 | children: nil 82 | ), 83 | AppBundleArtifact( 84 | artifactType: .directory, 85 | path: "App.app/en.lproj", 86 | size: 10, 87 | shasum: "App.strings", 88 | children: [ 89 | AppBundleArtifact( 90 | artifactType: .localization, 91 | path: "App.app/en.lproj/App.strings", 92 | size: 10, 93 | shasum: "App.strings", 94 | children: nil 95 | ), 96 | ] 97 | ), 98 | ] 99 | ) 100 | ) 101 | } 102 | } 103 | 104 | @Test func appInXCArchiveDoesNotExist() async throws { 105 | try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 106 | // Given 107 | let xcarchivePath = temporaryDirectory.appending(component: "App.xcarchive") 108 | try await fileSystem.makeDirectory(at: xcarchivePath) 109 | // When / Then 110 | await #expect( 111 | throws: RosalindError.appNotFound(xcarchivePath) 112 | ) { 113 | try await subject.analyzeAppBundle(at: xcarchivePath) 114 | } 115 | } 116 | } 117 | 118 | @Test func appInIPADoesNotExist() async throws { 119 | try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 120 | // Given 121 | try await fileSystem.makeDirectory(at: temporaryDirectory.appending(component: "Payload")) 122 | let ipaPath = temporaryDirectory.appending(component: "App.ipa") 123 | try await fileSystem.zipFileOrDirectoryContent(at: temporaryDirectory.appending(component: "Payload"), to: ipaPath) 124 | // When / Then 125 | await #expect( 126 | throws: RosalindError.appNotFound(ipaPath) 127 | ) { 128 | try await subject.analyzeAppBundle(at: ipaPath) 129 | } 130 | } 131 | } 132 | 133 | @Test func appBundleNotSupported() async throws { 134 | try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryDirectory in 135 | // Given 136 | let apkPath = temporaryDirectory.appending(component: "App.apk") 137 | try await fileSystem.makeDirectory(at: apkPath) 138 | // When / Then 139 | await #expect( 140 | throws: RosalindError.notSupported(apkPath) 141 | ) { 142 | try await subject.analyzeAppBundle(at: apkPath) 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/RosalindTests/__Snapshots__/RosalindAcceptanceTests/ios_app.1: -------------------------------------------------------------------------------- 1 | { 2 | "artifacts" : [ 3 | { 4 | "artifactType" : "binary", 5 | "path" : "App.app\/App", 6 | "shasum" : "3317dd834f446547065042f12c9a953dd61bf6007f53b2da8bfb9d48906f23d6", 7 | "size" : 91520 8 | }, 9 | { 10 | "artifactType" : "binary", 11 | "path" : "App.app\/App.debug.dylib", 12 | "shasum" : "5a1f7e4e7ce91c23a024d8776e95e86c0ad71d48df8cff239220f7589f89d49d", 13 | "size" : 146880 14 | }, 15 | { 16 | "artifactType" : "asset", 17 | "children" : [ 18 | { 19 | "artifactType" : "asset", 20 | "path" : "App.app\/Assets.car\/Tuist-Filled.png", 21 | "shasum" : "880c0fec121c36e2fe248e730234ccd6cdea5bbe46dea6c132d7c9bfb6a4e32b", 22 | "size" : 699 23 | }, 24 | { 25 | "artifactType" : "asset", 26 | "path" : "App.app\/Assets.car\/Tuist-Filled@2x.png", 27 | "shasum" : "62618f6f558cd57f6ecd5a3171324b2328ba44df7f13fda31cee214bc9ba2b10", 28 | "size" : 1370 29 | }, 30 | { 31 | "artifactType" : "asset", 32 | "path" : "App.app\/Assets.car\/Tuist-Filled@3x.png", 33 | "shasum" : "3171a380ea518c7de3fd35a0cb5a5dcb47d9df2bb72a2c7d75f48762dbda6463", 34 | "size" : 2033 35 | } 36 | ], 37 | "path" : "App.app\/Assets.car", 38 | "shasum" : "9bab6256f2842bbe1e55116e3dc95a5be7c8cbf679ea9d593c871ee3ba97d52b", 39 | "size" : 26175 40 | }, 41 | { 42 | "artifactType" : "file", 43 | "path" : "App.app\/Info.plist", 44 | "shasum" : "8c649d8b1680706da7c41b250849f5fa8f3faa7c39ae3083f9ef8d40ea04abe0", 45 | "size" : 1246 46 | }, 47 | { 48 | "artifactType" : "file", 49 | "path" : "App.app\/PkgInfo", 50 | "shasum" : "82502191c9484b04d685374f9879a0066069c49b8acae7a04b01d38d07e8eca0", 51 | "size" : 8 52 | }, 53 | { 54 | "artifactType" : "directory", 55 | "children" : [ 56 | { 57 | "artifactType" : "file", 58 | "path" : "App.app\/_CodeSignature\/CodeResources", 59 | "shasum" : "2434d1efbebaceaf3a7718beb9a2a0777840a6e8d5042db8b17d123141df4879", 60 | "size" : 2749 61 | } 62 | ], 63 | "path" : "App.app\/_CodeSignature", 64 | "shasum" : "37cc25d3eff8466abed46755183e96e3202702bb6669f9d21b3c5655e3a7e9b7", 65 | "size" : 2749 66 | }, 67 | { 68 | "artifactType" : "binary", 69 | "path" : "App.app\/__preview.dylib", 70 | "shasum" : "8aa3f10d52b0596b5de2668b3d3620378ec41b1a51cf732fe60695dcbb9e2c2f", 71 | "size" : 35024 72 | }, 73 | { 74 | "artifactType" : "file", 75 | "path" : "App.app\/embedded.mobileprovision", 76 | "shasum" : "c1f9bf606f548105625f57e1e9011f47d5f33d2309a30f7b50a38fe81522640f", 77 | "size" : 18493 78 | } 79 | ], 80 | "bundleId" : "dev.tuist.rosalind.App", 81 | "installSize" : 322095, 82 | "name" : "App", 83 | "platforms" : [ 84 | "iPhoneOS" 85 | ], 86 | "version" : "1.0" 87 | } -------------------------------------------------------------------------------- /Tests/RosalindTests/__Snapshots__/RosalindAcceptanceTests/ios_app_ipa.1: -------------------------------------------------------------------------------- 1 | { 2 | "artifacts" : [ 3 | { 4 | "artifactType" : "binary", 5 | "path" : "App.app\/App", 6 | "shasum" : "b9ed48c2cf99b440f6b08c1a0c2a151a581b5d0801332a1487f6594ed6fd94a5", 7 | "size" : 93600 8 | }, 9 | { 10 | "artifactType" : "asset", 11 | "children" : [ 12 | { 13 | "artifactType" : "asset", 14 | "path" : "App.app\/Assets.car\/Tuist-Filled.png", 15 | "shasum" : "880c0fec121c36e2fe248e730234ccd6cdea5bbe46dea6c132d7c9bfb6a4e32b", 16 | "size" : 699 17 | }, 18 | { 19 | "artifactType" : "asset", 20 | "path" : "App.app\/Assets.car\/Tuist-Filled@2x.png", 21 | "shasum" : "62618f6f558cd57f6ecd5a3171324b2328ba44df7f13fda31cee214bc9ba2b10", 22 | "size" : 1370 23 | }, 24 | { 25 | "artifactType" : "asset", 26 | "path" : "App.app\/Assets.car\/Tuist-Filled@3x.png", 27 | "shasum" : "3171a380ea518c7de3fd35a0cb5a5dcb47d9df2bb72a2c7d75f48762dbda6463", 28 | "size" : 2033 29 | } 30 | ], 31 | "path" : "App.app\/Assets.car", 32 | "shasum" : "9bab6256f2842bbe1e55116e3dc95a5be7c8cbf679ea9d593c871ee3ba97d52b", 33 | "size" : 26175 34 | }, 35 | { 36 | "artifactType" : "file", 37 | "path" : "App.app\/Info.plist", 38 | "shasum" : "8c649d8b1680706da7c41b250849f5fa8f3faa7c39ae3083f9ef8d40ea04abe0", 39 | "size" : 1246 40 | }, 41 | { 42 | "artifactType" : "file", 43 | "path" : "App.app\/PkgInfo", 44 | "shasum" : "82502191c9484b04d685374f9879a0066069c49b8acae7a04b01d38d07e8eca0", 45 | "size" : 8 46 | }, 47 | { 48 | "artifactType" : "directory", 49 | "children" : [ 50 | { 51 | "artifactType" : "file", 52 | "path" : "App.app\/_CodeSignature\/CodeResources", 53 | "shasum" : "430f792abd6a987126b4eb5aa267528e23040fdfaf27856d08a547b2dacc4461", 54 | "size" : 2317 55 | } 56 | ], 57 | "path" : "App.app\/_CodeSignature", 58 | "shasum" : "4cc29bd23aa69892a00a6a668566fa8fac7af5f14a1d6bcca55084206e9567b3", 59 | "size" : 2317 60 | }, 61 | { 62 | "artifactType" : "file", 63 | "path" : "App.app\/embedded.mobileprovision", 64 | "shasum" : "c1f9bf606f548105625f57e1e9011f47d5f33d2309a30f7b50a38fe81522640f", 65 | "size" : 18493 66 | } 67 | ], 68 | "bundleId" : "dev.tuist.rosalind.App", 69 | "downloadSize" : 27193, 70 | "installSize" : 141839, 71 | "name" : "App", 72 | "platforms" : [ 73 | "iPhoneOS" 74 | ], 75 | "version" : "1.0" 76 | } -------------------------------------------------------------------------------- /Tests/RosalindTests/__Snapshots__/RosalindAcceptanceTests/ios_app_xcarchive.1: -------------------------------------------------------------------------------- 1 | { 2 | "artifacts" : [ 3 | { 4 | "artifactType" : "binary", 5 | "path" : "App.app\/App", 6 | "shasum" : "6c96b831f3b2a53d1bda562788cdcc33039ab6c43612d7cce60bcd0de137b6f5", 7 | "size" : 93600 8 | }, 9 | { 10 | "artifactType" : "asset", 11 | "children" : [ 12 | { 13 | "artifactType" : "asset", 14 | "path" : "App.app\/Assets.car\/Tuist-Filled.png", 15 | "shasum" : "880c0fec121c36e2fe248e730234ccd6cdea5bbe46dea6c132d7c9bfb6a4e32b", 16 | "size" : 699 17 | }, 18 | { 19 | "artifactType" : "asset", 20 | "path" : "App.app\/Assets.car\/Tuist-Filled@2x.png", 21 | "shasum" : "62618f6f558cd57f6ecd5a3171324b2328ba44df7f13fda31cee214bc9ba2b10", 22 | "size" : 1370 23 | }, 24 | { 25 | "artifactType" : "asset", 26 | "path" : "App.app\/Assets.car\/Tuist-Filled@3x.png", 27 | "shasum" : "3171a380ea518c7de3fd35a0cb5a5dcb47d9df2bb72a2c7d75f48762dbda6463", 28 | "size" : 2033 29 | } 30 | ], 31 | "path" : "App.app\/Assets.car", 32 | "shasum" : "9bab6256f2842bbe1e55116e3dc95a5be7c8cbf679ea9d593c871ee3ba97d52b", 33 | "size" : 26175 34 | }, 35 | { 36 | "artifactType" : "file", 37 | "path" : "App.app\/Info.plist", 38 | "shasum" : "8c649d8b1680706da7c41b250849f5fa8f3faa7c39ae3083f9ef8d40ea04abe0", 39 | "size" : 1246 40 | }, 41 | { 42 | "artifactType" : "file", 43 | "path" : "App.app\/PkgInfo", 44 | "shasum" : "82502191c9484b04d685374f9879a0066069c49b8acae7a04b01d38d07e8eca0", 45 | "size" : 8 46 | }, 47 | { 48 | "artifactType" : "directory", 49 | "children" : [ 50 | { 51 | "artifactType" : "file", 52 | "path" : "App.app\/_CodeSignature\/CodeResources", 53 | "shasum" : "430f792abd6a987126b4eb5aa267528e23040fdfaf27856d08a547b2dacc4461", 54 | "size" : 2317 55 | } 56 | ], 57 | "path" : "App.app\/_CodeSignature", 58 | "shasum" : "4cc29bd23aa69892a00a6a668566fa8fac7af5f14a1d6bcca55084206e9567b3", 59 | "size" : 2317 60 | }, 61 | { 62 | "artifactType" : "file", 63 | "path" : "App.app\/embedded.mobileprovision", 64 | "shasum" : "c1f9bf606f548105625f57e1e9011f47d5f33d2309a30f7b50a38fe81522640f", 65 | "size" : 18493 66 | } 67 | ], 68 | "bundleId" : "dev.tuist.rosalind.App", 69 | "installSize" : 141839, 70 | "name" : "App", 71 | "platforms" : [ 72 | "iPhoneOS" 73 | ], 74 | "version" : "1.0" 75 | } -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [remote.github] 5 | owner = "tuist" 6 | repo = "Rosalind" 7 | # token = "" 8 | 9 | [changelog] 10 | # template for the changelog header 11 | header = """ 12 | # Changelog\n 13 | All notable changes to this project will be documented in this file. 14 | 15 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 16 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n 17 | """ 18 | # template for the changelog body 19 | # https://keats.github.io/tera/docs/#introduction 20 | body = """ 21 | {%- macro remote_url() -%} 22 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 23 | {%- endmacro -%} 24 | 25 | {% if version -%} 26 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 27 | {% else -%} 28 | ## [Unreleased] 29 | {% endif -%} 30 | 31 | ### Details\ 32 | 33 | {% for group, commits in commits | group_by(attribute="group") %} 34 | #### {{ group | upper_first }} 35 | {%- for commit in commits %} 36 | - {{ commit.message | upper_first | trim }}\ 37 | {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%} 38 | {% if commit.github.pr_number %} in \ 39 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \ 40 | {%- endif -%} 41 | {% endfor %} 42 | {% endfor %} 43 | 44 | {%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} 45 | ## New Contributors 46 | {%- endif -%} 47 | 48 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} 49 | * @{{ contributor.username }} made their first contribution 50 | {%- if contributor.pr_number %} in \ 51 | [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ 52 | {%- endif %} 53 | {%- endfor %}\n 54 | """ 55 | # template for the changelog footer 56 | footer = """ 57 | {%- macro remote_url() -%} 58 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 59 | {%- endmacro -%} 60 | 61 | {% for release in releases -%} 62 | {% if release.version -%} 63 | {% if release.previous.version -%} 64 | [{{ release.version | trim_start_matches(pat="v") }}]: \ 65 | {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }} 66 | {% endif -%} 67 | {% else -%} 68 | [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD 69 | {% endif -%} 70 | {% endfor %} 71 | 72 | """ 73 | # remove the leading and trailing whitespace from the templates 74 | trim = true 75 | # postprocessors 76 | postprocessors = [] 77 | 78 | [git] 79 | # parse the commits based on https://www.conventionalcommits.org 80 | conventional_commits = true 81 | # filter out the commits that are not conventional 82 | filter_unconventional = true 83 | # process each line of a commit as an individual commit 84 | split_commits = false 85 | # regex for preprocessing the commit messages 86 | commit_preprocessors = [ 87 | # remove issue numbers from commits 88 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, 89 | ] 90 | # protect breaking changes from being skipped due to matching a skipping commit_parser 91 | protect_breaking_commits = false 92 | # filter out the commits that are not matched by commit parsers 93 | filter_commits = false 94 | # regex for matching git tags 95 | tag_pattern = "[0-9].*" 96 | # regex for skipping tags 97 | skip_tags = "beta|alpha" 98 | # regex for ignoring tags 99 | ignore_tags = "rc" 100 | # sort the tags topologically 101 | topo_order = false 102 | # sort the commits inside sections by oldest/newest order 103 | sort_commits = "newest" 104 | 105 | [bump] 106 | breaking_always_bump_major = true 107 | features_always_bump_minor = true 108 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import { 3 | cubeOutlineIcon, 4 | cube02Icon, 5 | cube01Icon, 6 | barChartSquare02Icon, 7 | code02Icon, 8 | dataIcon, 9 | checkCircleIcon, 10 | tuistIcon, 11 | cloudBlank02Icon, 12 | server04Icon, 13 | } from "./icons.mjs"; 14 | 15 | // https://vitepress.dev/reference/site-config 16 | export default defineConfig({ 17 | title: "Rosalind", 18 | titleTemplate: ":title | Rosalind | Tuist", 19 | description: "Analyze Apple-generated bundles", 20 | sitemap: { 21 | hostname: "https://rosalind.tuist.io", 22 | }, 23 | themeConfig: { 24 | logo: "/logo.png", 25 | search: { 26 | provider: "local", 27 | }, 28 | nav: [ 29 | { 30 | text: "Changelog", 31 | link: "https://github.com/tuist/Rosalind/releases", 32 | }, 33 | ], 34 | editLink: { 35 | pattern: "https://github.com/tuist/Rosalind/edit/main/docs/:path", 36 | }, 37 | sidebar: [ 38 | { 39 | text: `Quick start ${tuistIcon()}`, 40 | items: [ 41 | { text: "Why Rosalind?", link: "/" }, 42 | { text: "Add dependency", link: "/quick-start/add-dependency" }, 43 | ], 44 | }, 45 | { 46 | text: `API ${cube01Icon()}`, 47 | items: [ 48 | { text: "Rosalind", link: "/api/rosalind" }, 49 | { text: "Schema", link: "/api/schema" }, 50 | ], 51 | }, 52 | ], 53 | 54 | socialLinks: [ 55 | { icon: "github", link: "https://github.com/tuist/tuist" }, 56 | { icon: "mastodon", link: "https://fosstodon.org/@tuist" }, 57 | { 58 | icon: "discourse", 59 | link: "https://community.tuist.dev", 60 | }, 61 | ], 62 | footer: { 63 | message: "Released under the MIT License.", 64 | copyright: "Copyright © 2024-present Tuist Inc.", 65 | }, 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /docs/.vitepress/icons.mjs: -------------------------------------------------------------------------------- 1 | export function cubeOutlineIcon(size = 15) { 2 | return ` 3 | 4 | 5 | `; 6 | } 7 | 8 | export function cube02Icon(size = 15) { 9 | return ` 10 | 11 | 12 | `; 13 | } 14 | 15 | export function cube01Icon(size = 15) { 16 | return ` 17 | 18 | 19 | 20 | `; 21 | } 22 | 23 | export function barChartSquare02Icon(size = 15) { 24 | return ` 25 | 26 | 27 | `; 28 | } 29 | 30 | export function code02Icon(size = 15) { 31 | return ` 32 | 33 | 34 | `; 35 | } 36 | 37 | export function dataIcon(size = 15) { 38 | return ` 39 | 40 | 41 | 42 | 43 | `; 44 | } 45 | 46 | export function checkCircleIcon(size = 15) { 47 | return ` 48 | 49 | 50 | `; 51 | } 52 | 53 | export function tuistIcon(size = 15) { 54 | return ` 55 | 56 | `; 57 | } 58 | 59 | export function cloudBlank02Icon(size = 15) { 60 | return ` 61 | 62 | 63 | `; 64 | } 65 | 66 | export function server04Icon(size = 15) { 67 | return ` 68 | 69 | 70 | `; 71 | } 72 | -------------------------------------------------------------------------------- /docs/api/rosalind.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Schema 3 | titleTemplate: ':title | Rosalind | Rosalind | Tuist' 4 | description: "Learn how to use Rosalind to analyze your build artifacts." 5 | --- 6 | 7 | # Rosalind 8 | 9 | The interface with Rosalind is through an instance of the `Rosalind` struct. 10 | It exposes a function, `analyze` that takes a path and returns a [report](/api/schema). 11 | 12 | ```swift 13 | let report = Rosalind().analyze(path: try AbsolutePath(validating: "/path/to/MyApp.app")) 14 | ``` 15 | 16 | Since `RosalindReport` conforms to `Coddable`, you can serialize it into a JSON `Data` instance: 17 | 18 | ```swift 19 | let jsonEncoder = JSONEncoder() 20 | jsonEncoder.outputFormatting = [.prettyPrinted, .sortedKeys] 21 | let reportData = try jsonEncoder.encode(value) 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/api/schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Schema 3 | titleTemplate: ':title | API | Rosalind | Tuist' 4 | description: "Learn about the schema of the results of the analysis." 5 | --- 6 | 7 | # Schema 8 | 9 | > [!TIP] 10 | > We haven't reached 1.0 yet, so breaking changes are possible. 11 | 12 | Rosalind returns an instance of `RosalindReport`, a type that can be encoded to [JSON](https://www.w3schools.com/whatis/whatis_json.asp) thanks to its conformance to the `Codable` protocol. 13 | 14 | `RosalindReport` creates a hierarchical tree structure where each node represents either a file or a directory, along with corresponding metadata. 15 | 16 | ### Attributes 17 | 18 | Each node in the tree includes these attributes: 19 | 20 | - **artifactType:** Categorizes the artifact as an `app`, `directory`, or `file`. 21 | - **path:** Specifies the artifact's path relative to the project root. 22 | - **size:** Records the artifact's size in bytes. 23 | - **shasum:** Provides a SHA-256 checksum of the artifact for integrity verification. 24 | - **children:** Contains an array of child artifacts (present only if the node is a directory). 25 | 26 | > [!NOTE] 27 | > In future updates, we plan to expand the `artifactType` enumeration to include more specific categories that better describe various artifacts. 28 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Why Rosalind?" 3 | titleTemplate: ':title | Quick start | Rosalind | Tuist' 4 | description: "Rosalind is a tool that helps you understand the size of your Xcode bundles and how to reduce it." 5 | --- 6 | 7 | # Why Rosalind? 8 | 9 | Understanding and optimizing applications requires standardized, detailed reports that reveal an application's internal architecture. Recognizing this universal challenge across development teams, we've open-sourced Rosalind under the MIT license to make this sophisticated analysis accessible to everyone. 10 | 11 | `Rosalind` is actively maintained by our dedicated team, ensuring it stays current with evolving development practices and toolchains across the mobile ecosystem. 12 | 13 | ## Schema 14 | 15 | Unlocking a layer of solutions that can help improve the quality and efficiency of mobile applications requires a **standard and open schema** that releases changes in a backward-compatible way. This standardization gives teams and organizations the freedom to migrate between tools with minimal effort and disruption. 16 | 17 | Rosalind's [schema](/api/schema) follows these principles and is fully documented, providing a foundation for building powerful tools and workflows to analyze and optimize applications. 18 | 19 | Whether you're working in a small team or a large organization, Rosalind's schema provides the flexibility and reliability needed to understand the deep structure of your applications. 20 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tuist/rosalind", 3 | "devDependencies": { 4 | "vitepress": "^1.3.2", 5 | "wrangler": "^4.0.0", 6 | "esbuild": "0.25.5", 7 | "cookie": ">=0.7.0" 8 | }, 9 | "scripts": { 10 | "dev": "vitepress dev", 11 | "build": "vitepress build", 12 | "preview": "vitepress preview", 13 | "deploy": "vitepress build && wrangler pages deploy .vitepress/dist --project-name tuist-rosalind --branch main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/docs/public/logo.png -------------------------------------------------------------------------------- /docs/quick-start/add-dependency.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Add dependency" 3 | titleTemplate: ':title | Quick start | Rosalind | Tuist' 4 | description: "Learn how to add Rosalind to your project." 5 | --- 6 | 7 | # Add dependency 8 | 9 | The first step is to add Rosalind as a dependency to your project. The method you choose depends on the type of project you have. 10 | 11 | ### Swift Package Manager 12 | 13 | You can edit your project's `Package.swift` and add `Command` as a dependency: 14 | 15 | ```swift 16 | import PackageDescription 17 | 18 | let package = Package( 19 | name: "MyProject", 20 | dependencies: [ 21 | .package(url: "https://github.com/tuist/Rosalind.git", .upToNextMajor(from: "0.1.0")) // [!code ++] 22 | ], 23 | targets: [ 24 | .target(name: "MyProject", 25 | dependencies: ["Rosalind", .product(name: "Rosalind", package: "Rosalind")]), // [!code ++] 26 | ] 27 | ) 28 | ``` 29 | 30 | ### Tuist 31 | 32 | First, you'll have to add the `Rosalind` package to your project's `Package.swift` file: 33 | 34 | ```swift 35 | import PackageDescription 36 | 37 | let package = Package( 38 | name: "MyProject", 39 | dependencies: [ 40 | .package(url: "https://github.com/tuist/Rosalind.git", .upToNextMajor(from: "0.1.0")) // [!code ++] 41 | ] 42 | ) 43 | ``` 44 | 45 | And then declare it as a dependency of one of your project's targets: 46 | 47 | ::: code-group 48 | ```swift [Project.swift] 49 | import ProjectDescription 50 | 51 | let project = Project( 52 | name: "App", 53 | organizationName: "tuist.io", 54 | targets: [ 55 | .target( 56 | name: "App", 57 | destinations: [.iPhone], 58 | product: .app, 59 | bundleId: "io.tuist.app", 60 | deploymentTargets: .iOS("13.0"), 61 | infoPlist: .default, 62 | sources: ["Targets/App/Sources/**"], 63 | dependencies: [ 64 | .external(name: "Rosalind"), // [!code ++] 65 | ] 66 | ), 67 | ] 68 | ) 69 | ``` 70 | ::: 71 | 72 | Make sure you run `tuist install` to fetch the dependencies before you generate the Xcode project. 73 | -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/App: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.app/App -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/App.debug.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.app/App.debug.dylib -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/Assets.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.app/Assets.car -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.app/Info.plist -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | App.debug.dylib 8 | 9 | rdJBFzW0lAeZ6Hi1SPCjWMgKZfg= 10 | 11 | Assets.car 12 | 13 | y4yCYzPATMC06C4hf7A3KewSVi4= 14 | 15 | Info.plist 16 | 17 | ERHu2xM3WDwWtkA8S3+T0agN/rY= 18 | 19 | PkgInfo 20 | 21 | n57qDP4tZfLD1rCS43W0B4LQjzE= 22 | 23 | __preview.dylib 24 | 25 | 6n4QPxM7TZU9hogf/4x6nQGUYaY= 26 | 27 | embedded.mobileprovision 28 | 29 | /wTiKJtdECfEQyPKNmCqg415FBc= 30 | 31 | 32 | files2 33 | 34 | App.debug.dylib 35 | 36 | hash2 37 | 38 | Wh9+TnzpHCOgJNh3bpXobArXHUjfjP8jkiD3WJ+J1J0= 39 | 40 | 41 | Assets.car 42 | 43 | hash2 44 | 45 | m6tiVvKEK74eVRFuPclaW+fIy/Z56p1ZPIce47qX1Ss= 46 | 47 | 48 | __preview.dylib 49 | 50 | hash2 51 | 52 | iqPxDVKwWWtd4maLPTYgN47EGxpRz3Mv5gaV3LueLC8= 53 | 54 | 55 | embedded.mobileprovision 56 | 57 | hash2 58 | 59 | wfm/YG9UgQViX1fh6QEfR9XzPSMJow97UKOP6BUiZA8= 60 | 61 | 62 | 63 | rules 64 | 65 | ^.* 66 | 67 | ^.*\.lproj/ 68 | 69 | optional 70 | 71 | weight 72 | 1000 73 | 74 | ^.*\.lproj/locversion.plist$ 75 | 76 | omit 77 | 78 | weight 79 | 1100 80 | 81 | ^Base\.lproj/ 82 | 83 | weight 84 | 1010 85 | 86 | ^version.plist$ 87 | 88 | 89 | rules2 90 | 91 | .*\.dSYM($|/) 92 | 93 | weight 94 | 11 95 | 96 | ^(.*/)?\.DS_Store$ 97 | 98 | omit 99 | 100 | weight 101 | 2000 102 | 103 | ^.* 104 | 105 | ^.*\.lproj/ 106 | 107 | optional 108 | 109 | weight 110 | 1000 111 | 112 | ^.*\.lproj/locversion.plist$ 113 | 114 | omit 115 | 116 | weight 117 | 1100 118 | 119 | ^Base\.lproj/ 120 | 121 | weight 122 | 1010 123 | 124 | ^Info\.plist$ 125 | 126 | omit 127 | 128 | weight 129 | 20 130 | 131 | ^PkgInfo$ 132 | 133 | omit 134 | 135 | weight 136 | 20 137 | 138 | ^embedded\.provisionprofile$ 139 | 140 | weight 141 | 20 142 | 143 | ^version\.plist$ 144 | 145 | weight 146 | 20 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/__preview.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.app/__preview.dylib -------------------------------------------------------------------------------- /fixtures/ios_app/App.app/embedded.mobileprovision: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.app/embedded.mobileprovision -------------------------------------------------------------------------------- /fixtures/ios_app/App.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.ipa -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ApplicationProperties 6 | 7 | ApplicationPath 8 | Applications/App.app 9 | Architectures 10 | 11 | arm64 12 | 13 | CFBundleIdentifier 14 | dev.tuist.rosalind.App 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | SigningIdentity 20 | Apple Development: Marek Fort (K5Q5B5S4HJ) 21 | Team 22 | U6LC622NKF 23 | 24 | ArchiveVersion 25 | 2 26 | CreationDate 27 | 2025-04-25T10:49:15Z 28 | Name 29 | App 30 | SchemeName 31 | App 32 | 33 | 34 | -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Products/Applications/App.app/App: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.xcarchive/Products/Applications/App.app/App -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Products/Applications/App.app/Assets.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.xcarchive/Products/Applications/App.app/Assets.car -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Products/Applications/App.app/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.xcarchive/Products/Applications/App.app/Info.plist -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Products/Applications/App.app/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Products/Applications/App.app/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Assets.car 8 | 9 | y4yCYzPATMC06C4hf7A3KewSVi4= 10 | 11 | Info.plist 12 | 13 | ERHu2xM3WDwWtkA8S3+T0agN/rY= 14 | 15 | PkgInfo 16 | 17 | n57qDP4tZfLD1rCS43W0B4LQjzE= 18 | 19 | embedded.mobileprovision 20 | 21 | /wTiKJtdECfEQyPKNmCqg415FBc= 22 | 23 | 24 | files2 25 | 26 | Assets.car 27 | 28 | hash2 29 | 30 | m6tiVvKEK74eVRFuPclaW+fIy/Z56p1ZPIce47qX1Ss= 31 | 32 | 33 | embedded.mobileprovision 34 | 35 | hash2 36 | 37 | wfm/YG9UgQViX1fh6QEfR9XzPSMJow97UKOP6BUiZA8= 38 | 39 | 40 | 41 | rules 42 | 43 | ^.* 44 | 45 | ^.*\.lproj/ 46 | 47 | optional 48 | 49 | weight 50 | 1000 51 | 52 | ^.*\.lproj/locversion.plist$ 53 | 54 | omit 55 | 56 | weight 57 | 1100 58 | 59 | ^Base\.lproj/ 60 | 61 | weight 62 | 1010 63 | 64 | ^version.plist$ 65 | 66 | 67 | rules2 68 | 69 | .*\.dSYM($|/) 70 | 71 | weight 72 | 11 73 | 74 | ^(.*/)?\.DS_Store$ 75 | 76 | omit 77 | 78 | weight 79 | 2000 80 | 81 | ^.* 82 | 83 | ^.*\.lproj/ 84 | 85 | optional 86 | 87 | weight 88 | 1000 89 | 90 | ^.*\.lproj/locversion.plist$ 91 | 92 | omit 93 | 94 | weight 95 | 1100 96 | 97 | ^Base\.lproj/ 98 | 99 | weight 100 | 1010 101 | 102 | ^Info\.plist$ 103 | 104 | omit 105 | 106 | weight 107 | 20 108 | 109 | ^PkgInfo$ 110 | 111 | omit 112 | 113 | weight 114 | 20 115 | 116 | ^embedded\.provisionprofile$ 117 | 118 | weight 119 | 20 120 | 121 | ^version\.plist$ 122 | 123 | weight 124 | 20 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcarchive/Products/Applications/App.app/embedded.mobileprovision: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App.xcarchive/Products/Applications/App.app/embedded.mobileprovision -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 6C32E67D2D7772ED00949239 /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 | 6C32E67F2D7772ED00949239 /* App */ = { 15 | isa = PBXFileSystemSynchronizedRootGroup; 16 | path = App; 17 | sourceTree = ""; 18 | }; 19 | /* End PBXFileSystemSynchronizedRootGroup section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 6C32E67A2D7772ED00949239 /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 6C32E6742D7772ED00949239 = { 33 | isa = PBXGroup; 34 | children = ( 35 | 6C32E67F2D7772ED00949239 /* App */, 36 | 6C32E67E2D7772ED00949239 /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | 6C32E67E2D7772ED00949239 /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 6C32E67D2D7772ED00949239 /* App.app */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | /* End PBXGroup section */ 49 | 50 | /* Begin PBXNativeTarget section */ 51 | 6C32E67C2D7772ED00949239 /* App */ = { 52 | isa = PBXNativeTarget; 53 | buildConfigurationList = 6C32E68B2D7772EF00949239 /* Build configuration list for PBXNativeTarget "App" */; 54 | buildPhases = ( 55 | 6C32E6792D7772ED00949239 /* Sources */, 56 | 6C32E67A2D7772ED00949239 /* Frameworks */, 57 | 6C32E67B2D7772ED00949239 /* Resources */, 58 | ); 59 | buildRules = ( 60 | ); 61 | dependencies = ( 62 | ); 63 | fileSystemSynchronizedGroups = ( 64 | 6C32E67F2D7772ED00949239 /* App */, 65 | ); 66 | name = App; 67 | packageProductDependencies = ( 68 | ); 69 | productName = App; 70 | productReference = 6C32E67D2D7772ED00949239 /* App.app */; 71 | productType = "com.apple.product-type.application"; 72 | }; 73 | /* End PBXNativeTarget section */ 74 | 75 | /* Begin PBXProject section */ 76 | 6C32E6752D7772ED00949239 /* Project object */ = { 77 | isa = PBXProject; 78 | attributes = { 79 | BuildIndependentTargetsInParallel = 1; 80 | LastSwiftUpdateCheck = 1620; 81 | LastUpgradeCheck = 1620; 82 | TargetAttributes = { 83 | 6C32E67C2D7772ED00949239 = { 84 | CreatedOnToolsVersion = 16.2; 85 | }; 86 | }; 87 | }; 88 | buildConfigurationList = 6C32E6782D7772ED00949239 /* Build configuration list for PBXProject "App" */; 89 | developmentRegion = en; 90 | hasScannedForEncodings = 0; 91 | knownRegions = ( 92 | en, 93 | Base, 94 | ); 95 | mainGroup = 6C32E6742D7772ED00949239; 96 | minimizedProjectReferenceProxies = 1; 97 | preferredProjectObjectVersion = 77; 98 | productRefGroup = 6C32E67E2D7772ED00949239 /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | 6C32E67C2D7772ED00949239 /* App */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | 6C32E67B2D7772ED00949239 /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXResourcesBuildPhase section */ 116 | 117 | /* Begin PBXSourcesBuildPhase section */ 118 | 6C32E6792D7772ED00949239 /* Sources */ = { 119 | isa = PBXSourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXSourcesBuildPhase section */ 126 | 127 | /* Begin XCBuildConfiguration section */ 128 | 6C32E6892D7772EF00949239 /* Debug */ = { 129 | isa = XCBuildConfiguration; 130 | buildSettings = { 131 | ALWAYS_SEARCH_USER_PATHS = NO; 132 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 133 | CLANG_ANALYZER_NONNULL = YES; 134 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 135 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_ENABLE_OBJC_WEAK = YES; 139 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 140 | CLANG_WARN_BOOL_CONVERSION = YES; 141 | CLANG_WARN_COMMA = YES; 142 | CLANG_WARN_CONSTANT_CONVERSION = YES; 143 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 144 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 146 | CLANG_WARN_EMPTY_BODY = YES; 147 | CLANG_WARN_ENUM_CONVERSION = YES; 148 | CLANG_WARN_INFINITE_RECURSION = YES; 149 | CLANG_WARN_INT_CONVERSION = YES; 150 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 151 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 152 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 155 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 156 | CLANG_WARN_STRICT_PROTOTYPES = YES; 157 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 158 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | DEBUG_INFORMATION_FORMAT = dwarf; 163 | ENABLE_STRICT_OBJC_MSGSEND = YES; 164 | ENABLE_TESTABILITY = YES; 165 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 166 | GCC_C_LANGUAGE_STANDARD = gnu17; 167 | GCC_DYNAMIC_NO_PIC = NO; 168 | GCC_NO_COMMON_BLOCKS = YES; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 175 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 176 | GCC_WARN_UNDECLARED_SELECTOR = YES; 177 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 178 | GCC_WARN_UNUSED_FUNCTION = YES; 179 | GCC_WARN_UNUSED_VARIABLE = YES; 180 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 181 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 182 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 183 | MTL_FAST_MATH = YES; 184 | ONLY_ACTIVE_ARCH = YES; 185 | SDKROOT = iphoneos; 186 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 187 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 188 | }; 189 | name = Debug; 190 | }; 191 | 6C32E68A2D7772EF00949239 /* Release */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | ALWAYS_SEARCH_USER_PATHS = NO; 195 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 196 | CLANG_ANALYZER_NONNULL = YES; 197 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 199 | CLANG_ENABLE_MODULES = YES; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_ENABLE_OBJC_WEAK = YES; 202 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 203 | CLANG_WARN_BOOL_CONVERSION = YES; 204 | CLANG_WARN_COMMA = YES; 205 | CLANG_WARN_CONSTANT_CONVERSION = YES; 206 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 209 | CLANG_WARN_EMPTY_BODY = YES; 210 | CLANG_WARN_ENUM_CONVERSION = YES; 211 | CLANG_WARN_INFINITE_RECURSION = YES; 212 | CLANG_WARN_INT_CONVERSION = YES; 213 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 214 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 215 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 217 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 218 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 219 | CLANG_WARN_STRICT_PROTOTYPES = YES; 220 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 221 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | COPY_PHASE_STRIP = NO; 225 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 226 | ENABLE_NS_ASSERTIONS = NO; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu17; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 238 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 239 | MTL_ENABLE_DEBUG_INFO = NO; 240 | MTL_FAST_MATH = YES; 241 | SDKROOT = iphoneos; 242 | SWIFT_COMPILATION_MODE = wholemodule; 243 | VALIDATE_PRODUCT = YES; 244 | }; 245 | name = Release; 246 | }; 247 | 6C32E68C2D7772EF00949239 /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 251 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 252 | CODE_SIGN_STYLE = Automatic; 253 | CURRENT_PROJECT_VERSION = 1; 254 | DEVELOPMENT_ASSET_PATHS = "\"App/Preview Content\""; 255 | DEVELOPMENT_TEAM = U6LC622NKF; 256 | ENABLE_PREVIEWS = YES; 257 | GENERATE_INFOPLIST_FILE = YES; 258 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 259 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 260 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 261 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 262 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 263 | LD_RUNPATH_SEARCH_PATHS = ( 264 | "$(inherited)", 265 | "@executable_path/Frameworks", 266 | ); 267 | MARKETING_VERSION = 1.0; 268 | PRODUCT_BUNDLE_IDENTIFIER = dev.tuist.rosalind.App; 269 | PRODUCT_NAME = "$(TARGET_NAME)"; 270 | SWIFT_EMIT_LOC_STRINGS = YES; 271 | SWIFT_VERSION = 5.0; 272 | TARGETED_DEVICE_FAMILY = "1,2"; 273 | }; 274 | name = Debug; 275 | }; 276 | 6C32E68D2D7772EF00949239 /* Release */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 280 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 281 | CODE_SIGN_STYLE = Automatic; 282 | CURRENT_PROJECT_VERSION = 1; 283 | DEVELOPMENT_ASSET_PATHS = "\"App/Preview Content\""; 284 | DEVELOPMENT_TEAM = U6LC622NKF; 285 | ENABLE_PREVIEWS = YES; 286 | GENERATE_INFOPLIST_FILE = YES; 287 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 288 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 289 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 290 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 291 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 292 | LD_RUNPATH_SEARCH_PATHS = ( 293 | "$(inherited)", 294 | "@executable_path/Frameworks", 295 | ); 296 | MARKETING_VERSION = 1.0; 297 | PRODUCT_BUNDLE_IDENTIFIER = dev.tuist.rosalind.App; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | SWIFT_EMIT_LOC_STRINGS = YES; 300 | SWIFT_VERSION = 5.0; 301 | TARGETED_DEVICE_FAMILY = "1,2"; 302 | }; 303 | name = Release; 304 | }; 305 | /* End XCBuildConfiguration section */ 306 | 307 | /* Begin XCConfigurationList section */ 308 | 6C32E6782D7772ED00949239 /* Build configuration list for PBXProject "App" */ = { 309 | isa = XCConfigurationList; 310 | buildConfigurations = ( 311 | 6C32E6892D7772EF00949239 /* Debug */, 312 | 6C32E68A2D7772EF00949239 /* Release */, 313 | ); 314 | defaultConfigurationIsVisible = 0; 315 | defaultConfigurationName = Release; 316 | }; 317 | 6C32E68B2D7772EF00949239 /* Build configuration list for PBXNativeTarget "App" */ = { 318 | isa = XCConfigurationList; 319 | buildConfigurations = ( 320 | 6C32E68C2D7772EF00949239 /* Debug */, 321 | 6C32E68D2D7772EF00949239 /* Release */, 322 | ); 323 | defaultConfigurationIsVisible = 0; 324 | defaultConfigurationName = Release; 325 | }; 326 | /* End XCConfigurationList section */ 327 | }; 328 | rootObject = 6C32E6752D7772ED00949239 /* Project object */; 329 | } 330 | -------------------------------------------------------------------------------- /fixtures/ios_app/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/AppApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct AppApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Tuist-Filled.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Tuist-Filled@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Tuist-Filled@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/Image.imageset/Tuist-Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App/Assets.xcassets/Image.imageset/Tuist-Filled.png -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/Image.imageset/Tuist-Filled@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App/Assets.xcassets/Image.imageset/Tuist-Filled@2x.png -------------------------------------------------------------------------------- /fixtures/ios_app/App/Assets.xcassets/Image.imageset/Tuist-Filled@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuist/Rosalind/632f289e28c434469facd7cc0c81878adaa2ad69/fixtures/ios_app/App/Assets.xcassets/Image.imageset/Tuist-Filled@3x.png -------------------------------------------------------------------------------- /fixtures/ios_app/App/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | VStack { 6 | Image(systemName: "globe") 7 | .imageScale(.large) 8 | .foregroundStyle(.tint) 9 | Text("Hello, world!") 10 | } 11 | .padding() 12 | } 13 | } 14 | 15 | #Preview { 16 | ContentView() 17 | } 18 | -------------------------------------------------------------------------------- /fixtures/ios_app/App/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | "git-cliff" = "2.4.0" 3 | "pnpm" = "10.11.0" 4 | "nodejs" = "22.16.0" 5 | "swiftlint" = "0.54.0" 6 | "swiftformat" = "0.52.10" 7 | -------------------------------------------------------------------------------- /mise/tasks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Build the project using Swift Package Manager" 3 | set -euo pipefail 4 | 5 | swift build --product Rosalind --package-path $MISE_PROJECT_ROOT --configuration release 6 | -------------------------------------------------------------------------------- /mise/tasks/build-fixture.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # mise description="Bundles the CLI for distribution" 3 | #USAGE arg "" help="The fixture to build" 4 | 5 | set -euo pipefail 6 | 7 | PROJECT_PATH="$MISE_PROJECT_ROOT/fixtures/$usage_fixture" 8 | rm -rf "$PROJECT_PATH/App.app" "$PROJECT_PATH/App.xcarchive" "$PROJECT_PATH/App.ipa" 9 | 10 | xcodebuild build -scheme App -destination 'generic/platform=iOS' -project "$PROJECT_PATH/App.xcodeproj" -derivedDataPath "$PROJECT_PATH/.build" 11 | 12 | mv "$PROJECT_PATH/.build/Build/Products/Debug-iphoneos/App.app" "$PROJECT_PATH/App.app" 13 | 14 | xcodebuild archive -project "$PROJECT_PATH/App.xcodeproj" -scheme App -sdk iphoneos -destination "generic/platform=iOS" -archivePath "$PROJECT_PATH/App.xcarchive" 15 | 16 | xcodebuild -exportArchive -archivePath "$PROJECT_PATH/App.xcarchive" -exportOptionsPlist "$PROJECT_PATH/App.xcarchive/Info.plist" -exportPath "$PROJECT_PATH/.build/ExportedApp" 17 | mv "$PROJECT_PATH/.build/ExportedApp/App.ipa" "$PROJECT_PATH/App.ipa" 18 | rm -rf "$PROJECT_PATH/.build" 19 | -------------------------------------------------------------------------------- /mise/tasks/build-linux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Builds the project using Swift Package Manager in Linux" 3 | set -euo pipefail 4 | 5 | docker run --rm \ 6 | --volume "$MISE_PROJECT_ROOT:/package" \ 7 | --workdir "/package" \ 8 | swiftlang/swift:nightly-6.0-focal \ 9 | /bin/bash -c \ 10 | "swift build --product Rosalind --build-path ./.build/linux" 11 | -------------------------------------------------------------------------------- /mise/tasks/cache: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Cache the dependencies using Tuist" 3 | set -euo pipefail 4 | 5 | tuist cache --path $MISE_PROJECT_ROOT -------------------------------------------------------------------------------- /mise/tasks/docs/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | pnpm run -C $MISE_PROJECT_ROOT/docs build -------------------------------------------------------------------------------- /mise/tasks/docs/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | pnpm run -C $MISE_PROJECT_ROOT/docs deploy -------------------------------------------------------------------------------- /mise/tasks/docs/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | pnpm run -C $MISE_PROJECT_ROOT/docs dev -------------------------------------------------------------------------------- /mise/tasks/docs/preview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | pnpm run -C $MISE_PROJECT_ROOT/docs preview -------------------------------------------------------------------------------- /mise/tasks/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Performs additional tasks that are necessary to work in this repository" 3 | 4 | set -euo pipefail 5 | 6 | pnpm install -C $MISE_PROJECT_ROOT/docs/ 7 | if [[ "$(uname)" == "Darwin" ]]; then 8 | tuist install --path $MISE_PROJECT_ROOT 9 | fi -------------------------------------------------------------------------------- /mise/tasks/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Lint the project using SwiftLint and SwiftFormat" 3 | #USAGE flag "-f --fix" help="Fix the fixable issues" 4 | set -eo pipefail 5 | 6 | if [ "$usage_fix" = "true" ]; then 7 | swiftformat $MISE_PROJECT_ROOT 8 | swiftlint lint --fix --quiet --config $MISE_PROJECT_ROOT/.swiftlint.yml $MISE_PROJECT_ROOT/Sources 9 | else 10 | swiftformat $MISE_PROJECT_ROOT --lint 11 | swiftlint lint --quiet --config $MISE_PROJECT_ROOT/.swiftlint.yml $MISE_PROJECT_ROOT/Sources 12 | fi 13 | -------------------------------------------------------------------------------- /mise/tasks/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Test the project using Swift Package Manager" 3 | 4 | set -euo pipefail 5 | 6 | swift test --package-path $MISE_PROJECT_ROOT -------------------------------------------------------------------------------- /mise/tasks/test-linux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mise description="Builds the project using Swift Package Manager in Linux" 3 | set -euo pipefail 4 | 5 | docker run --rm \ 6 | --volume "$MISE_PROJECT_ROOT:/package" \ 7 | --workdir "/package" \ 8 | swiftlang/swift:nightly-6.0-focal \ 9 | /bin/bash -c \ 10 | "swift test --build-path ./.build/linux" 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "docs" -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":semanticCommits", 5 | ":disableDependencyDashboard", 6 | "config:recommended" 7 | ], 8 | "packageRules": [ 9 | { 10 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 11 | "automerge": true, 12 | "automergeType": "pr", 13 | "automergeStrategy": "auto" 14 | } 15 | ], 16 | "lockFileMaintenance": { 17 | "enabled": true, 18 | "automerge": true 19 | } 20 | } --------------------------------------------------------------------------------