├── .github
    ├── ISSUE_TEMPLATE
    │   ├── 1-bug-report.yml
    │   └── 2-feature-request.yml
    └── workflows
    │   ├── deploy.yml
    │   ├── milestone-assignment.yml
    │   ├── milestone-creation.yml
    │   └── weekly-auto-deploy.yml
├── .gitignore
├── .gitmodules
├── .pre-commit-config.yaml
├── .rubocop.yml
├── FUNDING.yml
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── Makefile
├── README.md
├── VERSION
├── bin
    └── xccache
├── docs
    ├── README.md
    ├── case-study-kickstarter.md
    ├── configuration.md
    ├── contributing-guidelines.md
    ├── ex-viz
    │   ├── assets
    │   │   ├── cachemap.js
    │   │   └── style.css
    │   └── cachemap.html
    ├── features-roadmap.md
    ├── getting-started.md
    ├── how-to-install.md
    ├── overview.md
    ├── res
    │   ├── binary_macro.png
    │   ├── cachemap.png
    │   ├── kickstarter_clean_build.png
    │   ├── lockfile_add_new_dep.png
    │   ├── proxy_binary.png
    │   ├── proxy_local.png
    │   ├── resource_build_structure.png
    │   ├── resource_bundle_module.png
    │   ├── resource_bundle_overriding.png
    │   ├── switching_binary_to_source.gif
    │   ├── switching_binary_to_source.mp4
    │   ├── umbrella_product_dependencies.png
    │   ├── unknown_deps_err.png
    │   ├── xccache.png
    │   └── xcode_process_xcframeworks.png
    ├── troubleshooting.md
    └── under-the-hood
    │   ├── ensuring-bundle-module.md
    │   ├── macro-as-binary.md
    │   ├── packaging-as-xcframework.md
    │   └── proxy-packages.md
├── examples
    ├── .gitignore
    ├── EX.xcodeproj
    │   ├── project.pbxproj
    │   └── xcshareddata
    │   │   └── xcschemes
    │   │       └── EX.xcscheme
    ├── EX.xctestplan
    ├── EX
    │   ├── Assets.xcassets
    │   │   ├── AccentColor.colorset
    │   │   │   └── Contents.json
    │   │   ├── AppIcon.appiconset
    │   │   │   └── Contents.json
    │   │   └── Contents.json
    │   ├── ContentView.swift
    │   ├── EX-Bridging-Header.h
    │   ├── EXApp.swift
    │   ├── Extensions.swift
    │   ├── Preview Content
    │   │   └── Preview Assets.xcassets
    │   │   │   └── Contents.json
    │   ├── SPMPlayground.swift
    │   ├── SPMPlayground_ObjC.h
    │   └── SPMPlayground_ObjC.m
    ├── EXMac
    │   ├── Assets.xcassets
    │   │   ├── AccentColor.colorset
    │   │   │   └── Contents.json
    │   │   ├── AppIcon.appiconset
    │   │   │   └── Contents.json
    │   │   └── Contents.json
    │   ├── ContentView.swift
    │   ├── EXMac.entitlements
    │   └── EXMacApp.swift
    ├── EXTests
    │   ├── ResourceTests.swift
    │   └── SPMPlayground.swift
    ├── LocalPackages
    │   ├── core-utils
    │   │   ├── Package.swift
    │   │   └── Sources
    │   │   │   ├── Core
    │   │   │       └── dummy.swift
    │   │   │   ├── DebugKitObjc
    │   │   │       ├── DebugKit.m
    │   │   │       ├── Diagnose.m
    │   │   │       ├── Headers
    │   │   │       │   └── DebugKit.h
    │   │   │       ├── PrivateHeaders
    │   │   │       │   └── Diagnose.h
    │   │   │       └── token.txt
    │   │   │   ├── DisplayKit
    │   │   │       └── DisplayKit.swift
    │   │   │   ├── ResourceKit
    │   │   │       ├── ResourceKit.swift
    │   │   │       └── greetings.txt
    │   │   │   ├── Swizzler
    │   │   │       ├── Swizzler.m
    │   │   │       └── include
    │   │   │       │   └── Swizzler.h
    │   │   │   └── TestKit
    │   │   │       └── TestKit.swift
    │   └── wizard
    │   │   ├── Package.swift
    │   │   └── Sources
    │   │       ├── Wizard
    │   │           └── Wizard.swift
    │   │       ├── WizardImpl
    │   │           ├── HexColorMacro.swift
    │   │           └── WizardImpl.swift
    │   │       └── WizardPlayground
    │   │           └── main.swift
    ├── Makefile
    ├── config.xcconfig
    ├── xccache.lock
    └── xccache.yml
├── lib
    ├── xccache.rb
    └── xccache
    │   ├── assets
    │       └── templates
    │       │   ├── cachemap.html.template
    │       │   ├── cachemap.js.template
    │       │   ├── cachemap.style.css.template
    │       │   ├── framework.info.plist.template
    │       │   ├── framework.modulemap.template
    │       │   ├── resource_bundle_accessor.m.template
    │       │   ├── resource_bundle_accessor.swift.template
    │       │   └── xccache.yml.template
    │   ├── cache
    │       └── cachemap.rb
    │   ├── command.rb
    │   ├── command
    │       ├── base.rb
    │       ├── build.rb
    │       ├── cache.rb
    │       ├── cache
    │       │   ├── clean.rb
    │       │   └── list.rb
    │       ├── off.rb
    │       ├── pkg.rb
    │       ├── pkg
    │       │   └── build.rb
    │       ├── remote.rb
    │       ├── remote
    │       │   ├── pull.rb
    │       │   └── push.rb
    │       ├── rollback.rb
    │       └── use.rb
    │   ├── core.rb
    │   ├── core
    │       ├── cacheable.rb
    │       ├── config.rb
    │       ├── error.rb
    │       ├── git.rb
    │       ├── hash.rb
    │       ├── live_log.rb
    │       ├── lockfile.rb
    │       ├── log.rb
    │       ├── parallel.rb
    │       ├── sh.rb
    │       ├── syntax.rb
    │       ├── syntax
    │       │   ├── hash.rb
    │       │   ├── json.rb
    │       │   ├── plist.rb
    │       │   └── yml.rb
    │       └── system.rb
    │   ├── installer.rb
    │   ├── installer
    │       ├── build.rb
    │       ├── integration.rb
    │       ├── integration
    │       │   ├── build.rb
    │       │   ├── descs.rb
    │       │   ├── supporting_files.rb
    │       │   └── viz.rb
    │       ├── rollback.rb
    │       └── use.rb
    │   ├── main.rb
    │   ├── spm.rb
    │   ├── spm
    │       ├── build.rb
    │       ├── desc.rb
    │       ├── desc
    │       │   ├── base.rb
    │       │   ├── dep.rb
    │       │   ├── desc.rb
    │       │   ├── product.rb
    │       │   ├── target.rb
    │       │   └── target
    │       │   │   ├── binary.rb
    │       │   │   └── macro.rb
    │       ├── macro.rb
    │       ├── mixin.rb
    │       ├── pkg.rb
    │       ├── pkg
    │       │   ├── base.rb
    │       │   ├── proxy.rb
    │       │   └── proxy_executable.rb
    │       ├── xcframework.rb
    │       └── xcframework
    │       │   ├── metadata.rb
    │       │   ├── slice.rb
    │       │   └── xcframework.rb
    │   ├── storage.rb
    │   ├── storage
    │       ├── base.rb
    │       ├── git.rb
    │       └── s3.rb
    │   ├── swift
    │       ├── sdk.rb
    │       └── swiftc.rb
    │   ├── utils
    │       └── template.rb
    │   ├── xcodeproj.rb
    │   └── xcodeproj
    │       ├── build_configuration.rb
    │       ├── config.rb
    │       ├── file_system_synchronized_root_group.rb
    │       ├── group.rb
    │       ├── pkg.rb
    │       ├── pkg_product_dependency.rb
    │       ├── project.rb
    │       └── target.rb
└── xccache.gemspec
/.github/ISSUE_TEMPLATE/1-bug-report.yml:
--------------------------------------------------------------------------------
 1 | name: Bug Report
 2 | description: File a bug report (if something is not working properly)
 3 | title: "[Bug] 
"
 4 | labels: ["bug"]
 5 | body:
 6 |   - type: markdown
 7 |     attributes:
 8 |       value: |
 9 |         Thanks for taking the time to fill out this form!
10 | 
11 |         Make sure you've searched [existing issues](https://github.com/trinhngocthuyen/xccache/issues) for the bug you encountered.
12 |   - type: textarea
13 |     attributes:
14 |       label: What happened?
15 |       description: Also tell us, what did you expect to happen?
16 |       placeholder: Tell us what you see! Remember to run `xccache` with `--verbose` for more useful logs
17 |     validations:
18 |       required: true
19 |   - type: textarea
20 |     attributes:
21 |       label: Anything else?
22 |       description: |
23 |         Links? References? Anything that will give us more context about the issue you are encountering!
24 | 
25 |         💡 Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
26 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2-feature-request.yml:
--------------------------------------------------------------------------------
 1 | name: Feature Request
 2 | description: Request a feature (if something can be improved, to make the plugin better)
 3 | title: "[Feature Request] "
 4 | labels: ["feature-request"]
 5 | body:
 6 |   - type: textarea
 7 |     attributes:
 8 |       label: Motivation
 9 |       description: Share with us why we should have this change. What does it benefit the community?
10 |       placeholder: Tell us what you think!
11 |     validations:
12 |       required: true
13 |   - type: textarea
14 |     attributes:
15 |       label: Summary
16 |       description: Share with us what are the expected behaviors with this request.
17 |       placeholder: Tell us what you think!
18 |     validations:
19 |       required: true
20 | 
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
 1 | name: deploy
 2 | on:
 3 |   workflow_dispatch:
 4 |     inputs:
 5 |       deploy-rc:
 6 |         description: Deploy a RC version
 7 |         type: boolean
 8 |       deploy-stable:
 9 |         description: Deploy a stable version
10 |         type: boolean
11 | 
12 | jobs:
13 |   deploy:
14 |     uses: trinhngocthuyen/gh-actions/.github/workflows/rb-deploy.yml@main
15 |     with:
16 |       deploy-rc: ${{ inputs.deploy-rc }}
17 |       deploy-stable: ${{ inputs.deploy-stable }}
18 |     secrets: inherit
19 | 
--------------------------------------------------------------------------------
/.github/workflows/milestone-assignment.yml:
--------------------------------------------------------------------------------
 1 | name: milestone-assignment
 2 | on:
 3 |   pull_request:
 4 |     types: ["closed"]
 5 | 
 6 | jobs:
 7 |   milestone-assignment:
 8 |     if: github.event.pull_request.merged == true
 9 |     runs-on: ubuntu-latest
10 |     permissions:
11 |       contents: read
12 |       pull-requests: write
13 |       issues: write
14 |     steps:
15 |       - uses: actions/checkout@v4
16 |       - run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"
17 |         id: calc_version
18 |         shell: bash
19 |       - uses: trinhngocthuyen/gh-actions/gh/pr/assign_milestone@main
20 |         with:
21 |           github-token: ${{ secrets.GITHUB_TOKEN }}
22 |           title: ${{ steps.calc_version.outputs.version }}
23 | 
--------------------------------------------------------------------------------
/.github/workflows/milestone-creation.yml:
--------------------------------------------------------------------------------
 1 | name: milestone-creation
 2 | on:
 3 |   release:
 4 |     types: ["published"]
 5 | 
 6 | jobs:
 7 |   create-milestone:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |       - name: Checkout
11 |         uses: actions/checkout@v4
12 |       - name: Bump version
13 |         uses: trinhngocthuyen/gh-actions/core/bump_version@main
14 |         id: bump_version
15 |         with:
16 |           version-file: VERSION
17 |       - name: Create new milestone
18 |         uses: trinhngocthuyen/gh-actions/gh/milestone/create@main
19 |         with:
20 |           github-token: ${{ secrets.GH_TOKEN }}
21 |           title: ${{ steps.bump_version.outputs.version }}
22 |       - name: Close prev milestone
23 |         uses: trinhngocthuyen/gh-actions/gh/milestone/close@main
24 |         with:
25 |           github-token: ${{ secrets.GH_TOKEN }}
26 |           title: ${{ steps.bump_version.outputs.prev_version }}
27 | 
--------------------------------------------------------------------------------
/.github/workflows/weekly-auto-deploy.yml:
--------------------------------------------------------------------------------
 1 | name: weekly-auto-deploy
 2 | on:
 3 |   workflow_dispatch:
 4 |   schedule:
 5 |     - cron: '0 0 * * 0' # 12AM, Sunday (UTC)
 6 | 
 7 | jobs:
 8 |   check:
 9 |     runs-on: ubuntu-latest
10 |     steps:
11 |       - uses: actions/checkout@v4
12 |       - run: |
13 |           echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
14 |         shell: bash
15 |       - uses: actions/github-script@v7
16 |         env:
17 |           VERSION: ${{ env.VERSION }}
18 |         with:
19 |           github-token: ${{ secrets.GH_TOKEN }}
20 |           script: |
21 |             const { VERSION } = process.env;
22 |             const title = VERSION;
23 |             const milestones = await github.rest.issues.listMilestones({
24 |               owner: context.repo.owner,
25 |               repo: context.repo.repo,
26 |             });
27 |             const milestone = milestones.data.find(m => m.title === title);
28 |             if (!milestone) {
29 |               console.error(`Milestone '${title}' not found.`);
30 |               return;
31 |             }
32 |             console.log(`There are ${milestone.closed_issues} associated PRs/issues`);
33 |             if (milestone.closed_issues <= 0) {
34 |               console.log('No associated PRs/issues -> Do nothing');
35 |               return;
36 |             }
37 |             console.log('Will trigger deploy workflow...');
38 |             await github.rest.actions.createWorkflowDispatch({
39 |               owner: context.repo.owner,
40 |               repo: context.repo.repo,
41 |               workflow_id: ".github/workflows/deploy.yml",
42 |               ref: "main",
43 |               inputs: {"deploy-stable": true}
44 |             });
45 | 
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | *.gem
 2 | *.rbc
 3 | /.config
 4 | /coverage/
 5 | /InstalledFiles
 6 | /pkg/
 7 | /spec/reports/
 8 | /spec/examples.txt
 9 | /test/tmp/
10 | /test/version_tmp/
11 | /tmp/
12 | .env
13 | 
14 | # Used by dotenv library to load environment variables.
15 | # .env
16 | 
17 | # Ignore Byebug command history file.
18 | .byebug_history
19 | 
20 | ## Specific to RubyMotion:
21 | .dat*
22 | .repl_history
23 | build/
24 | *.bridgesupport
25 | build-iPhoneOS/
26 | build-iPhoneSimulator/
27 | 
28 | ## Specific to RubyMotion (use of CocoaPods):
29 | #
30 | # We recommend against adding the Pods directory to your .gitignore. However
31 | # you should judge for yourself, the pros and cons are mentioned at:
32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
33 | #
34 | # vendor/Pods/
35 | 
36 | ## Documentation cache and generated files:
37 | /.yardoc/
38 | /_yardoc/
39 | /doc/
40 | /rdoc/
41 | 
42 | ## Environment normalization:
43 | /.bundle/
44 | /vendor/bundle
45 | /lib/bundler/man/
46 | 
47 | # for a library or gem, you might want to ignore these files since the code is
48 | # intended to run in multiple environments; otherwise, check them in:
49 | # Gemfile.lock
50 | # .ruby-version
51 | # .ruby-gemset
52 | 
53 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
54 | .rvmrc
55 | 
56 | # Used by RuboCop. Remote config files pulled in from inherit_from directive.
57 | .rubocop-https?--*
58 | 
59 | .vscode/
60 | libexec/.local/
61 | libexec/.build/
62 | libexec/.download/
63 | 
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "tools/xccache-proxy"]
2 | 	path = tools/xccache-proxy
3 | 	url = git@github.com:trinhngocthuyen/xccache-proxy.git
4 | 
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
 1 | exclude: ^(VERSION|tools|.*\.xcframework|.*\.lock|.*\.json)
 2 | repos:
 3 |   - repo: https://github.com/pre-commit/pre-commit-hooks
 4 |     rev: v4.5.0
 5 |     hooks:
 6 |       - id: check-yaml
 7 |       - id: check-json
 8 |       - id: check-toml
 9 |       - id: check-xml
10 |       - id: end-of-file-fixer
11 |       - id: trailing-whitespace
12 |   - repo: https://github.com/rubocop/rubocop
13 |     rev: v1.75.2
14 |     hooks:
15 |       - id: rubocop
16 | 
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
 1 | inherit_from: https://raw.githubusercontent.com/trinhngocthuyen/templates/main/config/rubocop/default.yml
 2 | 
 3 | Style/ParallelAssignment:
 4 |   Enabled: false
 5 | Style/TrailingCommaInArguments:
 6 |   Enabled: false
 7 | 
 8 | Lint/EmptyClass:
 9 |   Enabled: false
10 | 
11 | Layout/EmptyLineAfterGuardClause:
12 |   Enabled: false
13 | 
14 | Naming/PredicateName:
15 |   Enabled: false
16 | Naming/MethodParameterName:
17 |   MinNameLength: 1
18 | 
--------------------------------------------------------------------------------
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | buy_me_a_coffee: trinhngocthuyen
2 | 
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
 1 | source "https://rubygems.org"
 2 | 
 3 | gemspec
 4 | 
 5 | group :development do
 6 |   gem "bundler", "> 1.3"
 7 |   gem "pry-nav"
 8 |   gem "rspec"
 9 |   gem "rubocop"
10 | end
11 | 
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
 1 | PATH
 2 |   remote: .
 3 |   specs:
 4 |     xccache (1.0.5)
 5 |       claide
 6 |       parallel
 7 |       tty-cursor
 8 |       tty-screen
 9 |       xcodeproj (>= 1.26.0)
10 | 
11 | GEM
12 |   remote: https://rubygems.org/
13 |   specs:
14 |     CFPropertyList (3.0.7)
15 |       base64
16 |       nkf
17 |       rexml
18 |     ast (2.4.3)
19 |     atomos (0.1.3)
20 |     base64 (0.2.0)
21 |     claide (1.1.0)
22 |     coderay (1.1.3)
23 |     colored2 (3.1.2)
24 |     diff-lcs (1.6.1)
25 |     json (2.10.2)
26 |     language_server-protocol (3.17.0.4)
27 |     lint_roller (1.1.0)
28 |     method_source (1.1.0)
29 |     nanaimo (0.4.0)
30 |     nkf (0.2.0)
31 |     parallel (1.26.3)
32 |     parser (3.3.7.3)
33 |       ast (~> 2.4.1)
34 |       racc
35 |     prism (1.4.0)
36 |     pry (0.14.2)
37 |       coderay (~> 1.1)
38 |       method_source (~> 1.0)
39 |     pry-nav (1.0.0)
40 |       pry (>= 0.9.10, < 0.15)
41 |     racc (1.8.1)
42 |     rainbow (3.1.1)
43 |     regexp_parser (2.10.0)
44 |     rexml (3.4.1)
45 |     rspec (3.13.0)
46 |       rspec-core (~> 3.13.0)
47 |       rspec-expectations (~> 3.13.0)
48 |       rspec-mocks (~> 3.13.0)
49 |     rspec-core (3.13.3)
50 |       rspec-support (~> 3.13.0)
51 |     rspec-expectations (3.13.3)
52 |       diff-lcs (>= 1.2.0, < 2.0)
53 |       rspec-support (~> 3.13.0)
54 |     rspec-mocks (3.13.2)
55 |       diff-lcs (>= 1.2.0, < 2.0)
56 |       rspec-support (~> 3.13.0)
57 |     rspec-support (3.13.2)
58 |     rubocop (1.75.1)
59 |       json (~> 2.3)
60 |       language_server-protocol (~> 3.17.0.2)
61 |       lint_roller (~> 1.1.0)
62 |       parallel (~> 1.10)
63 |       parser (>= 3.3.0.2)
64 |       rainbow (>= 2.2.2, < 4.0)
65 |       regexp_parser (>= 2.9.3, < 3.0)
66 |       rubocop-ast (>= 1.43.0, < 2.0)
67 |       ruby-progressbar (~> 1.7)
68 |       unicode-display_width (>= 2.4.0, < 4.0)
69 |     rubocop-ast (1.43.0)
70 |       parser (>= 3.3.7.2)
71 |       prism (~> 1.4)
72 |     ruby-progressbar (1.13.0)
73 |     tty-cursor (0.7.1)
74 |     tty-screen (0.8.2)
75 |     unicode-display_width (3.1.4)
76 |       unicode-emoji (~> 4.0, >= 4.0.4)
77 |     unicode-emoji (4.0.4)
78 |     xcodeproj (1.27.0)
79 |       CFPropertyList (>= 2.3.3, < 4.0)
80 |       atomos (~> 0.1.3)
81 |       claide (>= 1.0.2, < 2.0)
82 |       colored2 (~> 3.1)
83 |       nanaimo (~> 0.4.0)
84 |       rexml (>= 3.3.6, < 4.0)
85 | 
86 | PLATFORMS
87 |   arm64-darwin-22
88 |   ruby
89 | 
90 | DEPENDENCIES
91 |   bundler (> 1.3)
92 |   pry-nav
93 |   rspec
94 |   rubocop
95 |   xccache!
96 | 
97 | BUNDLED WITH
98 |    2.5.21
99 | 
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2022 Thuyen Trinh 
 2 | 
 3 | MIT License
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining
 6 | a copy of this software and associated documentation files (the
 7 | "Software"), to deal in the Software without restriction, including
 8 | without limitation the rights to use, copy, modify, merge, publish,
 9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 | 
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 | 
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | 
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
 1 | install:
 2 | 	which pre-commit &> /dev/null || pip3 install pre-commit
 3 | 	pre-commit install
 4 | 	bundle install
 5 | 
 6 | format:
 7 | 	pre-commit run --all-files
 8 | 
 9 | test:
10 | 	bundle exec rspec
11 | 
12 | ex.%:
13 | 	cd examples && make $*
14 | 
15 | proxy.build:
16 | 	mkdir -p libexec/.local
17 | 	cd tools/xccache-proxy && make build CONFIGURATION=release
18 | 	cp tools/xccache-proxy/.build/release/xccache-proxy libexec/.local/
19 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | # Yet another caching tool for Xcode projects but with SPM support
 4 | 
 5 | [](https://github.com/trinhngocthuyen/xccache/blob/main/LICENSE.txt)
 6 | [](https://rubygems.org/gems/xccache)
 7 | 
 8 | ## 🎯 Motivation
 9 | Caching frameworks is a popular technique to tackle project build time in many large-scale projects. A few outstanding tools in this category are cocoapods-binary-cache, Rugby, and XCRemoteCache. Rugby and cocoapods-binary-cache are particularly tailored for CocoaPods-based projects while XCRemoteCache works for a more general project structure. However, all three of them has the same limitation: **lacking support for SPM/Swift packages**.
10 | 
11 | This **xccache** tool attempts to bridge that gap.\
12 | The long-term vision of this initiative is to make it a unified caching tool for iOS projects, including CocoaPods-based structures.
13 | 
14 | ## 🔧 Installation
15 | Via [Bundler](https://bundler.io): Add the gem `xccache` to the Gemfile of your project.
16 | 
17 | ```rb
18 | gem "xccache"
19 | ```
20 | 
21 | Via [RubyGems](https://rubygems.org):
22 | ```sh
23 | $ gem install xccache
24 | ```
25 | 
26 | ## 🚀 Getting Started
27 | Check out this doc: [Getting Started](docs/getting-started.md)
28 | 
29 | TLDR: Simply run `xccache` to set it up. Run `xccache --help` to explore the usages.
30 | 
31 | If caches are in place, you should see this in Xcode build logs.
32 | 
33 |  34 | 
35 | #### Case Study: For Kickstarter iOS project
36 | 
37 | 👉🏻 Check it out: [here](docs/case-study-kickstarter.md) 🎉
38 | 
39 | 
40 | ## 📑 Documentation
41 | 
42 | Check out these docs to understand more about xccache:
43 | 
44 | - [🔧 How to Install](docs/how-to-install.md)
45 | - [📝 Overview](docs/overview.md)
46 | - [🚀 Getting Started](docs/getting-started.md)
47 | - [📖 Under the Hood](docs/under-the-hood)
48 |   - [Packaging as an xcframework](docs/under-the-hood/packaging-as-xcframework.md)
49 |   - [Ensuring `Bundle.module` When Accessing Resources](docs/under-the-hood/ensuring-bundle-module.md)
50 |   - [Macro as Binary](docs/under-the-hood/macro-as-binary.md)
51 |   - [Proxy Packages](docs/under-the-hood/proxy-packages.md)
52 | - [🩺 Troubleshooting](docs/troubleshooting.md)
53 | - [✍🏼 Case Study: Using XCCache in Kickstarter iOS Project](docs/case-study-kickstarter.md)
54 | 
55 | ## 📌 Features and Roadmap
56 | 
57 | Check out this doc: [Features and Roadmap](docs/features-roadmap.md)
58 | 
59 | ## 🤝 Contribution
60 | Refer to the [contributing guidelines](docs/contributing-guidelines.md) for details.
61 | 
62 | ## ⚖️ License
63 | The tool is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
64 | 
65 | ## ✍️ Acknowledgement
66 | - [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) for the cachemap visualization
67 | 
68 | ## ⭐ Support
69 | If you find this project interesting and useful, keep me going by leaving a star ⭐, sharing the project, or [buying me a coffee](https://buymeacoffee.com/trinhngocthuyen) 🫶.
70 | 
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 1.0.5
2 | 
--------------------------------------------------------------------------------
/bin/xccache:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "xccache"
3 | 
4 | XCCache::Command.run(ARGV)
5 | 
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
 1 | [< 🏠 Github](https://github.com/trinhngocthuyen/xccache)
 2 | 
 3 | # 📚 XCCache Knowledge Base
 4 | 
 5 | Yet another caching tool for Xcode projects but with SPM support
 6 | 
 7 | ## 🎯 Motivation
 8 | Caching frameworks is a popular technique to tackle project build time in many large-scale projects. A few outstanding tools in this category are cocoapods-binary-cache, Rugby, and XCRemoteCache. Rugby and cocoapods-binary-cache are particularly tailored for CocoaPods-based projects while XCRemoteCache works for a more general project structure. However, all three of them has the same limitation: **lacking support for SPM/Swift packages**.
 9 | 
10 | This **xccache** tool attempts to bridge that gap.\
11 | The long-term vision of this initiative is to make it a unified caching tool for iOS projects, including CocoaPods-based structures.
12 | 
13 | ## 📑 Documentation
14 | 
15 | Check out these docs to understand more about xccache:
16 | 
17 | - [🔧 How to Install](how-to-install.md)
18 | - [📝 Overview](overview.md)
19 | - [🚀 Getting Started](getting-started.md)
20 | - [📖 Under the Hood](under-the-hood)
21 |   - [Packaging as an xcframework](under-the-hood/packaging-as-xcframework.md)
22 |   - [Ensuring `Bundle.module` When Accessing Resources](under-the-hood/ensuring-bundle-module.md)
23 |   - [Macro as Binary](under-the-hood/macro-as-binary.md)
24 |   - [Proxy Packages](under-the-hood/proxy-packages.md)
25 | - [🩺 Troubleshooting](troubleshooting.md)
26 | - [✍🏼 Case Study: Using XCCache in Kickstarter iOS Project](case-study-kickstarter.md)
27 | 
28 | ## 📌 Features and Roadmap
29 | Check out this doc: [Features and Roadmap](features-roadmap.md)
30 | 
31 | ## 🤝 Contribution
32 | Refer to the [contributing guidelines](contributing-guidelines.md) for details.
33 | 
34 | ## ⚖️ License
35 | The tool is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
36 | 
37 | ## ✍️ Acknowledgement
38 | - [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) for the cachemap visualization
39 | 
40 | ## ⭐ Support
41 | If you find this project interesting and useful, keep me going by leaving a star ⭐, sharing the project, or [buying me a coffee](https://buymeacoffee.com/trinhngocthuyen) 🫶.
42 | 
--------------------------------------------------------------------------------
/docs/case-study-kickstarter.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # ✍🏼 Case Study: Using XCCache in Kickstarter iOS Project
 4 | 
 5 | Let’s try this xccache tool with the [Kickstarter iOS](https://github.com/kickstarter/ios-oss) project.
 6 | 
 7 | I forked the project to [trinhngocthuyen/kickstarter-ios](https://github.com/trinhngocthuyen/kickstarter-ios). Check out the branch [try/xccache](https://github.com/trinhngocthuyen/kickstarter-ios/tree/try/xccache) for the detailed integration.
 8 | 
 9 | Following are the steps needed for the integration.
10 | 
11 | #### Step 1. Add `xccache` to Gemfile
12 | 
13 | Then, run `bundle install` to have it installed.
14 | 
15 | #### Step 2. [Optional] Using existing remote cache
16 | 
17 | Specify the git repo for the cache in [`xccache.yml`](https://github.com/trinhngocthuyen/kickstarter-ios/blob/try/xccache/xccache.yml).
18 | 
19 | ```yaml
20 | remote:
21 |   default:
22 |     git: https://github.com/trinhngocthuyen/.cache.git
23 | ```
24 | 
25 | Then, pull the cache from the given repo:
26 | 
27 | ```bash
28 | bundle exec xccache remote pull
29 | ```
30 | 
31 | Now, the cache should be available in `~/.xccache/debug`.
32 | ```
33 | $  tree ~/.xccache/debug -L 2
34 | 
35 | /Users/thuyen/.xccache/debug
36 | ├── Alamofire
37 | │   └── Alamofire-513364f8.xcframework
38 | ├── AlamofireImage
39 | │   └── AlamofireImage-1eaf3b6.xcframework
40 | ├── Apollo
41 | │   └── Apollo-5db23797b.xcframework
42 | ├── ApolloAPI
43 | │   └── ApolloAPI-5db23797b.xcframework
44 | ├── ApolloUtils
45 | │   └── ApolloUtils-5db23797b.xcframework
46 | ├── AppboyKit
47 | │   └── AppboyKit-a3511ca.xcframework
48 | ├── AppboySegment
49 | │   └── AppboySegment-dc659b7.xcframework
50 | ├── AppboyUI
51 | │   └── AppboyUI-a3511ca.xcframework
52 | ```
53 | 
54 | #### Step 3. Run `bundle exec xccache` to integrate the cache
55 | 
56 | There are some highlighting changes as follows:
57 | 
58 | - A new file: [`xccache.lock`](https://github.com/trinhngocthuyen/kickstarter-ios/blob/try/xccache/xccache.lock) that captures the dependencies in the project.
59 | - Changes in xcodeproj files (see: [here](https://github.com/trinhngocthuyen/kickstarter-ios/commit/7520c590e067d08661bc985a035e1a5576ab7208#diff-9cb89939ff9e9815f0bcf171699ed9e3090ae718529ada6e606566b32cdd42adR116)):
60 |     - A special package (xccache/packages/umbrella) is added
61 |     - Packages and their product dependencies are removed from xcodeproj.
62 |     Don’t worry, you can still use those products though.
63 | 
64 | #### Step 4. Trigger “Resolve Package Versions”
65 | 
66 | Tip: You should trigger this after running xccache command because Xcode doesn’t auto-resolve dependencies upon changes in xccache’s package manifest.
67 | 
68 | #### Step 5. Try a clean build
69 | 
70 | And observe the build time when having cache.
71 | The observed buid time on my Macbook Air (M1, 2020) is just nearly **2 minutes** 🎉.
72 | 
73 |
34 | 
35 | #### Case Study: For Kickstarter iOS project
36 | 
37 | 👉🏻 Check it out: [here](docs/case-study-kickstarter.md) 🎉
38 | 
39 | 
40 | ## 📑 Documentation
41 | 
42 | Check out these docs to understand more about xccache:
43 | 
44 | - [🔧 How to Install](docs/how-to-install.md)
45 | - [📝 Overview](docs/overview.md)
46 | - [🚀 Getting Started](docs/getting-started.md)
47 | - [📖 Under the Hood](docs/under-the-hood)
48 |   - [Packaging as an xcframework](docs/under-the-hood/packaging-as-xcframework.md)
49 |   - [Ensuring `Bundle.module` When Accessing Resources](docs/under-the-hood/ensuring-bundle-module.md)
50 |   - [Macro as Binary](docs/under-the-hood/macro-as-binary.md)
51 |   - [Proxy Packages](docs/under-the-hood/proxy-packages.md)
52 | - [🩺 Troubleshooting](docs/troubleshooting.md)
53 | - [✍🏼 Case Study: Using XCCache in Kickstarter iOS Project](docs/case-study-kickstarter.md)
54 | 
55 | ## 📌 Features and Roadmap
56 | 
57 | Check out this doc: [Features and Roadmap](docs/features-roadmap.md)
58 | 
59 | ## 🤝 Contribution
60 | Refer to the [contributing guidelines](docs/contributing-guidelines.md) for details.
61 | 
62 | ## ⚖️ License
63 | The tool is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
64 | 
65 | ## ✍️ Acknowledgement
66 | - [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) for the cachemap visualization
67 | 
68 | ## ⭐ Support
69 | If you find this project interesting and useful, keep me going by leaving a star ⭐, sharing the project, or [buying me a coffee](https://buymeacoffee.com/trinhngocthuyen) 🫶.
70 | 
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 1.0.5
2 | 
--------------------------------------------------------------------------------
/bin/xccache:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "xccache"
3 | 
4 | XCCache::Command.run(ARGV)
5 | 
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
 1 | [< 🏠 Github](https://github.com/trinhngocthuyen/xccache)
 2 | 
 3 | # 📚 XCCache Knowledge Base
 4 | 
 5 | Yet another caching tool for Xcode projects but with SPM support
 6 | 
 7 | ## 🎯 Motivation
 8 | Caching frameworks is a popular technique to tackle project build time in many large-scale projects. A few outstanding tools in this category are cocoapods-binary-cache, Rugby, and XCRemoteCache. Rugby and cocoapods-binary-cache are particularly tailored for CocoaPods-based projects while XCRemoteCache works for a more general project structure. However, all three of them has the same limitation: **lacking support for SPM/Swift packages**.
 9 | 
10 | This **xccache** tool attempts to bridge that gap.\
11 | The long-term vision of this initiative is to make it a unified caching tool for iOS projects, including CocoaPods-based structures.
12 | 
13 | ## 📑 Documentation
14 | 
15 | Check out these docs to understand more about xccache:
16 | 
17 | - [🔧 How to Install](how-to-install.md)
18 | - [📝 Overview](overview.md)
19 | - [🚀 Getting Started](getting-started.md)
20 | - [📖 Under the Hood](under-the-hood)
21 |   - [Packaging as an xcframework](under-the-hood/packaging-as-xcframework.md)
22 |   - [Ensuring `Bundle.module` When Accessing Resources](under-the-hood/ensuring-bundle-module.md)
23 |   - [Macro as Binary](under-the-hood/macro-as-binary.md)
24 |   - [Proxy Packages](under-the-hood/proxy-packages.md)
25 | - [🩺 Troubleshooting](troubleshooting.md)
26 | - [✍🏼 Case Study: Using XCCache in Kickstarter iOS Project](case-study-kickstarter.md)
27 | 
28 | ## 📌 Features and Roadmap
29 | Check out this doc: [Features and Roadmap](features-roadmap.md)
30 | 
31 | ## 🤝 Contribution
32 | Refer to the [contributing guidelines](contributing-guidelines.md) for details.
33 | 
34 | ## ⚖️ License
35 | The tool is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
36 | 
37 | ## ✍️ Acknowledgement
38 | - [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) for the cachemap visualization
39 | 
40 | ## ⭐ Support
41 | If you find this project interesting and useful, keep me going by leaving a star ⭐, sharing the project, or [buying me a coffee](https://buymeacoffee.com/trinhngocthuyen) 🫶.
42 | 
--------------------------------------------------------------------------------
/docs/case-study-kickstarter.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # ✍🏼 Case Study: Using XCCache in Kickstarter iOS Project
 4 | 
 5 | Let’s try this xccache tool with the [Kickstarter iOS](https://github.com/kickstarter/ios-oss) project.
 6 | 
 7 | I forked the project to [trinhngocthuyen/kickstarter-ios](https://github.com/trinhngocthuyen/kickstarter-ios). Check out the branch [try/xccache](https://github.com/trinhngocthuyen/kickstarter-ios/tree/try/xccache) for the detailed integration.
 8 | 
 9 | Following are the steps needed for the integration.
10 | 
11 | #### Step 1. Add `xccache` to Gemfile
12 | 
13 | Then, run `bundle install` to have it installed.
14 | 
15 | #### Step 2. [Optional] Using existing remote cache
16 | 
17 | Specify the git repo for the cache in [`xccache.yml`](https://github.com/trinhngocthuyen/kickstarter-ios/blob/try/xccache/xccache.yml).
18 | 
19 | ```yaml
20 | remote:
21 |   default:
22 |     git: https://github.com/trinhngocthuyen/.cache.git
23 | ```
24 | 
25 | Then, pull the cache from the given repo:
26 | 
27 | ```bash
28 | bundle exec xccache remote pull
29 | ```
30 | 
31 | Now, the cache should be available in `~/.xccache/debug`.
32 | ```
33 | $  tree ~/.xccache/debug -L 2
34 | 
35 | /Users/thuyen/.xccache/debug
36 | ├── Alamofire
37 | │   └── Alamofire-513364f8.xcframework
38 | ├── AlamofireImage
39 | │   └── AlamofireImage-1eaf3b6.xcframework
40 | ├── Apollo
41 | │   └── Apollo-5db23797b.xcframework
42 | ├── ApolloAPI
43 | │   └── ApolloAPI-5db23797b.xcframework
44 | ├── ApolloUtils
45 | │   └── ApolloUtils-5db23797b.xcframework
46 | ├── AppboyKit
47 | │   └── AppboyKit-a3511ca.xcframework
48 | ├── AppboySegment
49 | │   └── AppboySegment-dc659b7.xcframework
50 | ├── AppboyUI
51 | │   └── AppboyUI-a3511ca.xcframework
52 | ```
53 | 
54 | #### Step 3. Run `bundle exec xccache` to integrate the cache
55 | 
56 | There are some highlighting changes as follows:
57 | 
58 | - A new file: [`xccache.lock`](https://github.com/trinhngocthuyen/kickstarter-ios/blob/try/xccache/xccache.lock) that captures the dependencies in the project.
59 | - Changes in xcodeproj files (see: [here](https://github.com/trinhngocthuyen/kickstarter-ios/commit/7520c590e067d08661bc985a035e1a5576ab7208#diff-9cb89939ff9e9815f0bcf171699ed9e3090ae718529ada6e606566b32cdd42adR116)):
60 |     - A special package (xccache/packages/umbrella) is added
61 |     - Packages and their product dependencies are removed from xcodeproj.
62 |     Don’t worry, you can still use those products though.
63 | 
64 | #### Step 4. Trigger “Resolve Package Versions”
65 | 
66 | Tip: You should trigger this after running xccache command because Xcode doesn’t auto-resolve dependencies upon changes in xccache’s package manifest.
67 | 
68 | #### Step 5. Try a clean build
69 | 
70 | And observe the build time when having cache.
71 | The observed buid time on my Macbook Air (M1, 2020) is just nearly **2 minutes** 🎉.
72 | 
73 |  74 | 
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # ⚙️ Configuration
 4 | 
 5 | The behavior of xccache can be controlled via a `xccache.yml` configuration file under the root project directory. Example: [xccache.yml](/examples/xccache.yml).
 6 | 
 7 | Following are some available configurations.
 8 | 
 9 | ### `ignore_local`
10 | - Default: `false`
11 | - Whether to ignore local packages.
12 | 
13 | ### `ignore`
14 | - Default: `[]`
15 | - Swift package targets to ignore, in patterns.
16 | ```yml
17 | ignore:
18 |   - core-utils/*
19 | ```
20 | > [!NOTE]
21 | > These patterns apply to targets, not products.
22 | 
23 | ### `keep_pkgs_in_project`
24 | - Default: `false`
25 | - Whether to keep or remove packages from xcodeproj. By default, packages managed by xccache will be removed from xcodeproj in order to reduce time for package resolution in Xcode.
26 | 
27 | ### `ignore_build_errors`
28 | - Default: `false`
29 | - Whether to ignore build errors in `xccache build`. This option might be useful when building multiple targets and one of them fails, with this option as `true`, the tool still continues building other targets.
30 | 
31 | ### `default_sdk`
32 | - Default: `iphonesimulator`
33 | - The default sdk to use. Valid values: `iphonesimulator`, `iphoneos`, `macos`, `appletvos`, `appletvsimulator`, `watchos`, `watchsimulator`, `xros`, `xrsimulator`.
34 | 
35 | ### `remote`
36 | - The remote cache configuration (using Git, S3, etc.).
37 | 
38 | NOTE: This configuration is per build/install configuration (debug/release), as follows.
39 | 
40 | **Using Git**
41 | ```yml
42 | remote:
43 |   debug: # remote cache config for debug & release
44 |     git: git@github.com/org/cache
45 | ```
46 | 
47 | **Using S3**
48 | ```yml
49 | remote:
50 |   debug: # remote cache config for debug
51 |     git: https://github.com/trinhngocthuyen/.cache.git
52 |   release: # remote cache config for release
53 |     s3:
54 |       uri: "s3://xccache/binaries"
55 |       creds: "path/to/aws_creds.json"
56 | ```
57 | - `s3:uri`: The S3 URI, ex. `s3://xccache/binaries`
58 | - `s3:creds`: The path to the json credentials (default: `~/.xccache/s3.creds.json`). This json contains the access key id and secret access key as follows:
59 | ```json
60 | {
61 |   "access_key": "YOUR_KEY_ID",
62 |   "secret_access_key": "YOUR_ACCESS_KEY"
63 | }
64 | ```
65 | 
--------------------------------------------------------------------------------
/docs/contributing-guidelines.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 🤝 Contributing
 4 | 
 5 | You are more than welcome to contribute to the project in various ways:
 6 | - Implement features
 7 | - Fix bugs
 8 | - Write tests
 9 | - Write documentation
10 | 
11 | The following section describes the development workflow when contributing to the project.
12 | 
13 | ## Development Workflow
14 | 
15 | **Step 1. Clone the project**
16 | 
17 | ```sh
18 | git clone https://github.com/trinhngocthuyen/xccache.git && cd xccache
19 | ```
20 | 
21 | **Step 2. Install dependencies**
22 | 
23 | ```sh
24 | make install
25 | ```
26 | 
27 | **Step 3. Make changes**
28 | 
29 | You can try out your changes with the example project at `examples`.
30 | 
31 | **Step 4. Format changes**
32 | 
33 | This project is using `pre-commit` (which is installed in step 2) to lint & format changes.\
34 | By default, pre-commit auto lints and formats your changes. Therefore, make sure step 2 succeeded.\
35 | In case you want to trigger the format, simply run `make format`.
36 | 
37 | **Step 5: Commit changes and create pull requests**
38 | 
39 | ### xccache-proxy
40 | 
41 | [xccache-proxy](https://github.com/trinhngocthuyen/xccache-proxy) is an internal tool (at: tools/xccache-proxy) used for generating proxy packages (to manipulate cache).
42 | 
43 | Its binary is downloaded from remote, to `libexec/.download//xccache-proxy`. When there's a binary at `libexec/.local/xccache-proxy`, this binary is picked up for the execution.
44 | 
45 | Using the local xccache-proxy binary is preferred for local development. If you're making changes in xccache-proxy and want to test against this repo (xccache), simply build the binary by:
46 | ```sh
47 | make proxy.build
48 | ```
49 | 
--------------------------------------------------------------------------------
/docs/ex-viz/assets/style.css:
--------------------------------------------------------------------------------
 1 | :root {
 2 |   --primary-color: #1492A0;
 3 |   --bg-color: color-mix(in srgb, var(--primary-color), white 80%);
 4 | }
 5 | body {
 6 |   font-family: Helvetica, Arial, sans-serif;
 7 |   font-size: 12px;
 8 |   margin: 0;
 9 |   line-height: 1.6;
10 | }
11 | a { color: var(--primary-color) }
12 | a:hover { color: #339966; }
13 | .fa-solid { color: var(--primary-color) }
14 | .container {
15 |   display: flex;
16 |   height: 100vh;
17 | }
18 | #cy {
19 |   flex: 1;
20 | }
21 | #sidebar {
22 |   position: relative;
23 |   background-color: var(--bg-color);
24 |   width: 250px;
25 |   transition: all 0.3s ease;
26 | }
27 | .sidebar-content {
28 |   width: calc(250px - 32px);
29 |   padding: 16px;
30 |   transform: translateX(0px);
31 |   transition: all 0.3s ease;
32 | }
33 | #sidebar.collapsed {
34 |   width: 0;
35 | }
36 | #sidebar.collapsed .sidebar-content{
37 |   transform: translateX(-250px);
38 | }
39 | #sidebar.collapsed .toggle-btn {
40 |   right: -36px;
41 |   transform: rotate(180deg);
42 | }
43 | .toggle-btn {
44 |   position: absolute;
45 |   top: 20px;
46 |   right: 20px;
47 |   z-index: 999;
48 |   cursor: pointer;
49 |   width: 16px;
50 |   height: 16px;
51 |   fill: var(--primary-color);
52 |   transition: all 0.3s;
53 | }
54 | #sidebar .title {
55 |   color: var(--primary-color);
56 |   font-size: 16px;
57 |   margin-top: 0;
58 | }
59 | #sidebar section {
60 |   padding: 16px 0;
61 | }
62 | #sidebar .section-header {
63 |   color: color-mix(in srgb, var(--primary-color), grey 20%);
64 |   font-weight: bold;
65 |   margin-block-end: 4px;
66 | }
67 | .node-info {
68 |   display: none;
69 | }
70 | .metadata .info {
71 |   font-size: 10px;
72 | }
73 | .info {
74 |   color: #888;
75 | }
76 | .info .value {
77 |   color: #666;
78 | }
79 | .footnote {
80 |   color: #888;
81 |   position: absolute;
82 |   left: 16px;
83 |   bottom: 8px;
84 | }
85 | .node {
86 |   border-radius: 3px;
87 |   padding: 1px 3px;
88 |   color: white;
89 |   background-color: var(--color)
90 | }
91 | .desc { color: var(--color) }
92 | .hit { --color: #339966 }
93 | .missed { --color: #ff6f00 }
94 | .ignored { --color: #888 }
95 | 
--------------------------------------------------------------------------------
/docs/ex-viz/cachemap.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   
 6 |   Cachemap Visualization
 7 |   
 8 |   
 9 |   
10 |   
11 |   
12 |   
13 | 
14 | 
15 |   
50 |   
51 |   
54 | 
55 | 
56 | 
--------------------------------------------------------------------------------
/docs/features-roadmap.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 📌 Features and Roadmap
 4 | 
 5 | **Live features**
 6 | - ✅ Local cache: building/using/rolling back cache
 7 | - ✅ Cache visualization
 8 | - ✅ Switching between binary cache and source code
 9 | - ✅ Support for Swift macros
10 | - ✅ Remote cache: pulling/pushing cache with Git, S3
11 | 
12 | **Planned features**
13 | - ☐ Support for CocoaPods-based projects
14 | - ☐ Support for Swift plugins
15 | 
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
  1 | [< Knowledge Base](README.md)
  2 | 
  3 | # 🚀 Getting Started
  4 | 
  5 |
74 | 
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # ⚙️ Configuration
 4 | 
 5 | The behavior of xccache can be controlled via a `xccache.yml` configuration file under the root project directory. Example: [xccache.yml](/examples/xccache.yml).
 6 | 
 7 | Following are some available configurations.
 8 | 
 9 | ### `ignore_local`
10 | - Default: `false`
11 | - Whether to ignore local packages.
12 | 
13 | ### `ignore`
14 | - Default: `[]`
15 | - Swift package targets to ignore, in patterns.
16 | ```yml
17 | ignore:
18 |   - core-utils/*
19 | ```
20 | > [!NOTE]
21 | > These patterns apply to targets, not products.
22 | 
23 | ### `keep_pkgs_in_project`
24 | - Default: `false`
25 | - Whether to keep or remove packages from xcodeproj. By default, packages managed by xccache will be removed from xcodeproj in order to reduce time for package resolution in Xcode.
26 | 
27 | ### `ignore_build_errors`
28 | - Default: `false`
29 | - Whether to ignore build errors in `xccache build`. This option might be useful when building multiple targets and one of them fails, with this option as `true`, the tool still continues building other targets.
30 | 
31 | ### `default_sdk`
32 | - Default: `iphonesimulator`
33 | - The default sdk to use. Valid values: `iphonesimulator`, `iphoneos`, `macos`, `appletvos`, `appletvsimulator`, `watchos`, `watchsimulator`, `xros`, `xrsimulator`.
34 | 
35 | ### `remote`
36 | - The remote cache configuration (using Git, S3, etc.).
37 | 
38 | NOTE: This configuration is per build/install configuration (debug/release), as follows.
39 | 
40 | **Using Git**
41 | ```yml
42 | remote:
43 |   debug: # remote cache config for debug & release
44 |     git: git@github.com/org/cache
45 | ```
46 | 
47 | **Using S3**
48 | ```yml
49 | remote:
50 |   debug: # remote cache config for debug
51 |     git: https://github.com/trinhngocthuyen/.cache.git
52 |   release: # remote cache config for release
53 |     s3:
54 |       uri: "s3://xccache/binaries"
55 |       creds: "path/to/aws_creds.json"
56 | ```
57 | - `s3:uri`: The S3 URI, ex. `s3://xccache/binaries`
58 | - `s3:creds`: The path to the json credentials (default: `~/.xccache/s3.creds.json`). This json contains the access key id and secret access key as follows:
59 | ```json
60 | {
61 |   "access_key": "YOUR_KEY_ID",
62 |   "secret_access_key": "YOUR_ACCESS_KEY"
63 | }
64 | ```
65 | 
--------------------------------------------------------------------------------
/docs/contributing-guidelines.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 🤝 Contributing
 4 | 
 5 | You are more than welcome to contribute to the project in various ways:
 6 | - Implement features
 7 | - Fix bugs
 8 | - Write tests
 9 | - Write documentation
10 | 
11 | The following section describes the development workflow when contributing to the project.
12 | 
13 | ## Development Workflow
14 | 
15 | **Step 1. Clone the project**
16 | 
17 | ```sh
18 | git clone https://github.com/trinhngocthuyen/xccache.git && cd xccache
19 | ```
20 | 
21 | **Step 2. Install dependencies**
22 | 
23 | ```sh
24 | make install
25 | ```
26 | 
27 | **Step 3. Make changes**
28 | 
29 | You can try out your changes with the example project at `examples`.
30 | 
31 | **Step 4. Format changes**
32 | 
33 | This project is using `pre-commit` (which is installed in step 2) to lint & format changes.\
34 | By default, pre-commit auto lints and formats your changes. Therefore, make sure step 2 succeeded.\
35 | In case you want to trigger the format, simply run `make format`.
36 | 
37 | **Step 5: Commit changes and create pull requests**
38 | 
39 | ### xccache-proxy
40 | 
41 | [xccache-proxy](https://github.com/trinhngocthuyen/xccache-proxy) is an internal tool (at: tools/xccache-proxy) used for generating proxy packages (to manipulate cache).
42 | 
43 | Its binary is downloaded from remote, to `libexec/.download//xccache-proxy`. When there's a binary at `libexec/.local/xccache-proxy`, this binary is picked up for the execution.
44 | 
45 | Using the local xccache-proxy binary is preferred for local development. If you're making changes in xccache-proxy and want to test against this repo (xccache), simply build the binary by:
46 | ```sh
47 | make proxy.build
48 | ```
49 | 
--------------------------------------------------------------------------------
/docs/ex-viz/assets/style.css:
--------------------------------------------------------------------------------
 1 | :root {
 2 |   --primary-color: #1492A0;
 3 |   --bg-color: color-mix(in srgb, var(--primary-color), white 80%);
 4 | }
 5 | body {
 6 |   font-family: Helvetica, Arial, sans-serif;
 7 |   font-size: 12px;
 8 |   margin: 0;
 9 |   line-height: 1.6;
10 | }
11 | a { color: var(--primary-color) }
12 | a:hover { color: #339966; }
13 | .fa-solid { color: var(--primary-color) }
14 | .container {
15 |   display: flex;
16 |   height: 100vh;
17 | }
18 | #cy {
19 |   flex: 1;
20 | }
21 | #sidebar {
22 |   position: relative;
23 |   background-color: var(--bg-color);
24 |   width: 250px;
25 |   transition: all 0.3s ease;
26 | }
27 | .sidebar-content {
28 |   width: calc(250px - 32px);
29 |   padding: 16px;
30 |   transform: translateX(0px);
31 |   transition: all 0.3s ease;
32 | }
33 | #sidebar.collapsed {
34 |   width: 0;
35 | }
36 | #sidebar.collapsed .sidebar-content{
37 |   transform: translateX(-250px);
38 | }
39 | #sidebar.collapsed .toggle-btn {
40 |   right: -36px;
41 |   transform: rotate(180deg);
42 | }
43 | .toggle-btn {
44 |   position: absolute;
45 |   top: 20px;
46 |   right: 20px;
47 |   z-index: 999;
48 |   cursor: pointer;
49 |   width: 16px;
50 |   height: 16px;
51 |   fill: var(--primary-color);
52 |   transition: all 0.3s;
53 | }
54 | #sidebar .title {
55 |   color: var(--primary-color);
56 |   font-size: 16px;
57 |   margin-top: 0;
58 | }
59 | #sidebar section {
60 |   padding: 16px 0;
61 | }
62 | #sidebar .section-header {
63 |   color: color-mix(in srgb, var(--primary-color), grey 20%);
64 |   font-weight: bold;
65 |   margin-block-end: 4px;
66 | }
67 | .node-info {
68 |   display: none;
69 | }
70 | .metadata .info {
71 |   font-size: 10px;
72 | }
73 | .info {
74 |   color: #888;
75 | }
76 | .info .value {
77 |   color: #666;
78 | }
79 | .footnote {
80 |   color: #888;
81 |   position: absolute;
82 |   left: 16px;
83 |   bottom: 8px;
84 | }
85 | .node {
86 |   border-radius: 3px;
87 |   padding: 1px 3px;
88 |   color: white;
89 |   background-color: var(--color)
90 | }
91 | .desc { color: var(--color) }
92 | .hit { --color: #339966 }
93 | .missed { --color: #ff6f00 }
94 | .ignored { --color: #888 }
95 | 
--------------------------------------------------------------------------------
/docs/ex-viz/cachemap.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   
 6 |   Cachemap Visualization
 7 |   
 8 |   
 9 |   
10 |   
11 |   
12 |   
13 | 
14 | 
15 |   
50 |   
51 |   
54 | 
55 | 
56 | 
--------------------------------------------------------------------------------
/docs/features-roadmap.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 📌 Features and Roadmap
 4 | 
 5 | **Live features**
 6 | - ✅ Local cache: building/using/rolling back cache
 7 | - ✅ Cache visualization
 8 | - ✅ Switching between binary cache and source code
 9 | - ✅ Support for Swift macros
10 | - ✅ Remote cache: pulling/pushing cache with Git, S3
11 | 
12 | **Planned features**
13 | - ☐ Support for CocoaPods-based projects
14 | - ☐ Support for Swift plugins
15 | 
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
  1 | [< Knowledge Base](README.md)
  2 | 
  3 | # 🚀 Getting Started
  4 | 
  5 | 
  6 | Table of Contents
  7 | 
 39 |  
 40 | 
 41 | ## Quick Start
 42 | 
 43 | [✍🏼 Case Study: Using XCCache in Kickstarter iOS Project](case-study-kickstarter.md) ← Read here.
 44 | 
 45 | Simply run `xccache` under the root directory of the project. Then, you should see:
 46 | - **`xccache.lock`**: containing the info about packages in the project alongside the products being used. You're recommended to track this file in git.
 47 | - The `xccache` directory: containing build intermediates for the integration. This directory is similar to the `Pods` directory (in CocoaPods). Do NOT remove this directory. Instead, please ignore it from git.
 48 | 
 49 | If you see product dependencies of a Swift package being removed from the *Link Binary With Libraries* section, it is expected.\
 50 | In return, this plugin adds another `.xccache` product which includes your product dependencies.
 51 | 
 52 |  53 | 
 54 | Also, you may notice that all packages turn into local packages. This is perfectly normal as the tool creates special packages called *proxy packages* to manipulate cache of a package.
 55 | 
 56 |
 53 | 
 54 | Also, you may notice that all packages turn into local packages. This is perfectly normal as the tool creates special packages called *proxy packages* to manipulate cache of a package.
 55 | 
 56 |  57 | 
 58 | ## Understanding the Tool
 59 | Read the overview: [here](overview.md).
 60 | 
 61 | Following are some docs about what happens under the hood:
 62 | - [Packaging as an xcframework](under-the-hood/packaging-as-xcframework.md)
 63 | - [Ensuring `Bundle.module` When Accessing Resources](under-the-hood/ensuring-bundle-module.md)
 64 | - [Macro as Binary](under-the-hood/macro-as-binary.md)
 65 | - [Proxy Packages](under-the-hood/proxy-packages.md)
 66 | 
 67 | ## Working With Cache
 68 | 
 69 | > [!TIP]
 70 | > Use the `--help` option in the CLI to explore available (sub)commands and their supported options/flags.
 71 | 
 72 | ### Building Cache
 73 | 
 74 | To build cache of Swift packages, run `xccache build`.
 75 | 
 76 | By default, the tool only builds cache-missed targets. To build specify targets, specify them in the arguments, for example:
 77 | ```sh
 78 | xccache build SwiftyBeaver SDWebImage
 79 | ```
 80 | The prebuilt xcframeworks are available under `xccache/binaries`, following the structure as below:
 81 | ```
 82 | xccache /-- binaries /-- SwiftyBeaver /-- SwiftyBeaver-.xcframework
 83 |                                       |-- SwiftyBeaver.xcframework
 84 | ```
 85 | To build dependencies if cache-missed, use the `--recursive` option. For example, to build cache of `FirebaseCrashlytics` (including its dependencies):
 86 | ```sh
 87 | xccache build FirebaseCrashlytics --recursive
 88 | ```
 89 | 
 90 | ### Using Cache
 91 | Run `xccache use` or simply `xccache` to integrate cache to the project. Note that cache, after being built with `xccache build`, is automatically integrated to the project. You don't need to run `xccache use` in this case.
 92 | 
 93 | In the Package Dependencies section in Xcode, you should notice a special package call `xccache`.
 94 | The `binaries` directory of this package reflects the cache being used. For example, in the following image, `SwiftyBeaver` is integrated as binary.
 95 | 
 96 |
 57 | 
 58 | ## Understanding the Tool
 59 | Read the overview: [here](overview.md).
 60 | 
 61 | Following are some docs about what happens under the hood:
 62 | - [Packaging as an xcframework](under-the-hood/packaging-as-xcframework.md)
 63 | - [Ensuring `Bundle.module` When Accessing Resources](under-the-hood/ensuring-bundle-module.md)
 64 | - [Macro as Binary](under-the-hood/macro-as-binary.md)
 65 | - [Proxy Packages](under-the-hood/proxy-packages.md)
 66 | 
 67 | ## Working With Cache
 68 | 
 69 | > [!TIP]
 70 | > Use the `--help` option in the CLI to explore available (sub)commands and their supported options/flags.
 71 | 
 72 | ### Building Cache
 73 | 
 74 | To build cache of Swift packages, run `xccache build`.
 75 | 
 76 | By default, the tool only builds cache-missed targets. To build specify targets, specify them in the arguments, for example:
 77 | ```sh
 78 | xccache build SwiftyBeaver SDWebImage
 79 | ```
 80 | The prebuilt xcframeworks are available under `xccache/binaries`, following the structure as below:
 81 | ```
 82 | xccache /-- binaries /-- SwiftyBeaver /-- SwiftyBeaver-.xcframework
 83 |                                       |-- SwiftyBeaver.xcframework
 84 | ```
 85 | To build dependencies if cache-missed, use the `--recursive` option. For example, to build cache of `FirebaseCrashlytics` (including its dependencies):
 86 | ```sh
 87 | xccache build FirebaseCrashlytics --recursive
 88 | ```
 89 | 
 90 | ### Using Cache
 91 | Run `xccache use` or simply `xccache` to integrate cache to the project. Note that cache, after being built with `xccache build`, is automatically integrated to the project. You don't need to run `xccache use` in this case.
 92 | 
 93 | In the Package Dependencies section in Xcode, you should notice a special package call `xccache`.
 94 | The `binaries` directory of this package reflects the cache being used. For example, in the following image, `SwiftyBeaver` is integrated as binary.
 95 | 
 96 |  97 | 
 98 | In Xcode build log, you should see xcframeworks of the cache-hit targets being processed by Xcode.
 99 |
 97 | 
 98 | In Xcode build log, you should see xcframeworks of the cache-hit targets being processed by Xcode.
 99 |  100 | 
101 | ### Viewing Cachemap Visualization
102 | 
103 | Whenever cache is integrated into your project (via `xccache`, `xccache use`, or `xccache build`), the tool generates an html (at `xccache/cachemap.html`) that visualizes the cache dependencies.\
104 | Example: 👉 [ex-viz/cachemap](https://trinhngocthuyen.com/xccache/ex-viz/cachemap).
105 | 
106 | Open this html in your browser to better understand the depenencies in your project.
107 |
100 | 
101 | ### Viewing Cachemap Visualization
102 | 
103 | Whenever cache is integrated into your project (via `xccache`, `xccache use`, or `xccache build`), the tool generates an html (at `xccache/cachemap.html`) that visualizes the cache dependencies.\
104 | Example: 👉 [ex-viz/cachemap](https://trinhngocthuyen.com/xccache/ex-viz/cachemap).
105 | 
106 | Open this html in your browser to better understand the depenencies in your project.
107 |  108 | 
109 | ### Switching Between Binary and Source Code
110 | 
111 | By default, the tool attemtps to use cache if exists. In case you want to force-switch to source mode for specific targets, there are a few approaches you may consider:
112 | 
113 | (1) Run `xccache off ` (ex. `xccache off DebugKit ResourceKit`).\
114 | Note that the preferences set by this command is not persistent. This means, the next time you run `xccache`, those targets will not be remembered; cache will be integrated if exists.
115 | 
116 | (2) If you're looking for a persistent preferences, consider adding them to the [`ignore`](configuration.md#ignore) list in the configuration file.
117 | 
118 | (3) Or, you can simply just delete the cache, ex. `rm -rf ~/.xccache/debug/DebugKit`.
119 | 
120 | > [!IMPORTANT]
121 | > After running any xccache command, remember to trigger resolving package versions again (File -> Packages -> Resolve Package Versions). Xcode doesn't automatically reload packages upon changes.
122 | 
123 | ### Rolling Back Cache
124 | 
125 | Run `xccache rollback`. This returns the project to the original state where product dependencies are specified in the *Link Binary With Libraries* section and `.xccache` is removed from this section.
126 | > [!WARNING]
127 | > Well, you're advised not to use this action if not necessary.\
128 | > If you want to use source code entirely, consider *purging the cache* (`xccache cache clean --all`) instead.
129 | 
130 | ### Multiplatform Cache
131 | 
132 | An xcframework can include slices for multiple platforms. Use the `--sdk` option to specify the sdk (iphonesimulator, iphoneos, etc.) to use. If not specified, it uses the [`default_sdk`](configuration.md#default_sdk) configuration in the config if exist. Otherwise, it defaults to `iphonesimulator`.
133 | 
134 | When building cache, the tool **merges existing slices with the newly created** to reduce unnecessary builds for multiplatform support. This behavior is controlled by the `--merge-slices` flag (default: `true`). To disable it, ie. replacing the existing xcframework if exists, specify `--no-merge-slices`.
135 | 
136 | ```sh
137 | xccache build SwiftyBeaver --sdk=iphonesimulator
138 | xccache build SwiftyBeaver --sdk=iphoneos # <-- here, xcframework contains both sdks: iphonesimulator and iphoneos
139 | 
140 | xccache build SwiftyBeaver --sdk=macos --no-merge-slices # <-- here, xcframework contains only macos sdk
141 | ```
142 | 
143 | ### Per-Configuration Cache
144 | 
145 | Cache of different build configurations (debug/release) is hosted in separate directories `~/.xccache/`. The build configuration is defaulted to `debug`. To specify a different build configuration, use the `--config` argument.
146 | ```sh
147 | xccache build SwiftyBeaver --config=release
148 | xccache --config=release
149 | ```
150 | 
151 | ### Sharing Remote Cache
152 | Cache can be shared among team with remote cache, using Git or S3.
153 | ```sh
154 | xccache remote pull # <-- pull cache
155 | xccache remote push # <-- push cache
156 | ```
157 | 
158 | Refer to the [remote configuration](configuration.md#remote) for the setup.
159 | 
160 | ## Working With Swift Packages
161 | ### Building a Swift Package Target
162 | 
163 | Packaging a Swift package target as binary is not as easy as it seems, which involves several steps. This tool offers a convenient way to build such a target into an xcframework with just only one step. Check out build options (ex. configuration, sdk, etc.) with `--help`.
164 | ```sh
165 | xccache pkg build 
166 | ```
167 | 
168 | ## Managing Dependencies
169 | ### Adding a Dependency
170 | 
171 | To add a new package, or new product dependencies (in the *Link Binary With Libraries* section), you can just add it the way you usually do (via Xcode), then just run `xccache` again.
172 | After that, you should see the changes reflected in xccache.lock.
173 | 
174 |
108 | 
109 | ### Switching Between Binary and Source Code
110 | 
111 | By default, the tool attemtps to use cache if exists. In case you want to force-switch to source mode for specific targets, there are a few approaches you may consider:
112 | 
113 | (1) Run `xccache off ` (ex. `xccache off DebugKit ResourceKit`).\
114 | Note that the preferences set by this command is not persistent. This means, the next time you run `xccache`, those targets will not be remembered; cache will be integrated if exists.
115 | 
116 | (2) If you're looking for a persistent preferences, consider adding them to the [`ignore`](configuration.md#ignore) list in the configuration file.
117 | 
118 | (3) Or, you can simply just delete the cache, ex. `rm -rf ~/.xccache/debug/DebugKit`.
119 | 
120 | > [!IMPORTANT]
121 | > After running any xccache command, remember to trigger resolving package versions again (File -> Packages -> Resolve Package Versions). Xcode doesn't automatically reload packages upon changes.
122 | 
123 | ### Rolling Back Cache
124 | 
125 | Run `xccache rollback`. This returns the project to the original state where product dependencies are specified in the *Link Binary With Libraries* section and `.xccache` is removed from this section.
126 | > [!WARNING]
127 | > Well, you're advised not to use this action if not necessary.\
128 | > If you want to use source code entirely, consider *purging the cache* (`xccache cache clean --all`) instead.
129 | 
130 | ### Multiplatform Cache
131 | 
132 | An xcframework can include slices for multiple platforms. Use the `--sdk` option to specify the sdk (iphonesimulator, iphoneos, etc.) to use. If not specified, it uses the [`default_sdk`](configuration.md#default_sdk) configuration in the config if exist. Otherwise, it defaults to `iphonesimulator`.
133 | 
134 | When building cache, the tool **merges existing slices with the newly created** to reduce unnecessary builds for multiplatform support. This behavior is controlled by the `--merge-slices` flag (default: `true`). To disable it, ie. replacing the existing xcframework if exists, specify `--no-merge-slices`.
135 | 
136 | ```sh
137 | xccache build SwiftyBeaver --sdk=iphonesimulator
138 | xccache build SwiftyBeaver --sdk=iphoneos # <-- here, xcframework contains both sdks: iphonesimulator and iphoneos
139 | 
140 | xccache build SwiftyBeaver --sdk=macos --no-merge-slices # <-- here, xcframework contains only macos sdk
141 | ```
142 | 
143 | ### Per-Configuration Cache
144 | 
145 | Cache of different build configurations (debug/release) is hosted in separate directories `~/.xccache/`. The build configuration is defaulted to `debug`. To specify a different build configuration, use the `--config` argument.
146 | ```sh
147 | xccache build SwiftyBeaver --config=release
148 | xccache --config=release
149 | ```
150 | 
151 | ### Sharing Remote Cache
152 | Cache can be shared among team with remote cache, using Git or S3.
153 | ```sh
154 | xccache remote pull # <-- pull cache
155 | xccache remote push # <-- push cache
156 | ```
157 | 
158 | Refer to the [remote configuration](configuration.md#remote) for the setup.
159 | 
160 | ## Working With Swift Packages
161 | ### Building a Swift Package Target
162 | 
163 | Packaging a Swift package target as binary is not as easy as it seems, which involves several steps. This tool offers a convenient way to build such a target into an xcframework with just only one step. Check out build options (ex. configuration, sdk, etc.) with `--help`.
164 | ```sh
165 | xccache pkg build 
166 | ```
167 | 
168 | ## Managing Dependencies
169 | ### Adding a Dependency
170 | 
171 | To add a new package, or new product dependencies (in the *Link Binary With Libraries* section), you can just add it the way you usually do (via Xcode), then just run `xccache` again.
172 | After that, you should see the changes reflected in xccache.lock.
173 | 
174 |  175 | 
176 | Alternatively, you can directly modify the lockfile with the changes above, and run `xccache`. This way, you can avoid modifying the xcodeproj file.
177 | 
178 | ### Removing a Dependency
179 | 
180 | Just directly update the lockfile:
181 | - Remove it from the dependencies section
182 | - Remove it from the packages section if not in use
183 | 
184 | ```
185 |   "dependencies": {
186 |     "EX": [
187 |       "Moya/Moya",
188 |       "SwiftyBeaver/SwiftyBeaver", // <-- Remove this if not in use
189 |     ]
190 |   },
191 |   "packages": [
192 |     { // <-- Remove this if not in use
193 |       "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver",
194 |       "requirement": {
195 |         "kind": "upToNextMajorVersion",
196 |         "minimumVersion": "2.1.1"
197 |       }
198 |     }
199 |   ]
200 | ```
201 | 
202 | ## Configuration
203 | Check out this doc: [Configuration](configuration.md)
204 | 
--------------------------------------------------------------------------------
/docs/how-to-install.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 🔧 How to Install
 4 | 
 5 | Via [Bundler](https://bundler.io): Add the gem `xccache` to the Gemfile of your project.
 6 | 
 7 | ```rb
 8 | gem "xccache"
 9 | ```
10 | 
11 | Via [RubyGems](https://rubygems.org):
12 | ```sh
13 | $ gem install xccache
14 | ```
15 | 
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 📝 Overview
 4 | ## Cache as xcframeworks
 5 | 
 6 | 
 7 | The `xccache` CLI provides some functionalities to build a Swift package target into an xcframework (for more details, check out the [Under the Hood](#under-the-hood) section). This xcframework can be used in the project in many ways. See more: [Declare a binary target in the package manifest](https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages#Declare-a-binary-target-in-the-package-manifest).
 8 | 
 9 | The tool manages a special *umbrella package* (at: xccache/packages/umbrella) to manipulate cache dependencies in the project. In case of cache hit, it replaces the original dependency (with source code) with the corresponding prebuilt dependency.
10 | 
11 | ### Cache Fallback
12 | In case of cache miss, it automatically uses the original dependency.
13 | 
14 | ### Cache Validation Model
15 | (1) **Checksum-based**: An xcframework is associated with a checksum of its package. If the checksum does not match -> cache miss.
16 | 
17 | 
18 | 
19 | ## Under the Hood
20 | - [Packaging as an xcframework](under-the-hood/packaging-as-xcframework.md)
21 | - [Ensuring `Bundle.module` When Accessing Resources](under-the-hood/ensuring-bundle-module.md)
22 | 
--------------------------------------------------------------------------------
/docs/res/binary_macro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/binary_macro.png
--------------------------------------------------------------------------------
/docs/res/cachemap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/cachemap.png
--------------------------------------------------------------------------------
/docs/res/kickstarter_clean_build.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/kickstarter_clean_build.png
--------------------------------------------------------------------------------
/docs/res/lockfile_add_new_dep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/lockfile_add_new_dep.png
--------------------------------------------------------------------------------
/docs/res/proxy_binary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/proxy_binary.png
--------------------------------------------------------------------------------
/docs/res/proxy_local.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/proxy_local.png
--------------------------------------------------------------------------------
/docs/res/resource_build_structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/resource_build_structure.png
--------------------------------------------------------------------------------
/docs/res/resource_bundle_module.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/resource_bundle_module.png
--------------------------------------------------------------------------------
/docs/res/resource_bundle_overriding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/resource_bundle_overriding.png
--------------------------------------------------------------------------------
/docs/res/switching_binary_to_source.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/switching_binary_to_source.gif
--------------------------------------------------------------------------------
/docs/res/switching_binary_to_source.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/switching_binary_to_source.mp4
--------------------------------------------------------------------------------
/docs/res/umbrella_product_dependencies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/umbrella_product_dependencies.png
--------------------------------------------------------------------------------
/docs/res/unknown_deps_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/unknown_deps_err.png
--------------------------------------------------------------------------------
/docs/res/xccache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/xccache.png
--------------------------------------------------------------------------------
/docs/res/xcode_process_xcframeworks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/xcode_process_xcframeworks.png
--------------------------------------------------------------------------------
/docs/troubleshooting.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 🩺 Troubleshooting
 4 | 
 5 | ### Unknown Product Dependencies
 6 | 
 7 |
175 | 
176 | Alternatively, you can directly modify the lockfile with the changes above, and run `xccache`. This way, you can avoid modifying the xcodeproj file.
177 | 
178 | ### Removing a Dependency
179 | 
180 | Just directly update the lockfile:
181 | - Remove it from the dependencies section
182 | - Remove it from the packages section if not in use
183 | 
184 | ```
185 |   "dependencies": {
186 |     "EX": [
187 |       "Moya/Moya",
188 |       "SwiftyBeaver/SwiftyBeaver", // <-- Remove this if not in use
189 |     ]
190 |   },
191 |   "packages": [
192 |     { // <-- Remove this if not in use
193 |       "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver",
194 |       "requirement": {
195 |         "kind": "upToNextMajorVersion",
196 |         "minimumVersion": "2.1.1"
197 |       }
198 |     }
199 |   ]
200 | ```
201 | 
202 | ## Configuration
203 | Check out this doc: [Configuration](configuration.md)
204 | 
--------------------------------------------------------------------------------
/docs/how-to-install.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 🔧 How to Install
 4 | 
 5 | Via [Bundler](https://bundler.io): Add the gem `xccache` to the Gemfile of your project.
 6 | 
 7 | ```rb
 8 | gem "xccache"
 9 | ```
10 | 
11 | Via [RubyGems](https://rubygems.org):
12 | ```sh
13 | $ gem install xccache
14 | ```
15 | 
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 📝 Overview
 4 | ## Cache as xcframeworks
 5 | 
 6 | 
 7 | The `xccache` CLI provides some functionalities to build a Swift package target into an xcframework (for more details, check out the [Under the Hood](#under-the-hood) section). This xcframework can be used in the project in many ways. See more: [Declare a binary target in the package manifest](https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages#Declare-a-binary-target-in-the-package-manifest).
 8 | 
 9 | The tool manages a special *umbrella package* (at: xccache/packages/umbrella) to manipulate cache dependencies in the project. In case of cache hit, it replaces the original dependency (with source code) with the corresponding prebuilt dependency.
10 | 
11 | ### Cache Fallback
12 | In case of cache miss, it automatically uses the original dependency.
13 | 
14 | ### Cache Validation Model
15 | (1) **Checksum-based**: An xcframework is associated with a checksum of its package. If the checksum does not match -> cache miss.
16 | 
17 | 
18 | 
19 | ## Under the Hood
20 | - [Packaging as an xcframework](under-the-hood/packaging-as-xcframework.md)
21 | - [Ensuring `Bundle.module` When Accessing Resources](under-the-hood/ensuring-bundle-module.md)
22 | 
--------------------------------------------------------------------------------
/docs/res/binary_macro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/binary_macro.png
--------------------------------------------------------------------------------
/docs/res/cachemap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/cachemap.png
--------------------------------------------------------------------------------
/docs/res/kickstarter_clean_build.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/kickstarter_clean_build.png
--------------------------------------------------------------------------------
/docs/res/lockfile_add_new_dep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/lockfile_add_new_dep.png
--------------------------------------------------------------------------------
/docs/res/proxy_binary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/proxy_binary.png
--------------------------------------------------------------------------------
/docs/res/proxy_local.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/proxy_local.png
--------------------------------------------------------------------------------
/docs/res/resource_build_structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/resource_build_structure.png
--------------------------------------------------------------------------------
/docs/res/resource_bundle_module.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/resource_bundle_module.png
--------------------------------------------------------------------------------
/docs/res/resource_bundle_overriding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/resource_bundle_overriding.png
--------------------------------------------------------------------------------
/docs/res/switching_binary_to_source.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/switching_binary_to_source.gif
--------------------------------------------------------------------------------
/docs/res/switching_binary_to_source.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/switching_binary_to_source.mp4
--------------------------------------------------------------------------------
/docs/res/umbrella_product_dependencies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/umbrella_product_dependencies.png
--------------------------------------------------------------------------------
/docs/res/unknown_deps_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/unknown_deps_err.png
--------------------------------------------------------------------------------
/docs/res/xccache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/xccache.png
--------------------------------------------------------------------------------
/docs/res/xcode_process_xcframeworks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trinhngocthuyen/xccache/6e3fe288127c529a0b88c653eb02b01ad16af205/docs/res/xcode_process_xcframeworks.png
--------------------------------------------------------------------------------
/docs/troubleshooting.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](README.md)
 2 | 
 3 | # 🩺 Troubleshooting
 4 | 
 5 | ### Unknown Product Dependencies
 6 | 
 7 |  8 | 
 9 | If you encountered this issue, it's because there are some product dependencies that the tool failed to infer their packages. This case is likely to happen when having **local packages** that were not properly added to the project. Below is the instructions to resolve this issue (once only).
10 | 
11 | After seeing this error, you should see `__unknown__/` (ex. `__unknown__/DebugKit`) in the lockfile (xccache.lock).
12 | ```json
13 | "EX.xcodeproj": {
14 |   "packages": [
15 |   ],
16 |   "dependencies": {
17 |     "EX": [
18 |       "__unknown__/DebugKit" // <-- HERE
19 |     ]
20 |   }
21 | }
22 | ```
23 | This mean, the tool cannot infer the package of `DebugKit`. So, what you need to do in this lockfile is:
24 | 
25 | (1) Specify the package of that unknown product.
26 | ```json
27 | "EX.xcodeproj": {
28 |   "packages": [
29 |     {
30 |       "path_from_root": "LocalPackages/core-utils" // <-- HERE
31 |     }
32 |   ]
33 | }
34 | ```
35 | 
36 | (2) Then, replace the `__unknown__` in the dependencies by the package slug (ex. changing `__unknown__/DebugKit` to `core-utils/DebugKit`).
37 | 
38 | ```json
39 | "EX.xcodeproj": {
40 |   "packages": [
41 |     {
42 |       "path_from_root": "LocalPackages/core-utils" // <-- HERE
43 |     }
44 |   ],
45 |   "dependencies": {
46 |     "EX": [
47 |       "core-utils/DebugKit" // <-- HERE
48 |     ]
49 |   }
50 | }
51 | ```
52 | 
53 | (3) After that, run the xccache workflow again.
54 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/ensuring-bundle-module.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Ensuring `Bundle.module` When Accessing Resources
 4 | 
 5 | ### Accessing Resources in Code
 6 | If a target includes resources, Xcode creates a resource bundle, which can be accessed using `Bundle.module`. This resource bundle is then copied to the app bundle:
 7 | ```
 8 | App.app
 9 |   |-- App (binary)
10 |   |-- _.bundle
11 | ```
12 | 
13 | > [!IMPORTANT]
14 | > You are recommended to always use `Bundle.module` to access resources, and not make assumptions about the exact location of the resource bundle.\
15 | > See: https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package#Access-a-resource-in-code
16 | 
17 | ### How `Bundle.module` is Accessible
18 | 
19 | For targets having resources, Xcode or SPM build system generates an internal code for the `Bundle.module` extension. More about SPM implementation for this: [read here](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0271-package-manager-resources.md#runtime-access-to-resources-bundle).
20 | 
21 | In Xcode, you can jump-to-definition on `Bundle.module` to see the generated code in a file called `resource_bundle_accessor.swift`. This file is later compiled (into an `.o` file) and merged into the library binary, along with other `.o` files.
22 | 
23 |
 8 | 
 9 | If you encountered this issue, it's because there are some product dependencies that the tool failed to infer their packages. This case is likely to happen when having **local packages** that were not properly added to the project. Below is the instructions to resolve this issue (once only).
10 | 
11 | After seeing this error, you should see `__unknown__/` (ex. `__unknown__/DebugKit`) in the lockfile (xccache.lock).
12 | ```json
13 | "EX.xcodeproj": {
14 |   "packages": [
15 |   ],
16 |   "dependencies": {
17 |     "EX": [
18 |       "__unknown__/DebugKit" // <-- HERE
19 |     ]
20 |   }
21 | }
22 | ```
23 | This mean, the tool cannot infer the package of `DebugKit`. So, what you need to do in this lockfile is:
24 | 
25 | (1) Specify the package of that unknown product.
26 | ```json
27 | "EX.xcodeproj": {
28 |   "packages": [
29 |     {
30 |       "path_from_root": "LocalPackages/core-utils" // <-- HERE
31 |     }
32 |   ]
33 | }
34 | ```
35 | 
36 | (2) Then, replace the `__unknown__` in the dependencies by the package slug (ex. changing `__unknown__/DebugKit` to `core-utils/DebugKit`).
37 | 
38 | ```json
39 | "EX.xcodeproj": {
40 |   "packages": [
41 |     {
42 |       "path_from_root": "LocalPackages/core-utils" // <-- HERE
43 |     }
44 |   ],
45 |   "dependencies": {
46 |     "EX": [
47 |       "core-utils/DebugKit" // <-- HERE
48 |     ]
49 |   }
50 | }
51 | ```
52 | 
53 | (3) After that, run the xccache workflow again.
54 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/ensuring-bundle-module.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Ensuring `Bundle.module` When Accessing Resources
 4 | 
 5 | ### Accessing Resources in Code
 6 | If a target includes resources, Xcode creates a resource bundle, which can be accessed using `Bundle.module`. This resource bundle is then copied to the app bundle:
 7 | ```
 8 | App.app
 9 |   |-- App (binary)
10 |   |-- _.bundle
11 | ```
12 | 
13 | > [!IMPORTANT]
14 | > You are recommended to always use `Bundle.module` to access resources, and not make assumptions about the exact location of the resource bundle.\
15 | > See: https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package#Access-a-resource-in-code
16 | 
17 | ### How `Bundle.module` is Accessible
18 | 
19 | For targets having resources, Xcode or SPM build system generates an internal code for the `Bundle.module` extension. More about SPM implementation for this: [read here](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0271-package-manager-resources.md#runtime-access-to-resources-bundle).
20 | 
21 | In Xcode, you can jump-to-definition on `Bundle.module` to see the generated code in a file called `resource_bundle_accessor.swift`. This file is later compiled (into an `.o` file) and merged into the library binary, along with other `.o` files.
22 | 
23 |  24 | 
25 | With SPM (using `swift build`), you should see the generated source in `.build/debug/.build/DerivedSources` as in the following image:
26 | 
27 |
24 | 
25 | With SPM (using `swift build`), you should see the generated source in `.build/debug/.build/DerivedSources` as in the following image:
26 | 
27 |  28 | 
29 | ### Resource Bundle Inside xcframework
30 | With xccache, we want this resource bundle to reside inside the xcframework for convenience. However, when integrating this xcframework as a binary target. The app bundle structure looks like this:
31 | ```
32 | App.app
33 |   |-- App (binary)
34 |   |-- Frameworks
35 |         |-- .framework
36 |               |--  (binary)
37 |               |-- _.bundle
38 | ```
39 | 
40 | `Bundle.module` can no longer detect the resource bundle. Note that the framework binary under `Frameworks/.framework/` is codeless. So, `Bundle(for: BundleFinder.self)` does not work either.
41 | 
42 | ### Workaround: Overriding Bundle Lookup Logic
43 | A workaround for this problem is to override the lookup logic. Before combining `.o` files into the framework binary (with `libtool`), we overwrite the object file `resource_bundle_accessor.swift.o` in the build directory.
44 | This can be done by:
45 | - First generating the source with the [additional bundle lookup](/lib/xccache/assets/templates/resource_bundle_accessor.swift.template#L13)
46 | - Compiling this file with `swiftc`, for example:
47 | ```sh
48 | swiftc -emit-library \
49 |   -module-name Foo \
50 |   -target arm64-apple-ios-simulator \
51 |   -sdk  \
52 |   -o  \
53 |   resource_bundle_accessor.swift
54 | ```
55 | 
56 |
28 | 
29 | ### Resource Bundle Inside xcframework
30 | With xccache, we want this resource bundle to reside inside the xcframework for convenience. However, when integrating this xcframework as a binary target. The app bundle structure looks like this:
31 | ```
32 | App.app
33 |   |-- App (binary)
34 |   |-- Frameworks
35 |         |-- .framework
36 |               |--  (binary)
37 |               |-- _.bundle
38 | ```
39 | 
40 | `Bundle.module` can no longer detect the resource bundle. Note that the framework binary under `Frameworks/.framework/` is codeless. So, `Bundle(for: BundleFinder.self)` does not work either.
41 | 
42 | ### Workaround: Overriding Bundle Lookup Logic
43 | A workaround for this problem is to override the lookup logic. Before combining `.o` files into the framework binary (with `libtool`), we overwrite the object file `resource_bundle_accessor.swift.o` in the build directory.
44 | This can be done by:
45 | - First generating the source with the [additional bundle lookup](/lib/xccache/assets/templates/resource_bundle_accessor.swift.template#L13)
46 | - Compiling this file with `swiftc`, for example:
47 | ```sh
48 | swiftc -emit-library \
49 |   -module-name Foo \
50 |   -target arm64-apple-ios-simulator \
51 |   -sdk  \
52 |   -o  \
53 |   resource_bundle_accessor.swift
54 | ```
55 | 
56 |  57 | 
58 | The same logic applies to Objective-C, ie. generating `resource_bundle_accessor.m` and compiling it into `resource_bundle_accessor.m.o` using `clang`.
59 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/macro-as-binary.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Macro as Binary
 4 | 
 5 | This technique was inspired by the approach mentioned in [this blog post](https://www.polpiella.dev/binary-swift-macros).
 6 | 
 7 | Consider the following usage where `#hexColor` is the macro and `Wizard` is its module.
 8 | 
 9 | ```swift
10 | import Wizard
11 | 
12 | let color = #hexColor(0xff0000)
13 | ```
14 | 
15 | Such a macro consists of two targets:
16 | - A macro target containing the implementation: `.macro(WizardImpl)`
17 | - A regular target containing the interfaces/declarations `.target(Wizard)`. This target depends on the macro implementation target.
18 | 
19 | ```
20 | .target(Wizard) -> .macro(WizardImpl)
21 | ```
22 | 
23 | Here, we need prebuilt artifacts of both `Wizard` and `WizardImpl`.
24 | 
25 | Building `Wizard` is just like building any other regular target. Building `Wizard` is just like building any other regular target.
26 | Meanwhile, obtaining the tool binary from `WizardImpl` is a bit tricky. Building `WizardImpl` itself does not produce the tool binary, only the `.o` files. And combining them into the tool binary is not straightforward given many intermediate dependencies in [swift-syntax](https://github.com/swiftlang/swift-syntax). Luckily
27 | Meanwhile, obtaining the tool binary from `WizardImpl` is a bit tricky. Building `WizardImpl` itself does not produce the tool binary, only the `.o` files. And combining them into the tool binary is not straightforward given many intermediate dependencies in [swift-syntax](https://github.com/swiftlang/swift-syntax). Luckily, building `Wizard`, the associated interfaces target, does produce the tool binary at `.build/arm64-apple-macosx/debug/WizardImpl-tool`.
28 | 
29 | After having the tool binary, macro expansion can be done simply by specifying the Swift flags as follows: `-load-plugin-executable path/to/WizardImpl#WizardImpl`.
30 | 
31 |
57 | 
58 | The same logic applies to Objective-C, ie. generating `resource_bundle_accessor.m` and compiling it into `resource_bundle_accessor.m.o` using `clang`.
59 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/macro-as-binary.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Macro as Binary
 4 | 
 5 | This technique was inspired by the approach mentioned in [this blog post](https://www.polpiella.dev/binary-swift-macros).
 6 | 
 7 | Consider the following usage where `#hexColor` is the macro and `Wizard` is its module.
 8 | 
 9 | ```swift
10 | import Wizard
11 | 
12 | let color = #hexColor(0xff0000)
13 | ```
14 | 
15 | Such a macro consists of two targets:
16 | - A macro target containing the implementation: `.macro(WizardImpl)`
17 | - A regular target containing the interfaces/declarations `.target(Wizard)`. This target depends on the macro implementation target.
18 | 
19 | ```
20 | .target(Wizard) -> .macro(WizardImpl)
21 | ```
22 | 
23 | Here, we need prebuilt artifacts of both `Wizard` and `WizardImpl`.
24 | 
25 | Building `Wizard` is just like building any other regular target. Building `Wizard` is just like building any other regular target.
26 | Meanwhile, obtaining the tool binary from `WizardImpl` is a bit tricky. Building `WizardImpl` itself does not produce the tool binary, only the `.o` files. And combining them into the tool binary is not straightforward given many intermediate dependencies in [swift-syntax](https://github.com/swiftlang/swift-syntax). Luckily
27 | Meanwhile, obtaining the tool binary from `WizardImpl` is a bit tricky. Building `WizardImpl` itself does not produce the tool binary, only the `.o` files. And combining them into the tool binary is not straightforward given many intermediate dependencies in [swift-syntax](https://github.com/swiftlang/swift-syntax). Luckily, building `Wizard`, the associated interfaces target, does produce the tool binary at `.build/arm64-apple-macosx/debug/WizardImpl-tool`.
28 | 
29 | After having the tool binary, macro expansion can be done simply by specifying the Swift flags as follows: `-load-plugin-executable path/to/WizardImpl#WizardImpl`.
30 | 
31 |  32 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/packaging-as-xcframework.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Packaging as an xcframework
 4 | 
 5 | The steps to create an xcframework out of a collection of Swift sources are:
 6 | - (1) Creating a framework slice (ex. for iOS iphone simulator - `arm64-apple-ios-simulator`). The result of this step is a framework bundle, ex. `SwiftyBeaver.framework`.
 7 | - (2) Creating an xcframework out of framework slices using `xcodebuild -create-xcframework`.
 8 | 
 9 | There are some tricky actions in step (1) so that the framework bundle meets requirements in step (2). For example, in case of Swift frameworks, `xcodebuild -create-xcframework` requires a swiftinterface in the swiftmodule.
10 | 
11 | ## Creating a Framework Slice
12 | 
13 | By default, building a Swift package target (with `swift build`) does not produce a `.framework` bundle. We have to package it outselve from .o files, headers, swiftmodules, etc.
14 | 
15 | ```
16 | A.framework
17 |   |-- A (binary)
18 |   |-- Info.plist
19 |   |
20 |   |-- Headers /
21 |   |-- Modules /
22 |         |-- module.modulemap
23 |         |-- A.swiftmodule /
24 |               |-- arm64-apple-ios-simulator.swiftinterface
25 |               |-- arm64-apple-ios-simulator.swiftdoc
26 |               ...
27 | ```
28 | Steps to create a framework:
29 | - (1) Run `swift build --target A ...` to build the target. Build artifacts are stored under `.build/debug`.
30 | - (2) Create the framework binary using `libtool` from `.o` files in `.build/debug/A.build`:
31 | ```sh
32 | libtool -static -o A.framework/A .build/debug/A.build/**/*.o
33 | ```
34 | - (3) Copying swiftmodules & swiftinterfaces in `.build/debug/A.build` and `.build/debug/Modules` to `A.framework/Modules`.\
35 | Also, creating the modulemap `module.modulemap` under `A.framework/Modules` so that this framework is visible to ObjC code.
36 | - (4) Copying headers (if any) to `A.framework/Headers`.
37 | - (5) Copying the resource bundle (if any) (ex. in `.build/debug/A_A.bundle`) to the framework bundle.
38 | 
39 | ## Creating an xcframework from Framework Slices
40 | 
41 | ```sh
42 | xcodebuild -create-xcframework \
43 |   -framework arm64-apple-ios-simulator/SwiftyBeaver.framework \
44 |   -framework arm64-apple-ios/SwiftyBeaver.framework \
45 |   -output SwiftyBeaver.xcframework
46 | ```
47 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/proxy-packages.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Proxy Packages
 4 | 
 5 | The introduction of proxy packages were highlighted in this discussion: [Cache Re-design (v2)](https://github.com/trinhngocthuyen/xccache/discussions/83#discussion-8346379)
 6 | 
 7 | Each resolved package has an accompanying package called proxy package. This package has a very similar manifest to the resolved package. Both share the same checkout sources. The proxy package manifest is derived from its counterpart.
 8 | 
 9 | ```
10 | umbrella
11 | ├── .build/checkouts
12 |     ├── SwiftyBeaver / -- Package.swift
13 |     │     ├── Package.swift
14 |     │     ├── Sources
15 |     │
16 |     └── Alamofire
17 | 
18 | proxy
19 | ├── .proxies
20 |     ├── SwiftyBeaver
21 |     │     ├── Package.swift (updated)
22 |     │     ├── src (symlink to umbrella/.build/checkouts/SwiftyBeaver)
23 |     │
24 |     └── Alamofire
25 | ```
26 | Dependencies of a proxy package are proxy packages
27 | 
28 | Take Moya as an example. It depends on Alamofire, a remote git repo as follows.
29 | ```swift
30 | let package = Package(
31 |   name: "Moya",
32 |   dependencies: [
33 |     .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.0.0"))
34 |   ]
35 | )
36 | ```
37 | When translating to the proxy model, the manifest should be like this:
38 | ```swift
39 | let package = Package(
40 |   name: "Moya",
41 |   dependencies: [
42 |     .package(path: "../Alamofire")
43 |   ]
44 | )
45 | ```
46 | Proxy packages reside adjacent to each other in the directory structure.
47 | ```
48 | umbrella
49 | 
50 | proxy
51 | ├── .proxies
52 | │   ├── Alamofire
53 | │   │     └── Package.swift
54 | │   ├── Moya
55 | │   │     └── Package.swift
56 | │   └── SwiftyBeaver
57 | │         └── Package.swift
58 | │
59 | └── Package.swift (to be integrated to the project)
60 | ```
61 | 
62 | When having cache, the targets declaration in the manifest is altered to use the xcframework.
63 | ```swift
64 | let package = Package(
65 |   name: "Alamofire",
66 |   products: [
67 |     .library(
68 |       name: "Alamofire",
69 |       targets: ["Alamofire"]
70 |     ),
71 |   ],
72 |   targets: [
73 |     .binaryTarget( // <-- HERE
74 |       name: "Alamofire",
75 |       path: "../../../binaries/Alamofire/Alamofire.xcframework"
76 |     )
77 |   ]
78 | )
79 | ```
80 | 
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
 1 | # User settings
 2 | xcuserdata/
 3 | 
 4 | # Obj-C/Swift specific
 5 | *.hmap
 6 | 
 7 | # App packaging
 8 | *.ipa
 9 | *.dSYM.zip
10 | *.dSYM
11 | 
12 | # Playgrounds
13 | timeline.xctimeline
14 | playground.xcworkspace
15 | 
16 | # Swift Package Manager
17 | Packages/
18 | Package.pins
19 | Package.resolved
20 | .swiftpm
21 | 
22 | DerivedData/
23 | .build/
24 | Pods/
25 | *.xcworkspace
26 | 
27 | # fastlane
28 | fastlane/README.md
29 | fastlane/report.xml
30 | fastlane/Preview.html
31 | fastlane/screenshots/**/*.png
32 | fastlane/test_output
33 | 
34 | .spm.pods/
35 | .logs/
36 | .xcconfigs/
37 | xccache/
38 | 
--------------------------------------------------------------------------------
/examples/EX.xcodeproj/xcshareddata/xcschemes/EX.xcscheme:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 5 |    
 9 |       
10 |          
16 |             
22 |             
23 |          
24 |       
25 |    
26 |    
31 |       
32 |          
35 |          
36 |       
37 |       
38 |          
41 |             
47 |             
48 |          
49 |       
50 |    
51 |    
61 |       
63 |          
69 |          
70 |       
71 |    
72 |    
78 |       
80 |          
86 |          
87 |       
88 |    
89 |    
91 |    
92 |    
95 |    
96 | 
97 | 
--------------------------------------------------------------------------------
/examples/EX.xctestplan:
--------------------------------------------------------------------------------
 1 | {
 2 |   "configurations" : [
 3 |     {
 4 |       "id" : "DA9D28D2-8487-4286-9EC0-082D1A2FA0C3",
 5 |       "name" : "Test Scheme Action",
 6 |       "options" : {
 7 | 
 8 |       }
 9 |     }
10 |   ],
11 |   "defaultOptions" : {
12 |     "targetForVariableExpansion" : {
13 |       "containerPath" : "container:EX.xcodeproj",
14 |       "identifier" : "F577C62E2D96971200C83C96",
15 |       "name" : "EX"
16 |     }
17 |   },
18 |   "testTargets" : [
19 |     {
20 |       "parallelizable" : false,
21 |       "target" : {
22 |         "containerPath" : "container:EX.xcodeproj",
23 |         "identifier" : "F5AF0F9B2DAE09EF00AB812D",
24 |         "name" : "EXTests"
25 |       }
26 |     }
27 |   ],
28 |   "version" : 1
29 | }
30 | 
--------------------------------------------------------------------------------
/examples/EX/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 | 
--------------------------------------------------------------------------------
/examples/EX/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 | 
--------------------------------------------------------------------------------
/examples/EX/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 |   "info" : {
3 |     "author" : "xcode",
4 |     "version" : 1
5 |   }
6 | }
7 | 
--------------------------------------------------------------------------------
/examples/EX/ContentView.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | import DebugKit
 3 | import ResourceKit
 4 | 
 5 | struct ContentView: View {
 6 |   var body: some View {
 7 |     VStack {
 8 |       Text(ResourceKit.greetings() ?? "N/A").font(.title)
 9 |       Form {
10 |         Section("Resources") {
11 |           labledContent("ResourceKit.bundle", ResourceKit.bundle.relativePath)
12 |           labledContent("DebugKit.bundle", DebugKit.bundle.relativePath)
13 |           labledContent("DebugKit.token", DebugKit.loadToken())
14 |         }
15 |         .font(.footnote)
16 |       }
17 |     }
18 |   }
19 | 
20 |   private func labledContent(_ label: String, _ value: String) -> some View {
21 |     LabeledContent {
22 |       Text(value)
23 |         .multilineTextAlignment(.trailing)
24 |         .foregroundStyle(value.contains("Frameworks/") ? .green : .gray)
25 |     } label: {
26 |       Text(label)
27 |     }
28 |   }
29 | }
30 | 
31 | #Preview {
32 |   ContentView()
33 | }
34 | 
--------------------------------------------------------------------------------
/examples/EX/EX-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #include "SPMPlayground_ObjC.h"
2 | 
--------------------------------------------------------------------------------
/examples/EX/EXApp.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | 
 3 | @main
 4 | struct EXApp: App {
 5 |   var body: some Scene {
 6 |     WindowGroup {
 7 |       ContentView()
 8 |         .onAppear {
 9 |           playground()
10 |         }
11 |     }
12 |   }
13 | }
14 | 
--------------------------------------------------------------------------------
/examples/EX/Extensions.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | extension Bundle {
 4 |   var relativePath: String {
 5 |     return bundlePath
 6 |       .replacing(Bundle.main.bundlePath, with: "")
 7 |       .replacing(#/^\//#, with: "")
 8 |   }
 9 | }
10 | 
--------------------------------------------------------------------------------
/examples/EX/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 |   "info" : {
3 |     "author" : "xcode",
4 |     "version" : 1
5 |   }
6 | }
7 | 
--------------------------------------------------------------------------------
/examples/EX/SPMPlayground.swift:
--------------------------------------------------------------------------------
 1 | import SwiftyBeaver
 2 | import Moya
 3 | import Alamofire
 4 | import CoreUtils_Wrapper
 5 | import DebugKit
 6 | import ResourceKit
 7 | import Swizzler
 8 | import DisplayKit
 9 | import GoogleMaps
10 | import SDWebImage
11 | import KingfisherWebP
12 | import FirebaseCrashlytics
13 | import FacebookLogin
14 | import Wizard
15 | 
16 | func swift_playground() {
17 |   print(SwiftyBeaver.self)        // SwiftyBeaver
18 |   print(MoyaError.self)           // Moya
19 |   print(AFError.self)             // Alamofire
20 |   print(DebugKit.self)            // core-utils
21 |   print(CoreUtils_Wrapper.self)   // core-utils
22 |   print(Swizzler.self)            // core-utils
23 |   print(ResourceKit.self)         // core-utils
24 |   print(DisplayKit.self)          // core-utils
25 |   print(GMSAddress.self)          // GoogleMaps
26 |   print(SDImageCacheOptions.self) // SDWebImage)
27 |   print(WebPProcessor.self)       // KingfisherWebP
28 |   print(CrashlyticsReport.self)   // FirebaseCrashlytics
29 |   print(LoginResult.self)         // FacebookLogin
30 |   print(FBLoginButton.self)       // FBSDKLoginKit
31 | }
32 | 
33 | func macro_playground() {
34 |   print(#hexColor(0xff0000))
35 | }
36 | 
37 | func playground() {
38 |   swift_playground()
39 |   objc_playground()
40 |   macro_playground()
41 | }
42 | 
--------------------------------------------------------------------------------
/examples/EX/SPMPlayground_ObjC.h:
--------------------------------------------------------------------------------
 1 | #ifndef SPMPlayground_ObjC_h
 2 | #define SPMPlayground_ObjC_h
 3 | 
 4 | #import 
 5 | 
 6 | @interface ObjCPlayground: NSObject
 7 | 
 8 | void objc_playground(void);
 9 | 
10 | @end
11 | 
12 | #endif
13 | 
--------------------------------------------------------------------------------
/examples/EX/SPMPlayground_ObjC.m:
--------------------------------------------------------------------------------
 1 | #import "SPMPlayground_ObjC.h"
 2 | @import Foundation;
 3 | @import CoreUtils_Wrapper;
 4 | @import DebugKit;
 5 | @import ResourceKit;
 6 | 
 7 | @implementation ObjCPlayground
 8 | 
 9 | void objc_playground(void) {
10 |   check(CoreUtils_Wrapper.class);
11 |   check(DebugKit.class);
12 |   check(ResourceKit.class);
13 | }
14 | 
15 | void check(id object) {
16 |   NSLog(@"%@", object);
17 | }
18 | 
19 | @end
20 | 
--------------------------------------------------------------------------------
/examples/EXMac/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 | 
--------------------------------------------------------------------------------
/examples/EXMac/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "images" : [
 3 |     {
 4 |       "idiom" : "mac",
 5 |       "scale" : "1x",
 6 |       "size" : "16x16"
 7 |     },
 8 |     {
 9 |       "idiom" : "mac",
10 |       "scale" : "2x",
11 |       "size" : "16x16"
12 |     },
13 |     {
14 |       "idiom" : "mac",
15 |       "scale" : "1x",
16 |       "size" : "32x32"
17 |     },
18 |     {
19 |       "idiom" : "mac",
20 |       "scale" : "2x",
21 |       "size" : "32x32"
22 |     },
23 |     {
24 |       "idiom" : "mac",
25 |       "scale" : "1x",
26 |       "size" : "128x128"
27 |     },
28 |     {
29 |       "idiom" : "mac",
30 |       "scale" : "2x",
31 |       "size" : "128x128"
32 |     },
33 |     {
34 |       "idiom" : "mac",
35 |       "scale" : "1x",
36 |       "size" : "256x256"
37 |     },
38 |     {
39 |       "idiom" : "mac",
40 |       "scale" : "2x",
41 |       "size" : "256x256"
42 |     },
43 |     {
44 |       "idiom" : "mac",
45 |       "scale" : "1x",
46 |       "size" : "512x512"
47 |     },
48 |     {
49 |       "idiom" : "mac",
50 |       "scale" : "2x",
51 |       "size" : "512x512"
52 |     }
53 |   ],
54 |   "info" : {
55 |     "author" : "xcode",
56 |     "version" : 1
57 |   }
58 | }
59 | 
--------------------------------------------------------------------------------
/examples/EXMac/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 |   "info" : {
3 |     "author" : "xcode",
4 |     "version" : 1
5 |   }
6 | }
7 | 
--------------------------------------------------------------------------------
/examples/EXMac/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 | 
--------------------------------------------------------------------------------
/examples/EXMac/EXMac.entitlements:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 5 | 	com.apple.security.app-sandbox
 6 | 	
 7 | 	com.apple.security.files.user-selected.read-only
 8 | 	
 9 | 
10 | 
11 | 
--------------------------------------------------------------------------------
/examples/EXMac/EXMacApp.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | import SwiftyBeaver
 3 | 
 4 | @main
 5 | struct EXMacApp: App {
 6 |   var body: some Scene {
 7 |     WindowGroup {
 8 |       ContentView()
 9 |     }
10 |   }
11 | }
12 | 
--------------------------------------------------------------------------------
/examples/EXTests/ResourceTests.swift:
--------------------------------------------------------------------------------
 1 | import Testing
 2 | import DebugKit
 3 | import ResourceKit
 4 | 
 5 | struct PkgResourceTests {
 6 |   @Test func checkPkgResources() {
 7 |     #expect(ResourceKit.greetings() == "Hi from xccache!")
 8 |     #expect(DebugKit.loadToken() == "12345")
 9 |   }
10 | }
11 | 
--------------------------------------------------------------------------------
/examples/EXTests/SPMPlayground.swift:
--------------------------------------------------------------------------------
 1 | import DebugKit
 2 | import ResourceKit
 3 | import TestKit
 4 | 
 5 | func playground() {
 6 |   print(DebugKit.self)      // DebugKit
 7 |   print(ResourceKit.self)   // ResourceKit
 8 |   print(TestKit.self)       // TestKit
 9 |   print(BaseTestCase.self)  // TestKit
10 | }
11 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Package.swift:
--------------------------------------------------------------------------------
 1 | // swift-tools-version: 6.0
 2 | 
 3 | import PackageDescription
 4 | 
 5 | let package = Package(
 6 |   name: "CoreUtils",
 7 |   platforms: [.iOS(.v17), .macOS(.v13)],
 8 |   products: [
 9 |     .library(name: "Swizzler", targets: ["Swizzler"]),
10 |     .library(name: "ResourceKit", targets: ["ResourceKit"]),
11 |     .library(name: "DebugKit", targets: ["DebugKit"]),
12 |     .library(name: "DisplayKit", targets: ["DisplayKit"]),
13 |     .library(name: "TestKit", targets: ["TestKit"]),
14 |   ],
15 |   dependencies: [
16 |     .package(url: "https://github.com/SwiftyBeaver/SwiftyBeaver.git", .upToNextMajor(from: "2.1.1")),
17 |     .package(url: "https://github.com/Moya/Moya", .upToNextMajor(from: "15.0.3")),
18 |     .package(path: "../wizard"),
19 |   ],
20 |   targets: [
21 |     .target(
22 |       name: "Swizzler",
23 |       dependencies: [
24 |         "CoreUtils-Wrapper"
25 |       ]
26 |     ),
27 |     .target(
28 |       name: "ResourceKit",
29 |       dependencies: ["CoreUtils-Wrapper"],
30 |       resources: [.copy("greetings.txt")]
31 |     ),
32 |     .target(
33 |       name: "DebugKit",
34 |       dependencies: [
35 |         "CoreUtils-Wrapper",
36 |         "Swizzler",
37 |         .product(name: "SwiftyBeaver", package: "SwiftyBeaver"),
38 |         .product(name: "Moya", package: "Moya"),
39 |       ],
40 |       path: "Sources/DebugKitObjC",
41 |       resources: [.copy("token.txt")],
42 |       publicHeadersPath: "Headers",
43 |       cSettings: [
44 |         .headerSearchPath("PrivateHeaders"),
45 |       ]
46 |     ),
47 |     .target(
48 |       name: "DisplayKit",
49 |       dependencies: [
50 |         .product(name: "Wizard", package: "wizard"),
51 |       ]
52 |     ),
53 |     .target(
54 |       name: "CoreUtils-Wrapper",
55 |       path: "Sources/Core"
56 |     ),
57 |     .target(name: "TestKit"),
58 |   ]
59 | )
60 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/Core/dummy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | 
3 | @objc public class CoreUtils_Wrapper: NSObject { }
4 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/DebugKit.m:
--------------------------------------------------------------------------------
 1 | #import 
 2 | #import 
 3 | #import 
 4 | #import 
 5 | 
 6 | @implementation DebugKit
 7 | + (NSBundle *)bundle {
 8 |   return SWIFTPM_MODULE_BUNDLE;
 9 | }
10 | + (NSString *)loadToken {
11 |   NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
12 |   NSString *tokenPath = [bundle pathForResource:@"token" ofType:@"txt"];
13 |   NSString *content = [NSString stringWithContentsOfFile:tokenPath encoding:NSUTF8StringEncoding error:nil];
14 |   return [content stringByReplacingOccurrencesOfString:@"\n" withString:@""];
15 | }
16 | + (void)diagnose {
17 |   [Swizzler swizzle:@"foo" with:@"bar" forClass:DebugKit.class];
18 |   [Diagnoser diagnoseDevice];
19 | }
20 | @end
21 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/Diagnose.m:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | @implementation Diagnoser
 4 | 
 5 | + (void)diagnoseDevice {
 6 |   NSLog(@"Diagnosing device...");
 7 | }
 8 | 
 9 | @end
10 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/Headers/DebugKit.h:
--------------------------------------------------------------------------------
 1 | #import 
 2 | // 👇 expect xccache to convert to nested angle-bracket style `#import `
 3 | #import 
 4 | 
 5 | NS_ASSUME_NONNULL_BEGIN
 6 | 
 7 | @interface DebugKit: NSObject
 8 | @property (class, nonatomic, readonly, strong) NSBundle *bundle;
 9 | + (NSString *)loadToken;
10 | + (void)diagnose;
11 | @end
12 | 
13 | NS_ASSUME_NONNULL_END
14 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/PrivateHeaders/Diagnose.h:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | NS_ASSUME_NONNULL_BEGIN
 4 | 
 5 | @interface Diagnoser: NSObject
 6 | + (void)diagnoseDevice;
 7 | @end
 8 | 
 9 | NS_ASSUME_NONNULL_END
10 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/token.txt:
--------------------------------------------------------------------------------
1 | 12345
2 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DisplayKit/DisplayKit.swift:
--------------------------------------------------------------------------------
1 | import Wizard
2 | 
3 | public class DisplayKit {
4 |   public static let baseColor = #hexColor(0xff0000)
5 | }
6 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/ResourceKit/ResourceKit.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | @objc public class ResourceKit: NSObject {
 4 |   public static let bundle = Bundle.module
 5 |   public static func greetings() -> String? {
 6 |     guard let url = Bundle.module.url(forResource: "greetings", withExtension: "txt"),
 7 |           let content = try? String(contentsOf: url, encoding: .utf8)
 8 |     else { return nil }
 9 |     return content.replacing(#/\s*$/#, with: "") // Strip trailing spaces
10 |   }
11 | }
12 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/ResourceKit/greetings.txt:
--------------------------------------------------------------------------------
1 | Hi from xccache!
2 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/Swizzler/Swizzler.m:
--------------------------------------------------------------------------------
1 | #import "Swizzler.h"
2 | 
3 | @implementation Swizzler
4 | + (void)swizzle:(NSString* )m1 with:(NSString *)m2 forClass:(Class)cls {
5 |   NSLog(@"Swizzle %@ with %@", m1, m2);
6 | }
7 | @end
8 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/Swizzler/include/Swizzler.h:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | NS_ASSUME_NONNULL_BEGIN
 4 | 
 5 | @interface Swizzler: NSObject
 6 | + (void)swizzle:(NSString* )m1 with:(NSString *)m2 forClass:(Class)cls;
 7 | @end
 8 | 
 9 | NS_ASSUME_NONNULL_END
10 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/TestKit/TestKit.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | import XCTest
3 | 
4 | @objc public class TestKit: NSObject { }
5 | 
6 | public class BaseTestCase: XCTestCase { }
7 | public protocol BaseTrait: Trait { }
8 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Package.swift:
--------------------------------------------------------------------------------
 1 | // swift-tools-version: 6.0
 2 | 
 3 | import PackageDescription
 4 | import CompilerPluginSupport
 5 | 
 6 | let package = Package(
 7 |   name: "Wizard",
 8 |   platforms: [.iOS(.v17), .macOS(.v13)],
 9 |   products: [
10 |     .library(
11 |       name: "Wizard",
12 |       targets: ["Wizard"]
13 |     ),
14 |     .executable(
15 |       name: "WizardPlayground",
16 |       targets: ["WizardPlayground"]
17 |     ),
18 |   ],
19 |   dependencies: [
20 |     .package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.0"),
21 |   ],
22 |   targets: [
23 |     .macro(
24 |       name: "WizardImpl",
25 |       dependencies: [
26 |         .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
27 |         .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
28 |       ]
29 |     ),
30 |     .target(name: "Wizard", dependencies: ["WizardImpl"]),
31 |     .executableTarget(name: "WizardPlayground", dependencies: ["Wizard"]),
32 |   ]
33 | )
34 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/Wizard/Wizard.swift:
--------------------------------------------------------------------------------
 1 | #if canImport(UIKit)
 2 | import UIKit
 3 | public typealias XColor = UIColor
 4 | 
 5 | #elseif canImport(AppKit)
 6 | import AppKit
 7 | public typealias XColor = NSColor
 8 | 
 9 | #else
10 | #error("Only usable with UIKit or AppKit")
11 | #endif
12 | 
13 | @freestanding(expression)
14 | public macro hexColor(_ intLiteral: IntegerLiteralType ) -> XColor = #externalMacro(module: "WizardImpl", type: "HexColorMacro")
15 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/WizardImpl/HexColorMacro.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | import SwiftCompilerPlugin
 3 | import SwiftSyntax
 4 | import SwiftSyntaxBuilder
 5 | import SwiftSyntaxMacros
 6 | 
 7 | enum HexColorMacroError: Error {
 8 |   case notFoundHex
 9 | }
10 | 
11 | public struct HexColorMacro: ExpressionMacro {
12 |   public static func expansion(
13 |     of node: some FreestandingMacroExpansionSyntax,
14 |     in context: some MacroExpansionContext
15 |   ) throws -> ExprSyntax {
16 |     guard let arg = node.arguments.first,
17 |           let expr = arg.expression.as(IntegerLiteralExprSyntax.self)
18 |     else {
19 |       throw HexColorMacroError.notFoundHex
20 |     }
21 |     let hex = expr.literal.text
22 |     return """
23 |     XColor(
24 |       red: .init((\(raw: hex) >> 16) & 0xff) / 255,
25 |       green: .init((\(raw: hex) >> 8) & 0xff) / 255,
26 |       blue: .init((\(raw: hex) >> 0) & 0xff) / 255,
27 |       alpha: 1.0
28 |     )
29 |     """
30 |   }
31 | }
32 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/WizardImpl/WizardImpl.swift:
--------------------------------------------------------------------------------
 1 | import SwiftCompilerPlugin
 2 | import SwiftSyntax
 3 | import SwiftSyntaxBuilder
 4 | import SwiftSyntaxMacros
 5 | 
 6 | @main
 7 | struct HexColorMacroPlugin: CompilerPlugin {
 8 |   let providingMacros: [Macro.Type] = [
 9 |     HexColorMacro.self,
10 |   ]
11 | }
12 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/WizardPlayground/main.swift:
--------------------------------------------------------------------------------
1 | import Wizard
2 | 
3 | let color = #hexColor(0xff0000)
4 | print(color)
5 | 
--------------------------------------------------------------------------------
/examples/Makefile:
--------------------------------------------------------------------------------
 1 | XCCACHE_ARGS := --verbose
 2 | 
 3 | format:
 4 | 	cd .. && make format
 5 | 
 6 | cache.use:
 7 | 	bundle exec xccache use $(XCCACHE_ARGS)
 8 | 
 9 | cache.build:
10 | 	bundle exec xccache build $(TARGET) $(XCCACHE_ARGS)
11 | 
12 | cache.init:
13 | 	bundle exec xccache init $(XCCACHE_ARGS)
14 | 
15 | cache.rollback:
16 | 	bundle exec xccache rollback $(XCCACHE_ARGS)
17 | 
18 | cache.viz:
19 | 	bundle exec xccache viz --out=xccache $(XCCACHE_ARGS)
20 | 
21 | build:
22 | 	cicd ios build
23 | 
24 | test:
25 | 	cicd ios test
26 | 
27 | check:
28 | 	rm -rf xccache/packages/umbrella/.build xccache/cachemap.json
29 | 	make cache.build build
30 | 
31 | ex.%:
32 | 	make $*
33 | 
--------------------------------------------------------------------------------
/examples/config.xcconfig:
--------------------------------------------------------------------------------
1 | #include? ".xcconfigs/hook.xcconfig"
2 | 
--------------------------------------------------------------------------------
/examples/xccache.lock:
--------------------------------------------------------------------------------
 1 | {
 2 |   "EX.xcodeproj": {
 3 |     "packages": [
 4 |       {
 5 |         "path_from_root": "LocalPackages/core-utils"
 6 |       },
 7 |       {
 8 |         "path_from_root": "LocalPackages/wizard"
 9 |       },
10 |       {
11 |         "repositoryURL": "https://github.com/facebook/facebook-ios-sdk",
12 |         "requirement": {
13 |           "kind": "upToNextMajorVersion",
14 |           "minimumVersion": "9.0.0"
15 |         }
16 |       },
17 |       {
18 |         "repositoryURL": "https://github.com/firebase/firebase-ios-sdk",
19 |         "requirement": {
20 |           "kind": "upToNextMajorVersion",
21 |           "minimumVersion": "11.4.0"
22 |         }
23 |       },
24 |       {
25 |         "repositoryURL": "https://github.com/googlemaps/ios-maps-sdk",
26 |         "requirement": {
27 |           "kind": "upToNextMajorVersion",
28 |           "minimumVersion": "9.4.0"
29 |         }
30 |       },
31 |       {
32 |         "repositoryURL": "https://github.com/Moya/Moya",
33 |         "requirement": {
34 |           "kind": "upToNextMajorVersion",
35 |           "minimumVersion": "15.0.3"
36 |         }
37 |       },
38 |       {
39 |         "repositoryURL": "https://github.com/SDWebImage/SDWebImage",
40 |         "requirement": {
41 |           "kind": "upToNextMajorVersion",
42 |           "minimumVersion": "5.21.0"
43 |         }
44 |       },
45 |       {
46 |         "repositoryURL": "https://github.com/SnapKit/SnapKit",
47 |         "requirement": {
48 |           "kind": "upToNextMajorVersion",
49 |           "minimumVersion": "5.7.1"
50 |         }
51 |       },
52 |       {
53 |         "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver.git",
54 |         "requirement": {
55 |           "kind": "upToNextMajorVersion",
56 |           "minimumVersion": "2.1.1"
57 |         }
58 |       },
59 |       {
60 |         "repositoryURL": "https://github.com/yeatse/KingfisherWebP",
61 |         "requirement": {
62 |           "kind": "upToNextMajorVersion",
63 |           "minimumVersion": "1.6.0"
64 |         }
65 |       }
66 |     ],
67 |     "dependencies": {
68 |       "EX": [
69 |         "core-utils/DebugKit",
70 |         "core-utils/DisplayKit",
71 |         "core-utils/ResourceKit",
72 |         "core-utils/Swizzler",
73 |         "facebook-ios-sdk/FacebookLogin",
74 |         "firebase-ios-sdk/FirebaseCrashlytics",
75 |         "ios-maps-sdk/GoogleMaps",
76 |         "KingfisherWebP/KingfisherWebP",
77 |         "Moya/Moya",
78 |         "SDWebImage/SDWebImage",
79 |         "SnapKit/SnapKit-Dynamic",
80 |         "SwiftyBeaver/SwiftyBeaver",
81 |         "wizard/Wizard"
82 |       ],
83 |       "EXTests": [
84 |         "core-utils/TestKit"
85 |       ],
86 |       "EXMac": [
87 |         "SwiftyBeaver/SwiftyBeaver"
88 |       ]
89 |     },
90 |     "platforms": {
91 |       "ios": "17.6",
92 |       "osx": "15.4"
93 |     }
94 |   }
95 | }
--------------------------------------------------------------------------------
/examples/xccache.yml:
--------------------------------------------------------------------------------
 1 | # ignore_local: true
 2 | ignore: []
 3 | default_sdk: iphonesimulator
 4 | remote:
 5 |   # debug:
 6 |   #   git: https://github.com/trinhngocthuyen/.cache.git
 7 |   # release:
 8 |   #   s3:
 9 |   #     uri: "s3://xccache/release"
10 | 
--------------------------------------------------------------------------------
/lib/xccache.rb:
--------------------------------------------------------------------------------
1 | require "xccache/main"
2 | 
3 | module XCCache
4 |   ROOT = Pathname(__dir__).parent
5 |   LIBEXEC = ROOT / "libexec"
6 | end
7 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/cachemap.html.template:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   
 6 |   Cachemap Visualization
 7 |   
 8 |   
 9 |   
10 |   
11 |   
12 |   
13 | 
14 | 
15 |   
50 |   
51 |   
54 | 
55 | 
56 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/cachemap.js.template:
--------------------------------------------------------------------------------
 1 | const graph = JSON.parse(`
 2 | <%= json %>
 3 | `);
 4 | 
 5 | // ------------------------------------------------
 6 | 
 7 | const COLORS = {
 8 |   'hit': '#339966',
 9 |   'missed': '#ff6f00',
10 |   'ignored': '#888',
11 |   'NA': '#888',
12 | }
13 | const cy = cytoscape({
14 |   container: $('#cy'),
15 |   elements: ([...graph.nodes, ...graph.edges]).map(x => ({data: x})),
16 |   style: [
17 |     {
18 |       selector: 'node',
19 |       style: {
20 |         'label': (e) => e.id().split("/")[1],
21 |         'color': '#fff',
22 |         'text-valign': 'center',
23 |         'text-halign': 'center',
24 |         'font-size': '14px',
25 |         'shape': 'roundrectangle',
26 |         'width': (e) => Math.max(50, e.id().split('/')[1].length * 8),
27 |         'background-color': (e) => COLORS[e.data('cache') || 'NA'],
28 |       }
29 |     },
30 |     {
31 |       selector: 'node:selected',
32 |       style: {
33 |         'font-weight': 'bold',
34 |         'border-width': 3,
35 |         'border-color': '#333',
36 |       }
37 |     },
38 |     {
39 |       selector: 'node[type="agg"]',
40 |       style: {
41 |         'background-color': '#333',
42 |       }
43 |     },
44 |     {
45 |       selector: 'edge',
46 |       style: {
47 |         'width': 1,
48 |         'target-arrow-shape': 'triangle',
49 |         'curve-style': 'bezier',
50 |         'line-color': '#ccc',
51 |         'target-arrow-color': '#ccc',
52 |       }
53 |     },
54 |   ],
55 |   layout: {
56 |     name: 'fcose',
57 |     animationDuration: 200,
58 |     nodeRepulsion: 10000,
59 |     idealEdgeLength: 120,
60 |     gravity: 0.25,
61 |   }
62 | });
63 | 
64 | cy.on('select', 'node', function(event) {
65 |   const node = event.target;
66 |   node.displayDetails();
67 |   node.neighborhood().add(node).focus();
68 | });
69 | 
70 | cy.on('tap', function(event) {
71 |   if (event.target == cy) {
72 |     $('.node-info').css('display', 'none');
73 |     cy.elements().animateStyle({'opacity': 1, 'line-color': '#ccc', 'target-arrow-color': '#ccc'});
74 |   }
75 | });
76 | 
77 | // -----------------------------------------------------------------
78 | 
79 | cytoscape('collection', 'animateStyle', function(style) {
80 |   this.animate({style: style, duration: 200, easing: 'ease-out'})
81 | });
82 | cytoscape('collection', 'focus', function() {
83 |   this.animateStyle({'opacity': 1, 'line-color': '#666', 'target-arrow-color': '#666'});
84 |   cy.elements().not(this).animateStyle({'opacity': 0.15, 'line-color': '#ccc', 'target-arrow-color': '#ccc'});
85 | });
86 | cytoscape('collection', 'displayDetails', function() {
87 |   $('.node-info').css('display', 'block');
88 |   const info = $('.node-info .info');
89 |   info.find('.target').html(this.id());
90 |   info.find('.checksum').html(this.data('checksum') || 'NA');
91 |   info.find('.binary')
92 |     .html((this.data('binary') || 'NA').split('/').slice(-1))
93 |     .attr({'href': this.data('binary') || ''});
94 |   info.find('.others').html(`Node degree: ${this.degree()} (${this.indegree()} in, ${this.outdegree()} out)`);
95 | });
96 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/cachemap.style.css.template:
--------------------------------------------------------------------------------
 1 | :root {
 2 |   --primary-color: #1492A0;
 3 |   --bg-color: color-mix(in srgb, var(--primary-color), white 80%);
 4 | }
 5 | body {
 6 |   font-family: Helvetica, Arial, sans-serif;
 7 |   font-size: 12px;
 8 |   margin: 0;
 9 |   line-height: 1.6;
10 | }
11 | a { color: var(--primary-color) }
12 | a:hover { color: #339966; }
13 | .fa-solid { color: var(--primary-color) }
14 | .container {
15 |   display: flex;
16 |   height: 100vh;
17 | }
18 | #cy {
19 |   flex: 1;
20 | }
21 | #sidebar {
22 |   position: relative;
23 |   background-color: var(--bg-color);
24 |   width: 250px;
25 |   transition: all 0.3s ease;
26 | }
27 | .sidebar-content {
28 |   width: calc(250px - 32px);
29 |   padding: 16px;
30 |   transform: translateX(0px);
31 |   transition: all 0.3s ease;
32 | }
33 | #sidebar.collapsed {
34 |   width: 0;
35 | }
36 | #sidebar.collapsed .sidebar-content{
37 |   transform: translateX(-250px);
38 | }
39 | #sidebar.collapsed .toggle-btn {
40 |   right: -36px;
41 |   transform: rotate(180deg);
42 | }
43 | .toggle-btn {
44 |   position: absolute;
45 |   top: 20px;
46 |   right: 20px;
47 |   z-index: 999;
48 |   cursor: pointer;
49 |   width: 16px;
50 |   height: 16px;
51 |   fill: var(--primary-color);
52 |   transition: all 0.3s;
53 | }
54 | #sidebar .title {
55 |   color: var(--primary-color);
56 |   font-size: 16px;
57 |   margin-top: 0;
58 | }
59 | #sidebar section {
60 |   padding: 16px 0;
61 | }
62 | #sidebar .section-header {
63 |   color: color-mix(in srgb, var(--primary-color), grey 20%);
64 |   font-weight: bold;
65 |   margin-block-end: 4px;
66 | }
67 | .node-info {
68 |   display: none;
69 | }
70 | .metadata .info {
71 |   font-size: 10px;
72 | }
73 | .info {
74 |   color: #888;
75 | }
76 | .info .value {
77 |   color: #666;
78 | }
79 | .footnote {
80 |   color: #888;
81 |   position: absolute;
82 |   left: 16px;
83 |   bottom: 8px;
84 | }
85 | .node {
86 |   border-radius: 3px;
87 |   padding: 1px 3px;
88 |   color: white;
89 |   background-color: var(--color)
90 | }
91 | .desc { color: var(--color) }
92 | .hit { --color: #339966 }
93 | .missed { --color: #ff6f00 }
94 | .ignored { --color: #888 }
95 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/framework.info.plist.template:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 5 | 	AvailableLibraries
 6 | 	
 7 | 		
 8 | 			BinaryPath
 9 | 			<%= module_name %>.framework/<%= module_name %>
10 | 			LibraryPath
11 | 			<%= module_name %>.framework
12 | 		
13 | 	
14 |   CFBundleExecutable
15 | 	<%= module_name %>
16 |   CFBundleName
17 |   <%= module_name %>
18 |   CFBundleIdentifier
19 |   com.xccache.<%= module_name %>
20 | 	CFBundlePackageType
21 | 	XFWK
22 | 	XCFrameworkFormatVersion
23 | 	1.0
24 | 
25 | 
26 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/framework.modulemap.template:
--------------------------------------------------------------------------------
1 | framework module <%= module_name %> {
2 |   umbrella header "<%= target %>-umbrella.h"
3 | 
4 |   export *
5 |   module * { export * }
6 | }
7 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/resource_bundle_accessor.m.template:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | @interface BundleFinder_<%= module_name %> : NSObject
 4 | @end
 5 | 
 6 | @implementation BundleFinder_<%= module_name %>
 7 | @end
 8 | 
 9 | NSBundle* <%= module_name %>_SWIFTPM_MODULE_BUNDLE() {
10 |   NSString *bundleName = @"<%= pkg %>_<%= target %>";
11 |   NSArray *candidates = @[
12 |     NSBundle.mainBundle.resourceURL,
13 |     [NSBundle bundleForClass:[BundleFinder_<%= module_name %> class]].resourceURL,
14 |     NSBundle.mainBundle.bundleURL,
15 |     [NSBundle.mainBundle.bundleURL URLByAppendingPathComponent:@"Frameworks/<%= target %>.framework"]
16 |   ];
17 | 
18 |   for (NSURL *candidate in candidates) {
19 |     NSURL *bundlePath = [candidate URLByAppendingPathComponent:[bundleName stringByAppendingString:@".bundle"]];
20 |     NSBundle *bundle = [NSBundle bundleWithURL:bundlePath];
21 |     if (bundle) {
22 |       return bundle;
23 |     }
24 |   }
25 |   [NSException raise:NSInternalInconsistencyException format:@"Unable to find bundle named %@", bundleName];
26 |   return nil;
27 | }
28 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/resource_bundle_accessor.swift.template:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | private class BundleFinder {}
 4 | 
 5 | extension Bundle {
 6 |   @available(iOS 8.0, *)
 7 |   static let module: Bundle = {
 8 |     let bundleName = "<%= pkg %>_<%= target %>"
 9 |     let candidates = [
10 |       Bundle.main.resourceURL,
11 |       Bundle(for: BundleFinder.self).resourceURL,
12 |       Bundle.main.bundleURL,
13 |       Bundle.main.bundleURL.appendingPathComponent("Frameworks/<%= target %>.framework")
14 |     ]
15 | 
16 |     for candidate in candidates {
17 |       let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
18 |       if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
19 |         return bundle
20 |       }
21 |     }
22 |     fatalError("unable to find bundle named \(bundleName)")
23 |   }()
24 | }
25 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/xccache.yml.template:
--------------------------------------------------------------------------------
1 | # Check out this doc for details
2 | # https://github.com/trinhngocthuyen/xccache/blob/main/docs/configuration.md
3 | 
--------------------------------------------------------------------------------
/lib/xccache/cache/cachemap.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core"
 2 | 
 3 | module XCCache
 4 |   module Cache
 5 |     class Cachemap < JSONRepresentable
 6 |       def depgraph_data
 7 |         raw["depgraph"] ||= {}
 8 |       end
 9 | 
10 |       def cache_data
11 |         raw["cache"] ||= {}
12 |       end
13 | 
14 |       def missed?(name)
15 |         missed.include?(name)
16 |       end
17 | 
18 |       def missed
19 |         get_cache_data(:missed)
20 |       end
21 | 
22 |       def stats
23 |         %i[hit missed ignored].to_h do |type|
24 |           count, total_count = get_cache_data(type).count, cache_data.count
25 |           percent = total_count.positive? ? (count.to_f * 100 / total_count).round : 0
26 |           [type, "#{percent}% (#{count}/#{total_count})"]
27 |         end
28 |       end
29 | 
30 |       def print_stats
31 |         verbose = Config.instance.verbose?
32 |         colors = { :hit => "green", :missed => "yellow" }
33 |         descs = %i[hit missed ignored].to_h do |type|
34 |           colorize = proc { |s| colors.key?(type) ? s.send(colors[type]).dark : s.dark }
35 |           items = get_cache_data(type)
36 |           percent = cache_data.count.positive? ? items.count.to_f / cache_data.count * 100 : 0
37 |           desc = "#{type} #{percent.round}% (#{items.count}/#{cache_data.count})"
38 |           desc = desc.capitalize if verbose
39 |           desc = "#{desc} #{colorize.call(items.to_s)}" if verbose && !items.empty?
40 |           [type, desc]
41 |         end
42 |         if verbose
43 |           UI.info <<~DESC
44 |             -------------------------------------------------------------------
45 |             Cache stats
46 |             #{descs.values.map { |s| "• #{s}" }.join("\n")}
47 |             -------------------------------------------------------------------
48 |           DESC
49 |         else
50 |           UI.info <<~DESC
51 |             -------------------------------------------------------------------
52 |             Cache stats: #{descs.values.join(', ')}
53 |             To see the full stats, use --verbose in the xccache command
54 |             -------------------------------------------------------------------
55 |           DESC
56 |         end
57 |       end
58 | 
59 |       def get_cache_data(type)
60 |         cache_data.select { |_, v| v == type }.keys
61 |       end
62 | 
63 |       def update_from_graph(graph)
64 |         cache_data =
65 |           graph["cache"]
66 |           .reject { |k, _| k.end_with?(".xccache") }
67 |           .to_h do |k, v|
68 |             next [k, :hit] if v
69 |             next [k, :ignored] if Config.instance.ignore?(k)
70 |             [k, :missed]
71 |           end
72 | 
73 |         deps = graph["deps"]
74 |         edges = deps.flat_map { |k, xs| xs.map { |v| { :source => k, :target => v } } }
75 |         nodes = deps.keys.map do |k|
76 |           {
77 |             :id => k,
78 |             :cache => cache_data[k],
79 |             :type => ("agg" if k.end_with?(".xccache")),
80 |             :binary => graph["cache"][k],
81 |           }
82 |         end
83 |         self.raw = {
84 |           "cache" => cache_data,
85 |           "depgraph" => { "nodes" => nodes, "edges" => edges },
86 |         }
87 |         save
88 |         print_stats
89 |       end
90 |     end
91 |   end
92 | end
93 | 
--------------------------------------------------------------------------------
/lib/xccache/command.rb:
--------------------------------------------------------------------------------
 1 | require "claide"
 2 | require "xccache/core/config"
 3 | require "xccache/swift/sdk"
 4 | 
 5 | module XCCache
 6 |   class Command < CLAide::Command
 7 |     include Config::Mixin
 8 |     Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 9 | 
10 |     self.abstract_command = true
11 |     self.default_subcommand = "use"
12 |     self.summary = "xccache - a build caching tool"
13 | 
14 |     attr_reader :install_options, :build_options
15 | 
16 |     def initialize(argv)
17 |       super
18 |       set_ansi_mode
19 |       config.verbose = verbose unless verbose.nil?
20 |       config.install_config = argv.option("config", "debug")
21 |       @install_options = {
22 |         :sdks => str_to_sdks(argv.option("sdk")),
23 |         :config => config.install_config,
24 |       }
25 |       @build_options = {
26 |         **@install_options,
27 |         :log_dir => argv.option("log-dir"),
28 |         :recursive => argv.flag?("recursive"),
29 |         :merge_slices => argv.flag?("merge-slices", true),
30 |         :library_evolution => argv.flag?("library-evolution"),
31 |       }
32 |     end
33 | 
34 |     def str_to_sdks(str)
35 |       (str || config.default_sdk).split(",").map { |s| Swift::Sdk.new(s) }
36 |     end
37 | 
38 |     private
39 | 
40 |     def set_ansi_mode
41 |       config.ansi = ansi_output?
42 |       return if ansi_output?
43 |       Colored2.disable!
44 |       String.send(:define_method, :colorize) { |s, _| s }
45 |     end
46 |   end
47 | end
48 | 
--------------------------------------------------------------------------------
/lib/xccache/command/base.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Options
 6 |       SDK = ["--sdk=foo,bar", "SDKs (iphonesimulator, iphoneos, macos, etc.)"].freeze
 7 |       CONFIG = ["--config=foo", "Configuration (debug, release) (default: debug)"].freeze
 8 |       LOG_DIR = ["--log-dir=foo", "Build log directory"].freeze
 9 |       MERGE_SLICES = [
10 |         "--merge-slices/--no-merge-slices",
11 |         "Whether to merge with existing slices/sdks in the xcframework (default: true)",
12 |       ].freeze
13 |       LIBRARY_EVOLUTION = [
14 |         "--library-evolution/--no-library-evolution",
15 |         "Whether to enable library evolution (build for distribution) (default: false)",
16 |       ].freeze
17 | 
18 |       def self.install_options
19 |         [SDK, CONFIG]
20 |       end
21 | 
22 |       def self.build_options
23 |         install_options + [LOG_DIR, MERGE_SLICES, LIBRARY_EVOLUTION]
24 |       end
25 |     end
26 |   end
27 | end
28 | 
--------------------------------------------------------------------------------
/lib/xccache/command/build.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Build < Command
 7 |       self.summary = "Build packages to xcframeworks"
 8 |       def self.options
 9 |         [
10 |           *Options.build_options,
11 |           ["--recursive", "Whether to build their recursive targets if cache-missed (default: false)"],
12 |         ].concat(super)
13 |       end
14 |       self.arguments = [
15 |         CLAide::Argument.new("TARGET", false, true),
16 |       ]
17 | 
18 |       def initialize(argv)
19 |         super
20 |         @targets = argv.arguments!
21 |       end
22 | 
23 |       def run
24 |         Installer::Build.new(ctx: self, targets: @targets).install!
25 |       end
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/command/cache.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | require "xccache/command/cache/clean"
 3 | require "xccache/command/cache/list"
 4 | 
 5 | module XCCache
 6 |   class Command
 7 |     class Cache < Command
 8 |       self.abstract_command = true
 9 |       self.summary = "Working with cache (list, clean, etc.)"
10 |     end
11 |   end
12 | end
13 | 
--------------------------------------------------------------------------------
/lib/xccache/command/cache/clean.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Command
 3 |     class Cache < Command
 4 |       class Clean < Cache
 5 |         self.summary = "Cleaning/purging cache"
 6 |         self.arguments = [CLAide::Argument.new("TARGET", false, true)]
 7 |         def self.options
 8 |           [
 9 |             ["--all", "Whether to remove all cache (default: false)"],
10 |             ["--dry", "Dry run - don't remove cache, just show what shall be removed (default: false)"],
11 |           ].concat(super)
12 |         end
13 | 
14 |         def initialize(argv)
15 |           super
16 |           @all = argv.flag?("all")
17 |           @dry = argv.flag?("dry")
18 |           @targets = argv.arguments!
19 |         end
20 | 
21 |         def run
22 |           to_remove = @targets.flat_map { |t| config.spm_cache_dir.glob("#{t}/*") }
23 |           to_remove = config.spm_cache_dir.glob("*/*") if @all
24 |           to_remove.each do |p|
25 |             UI.info("Removing #{p.basename.to_s.yellow}")
26 |             p.rmtree unless @dry
27 |           end
28 |         end
29 |       end
30 |     end
31 |   end
32 | end
33 | 
--------------------------------------------------------------------------------
/lib/xccache/command/cache/list.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Command
 3 |     class Cache < Command
 4 |       class List < Cache
 5 |         self.summary = "Listing cache"
 6 | 
 7 |         def run
 8 |           target_paths = config.spm_cache_dir.glob("*")
 9 |           target_paths.each do |target_path|
10 |             next if (paths = target_path.glob("*")).empty?
11 |             descs = paths.map { |p| "  #{p.basename.to_s.green}" }
12 |             UI.info <<~DESC
13 |               #{target_path.basename.to_s.cyan}:
14 |               #{descs.join('\n')}
15 |             DESC
16 |           end
17 |         end
18 |       end
19 |     end
20 |   end
21 | end
22 | 
--------------------------------------------------------------------------------
/lib/xccache/command/off.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Off < Command
 7 |       self.summary = "Force-switch to source mode for specific targets"
 8 |       self.arguments = [
 9 |         CLAide::Argument.new("TARGET", false, true),
10 |       ]
11 | 
12 |       def initialize(argv)
13 |         super
14 |         @targets = argv.arguments!
15 |       end
16 | 
17 |       def run
18 |         return if @targets.empty?
19 | 
20 |         UI.info("Will force-use source mode for targets: #{@targets}")
21 |         @targets.each { |t| config.ignore_list << "*/#{t}" }
22 |         Installer::Use.new(ctx: self).install!
23 |       end
24 |     end
25 |   end
26 | end
27 | 
--------------------------------------------------------------------------------
/lib/xccache/command/pkg.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | require "xccache/command/pkg/build"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Pkg < Command
 7 |       self.abstract_command = true
 8 |       self.summary = "Working with Swift packages"
 9 |     end
10 |   end
11 | end
12 | 
--------------------------------------------------------------------------------
/lib/xccache/command/pkg/build.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Pkg < Command
 6 |       class Build < Pkg
 7 |         self.summary = "Build a Swift package into an xcframework"
 8 |         def self.options
 9 |           [
10 |             *Options.build_options,
11 |             ["--out=foo", "Output directory for the xcframework"],
12 |             ["--checksum/no-checksum", "Whether to include checksum to the binary name"],
13 |           ].concat(super)
14 |         end
15 |         self.arguments = [
16 |           CLAide::Argument.new("TARGET", false, true),
17 |         ]
18 | 
19 |         def initialize(argv)
20 |           super
21 |           @targets = argv.arguments!
22 |           @out_dir = argv.option("out")
23 |           @include_checksum = argv.flag?("checksum")
24 |         end
25 | 
26 |         def run
27 |           pkg = SPM::Package.new
28 |           pkg.build(
29 |             targets: @targets,
30 |             config: @config,
31 |             out_dir: @out_dir,
32 |             checksum: @include_checksum,
33 |             **@build_options,
34 |           )
35 |         end
36 |       end
37 |     end
38 |   end
39 | end
40 | 
--------------------------------------------------------------------------------
/lib/xccache/command/remote.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | require "xccache/storage"
 3 | require "xccache/command/remote/pull"
 4 | require "xccache/command/remote/push"
 5 | 
 6 | module XCCache
 7 |   class Command
 8 |     class Remote < Command
 9 |       self.abstract_command = true
10 |       self.summary = "Working with remote cache"
11 |       def self.options
12 |         [
13 |           Options::CONFIG,
14 |           ["--branch=foo", "Cache branch (if using git) (default: main)"],
15 |         ].concat(super)
16 |       end
17 | 
18 |       def initialize(argv)
19 |         super
20 |         @branch = argv.option("branch", "main")
21 |       end
22 | 
23 |       def storage
24 |         @storage ||= create_storage
25 |       end
26 | 
27 |       private
28 | 
29 |       def create_storage
30 |         remote_config = config.remote_config
31 |         if (remote = remote_config["git"])
32 |           return GitStorage.new(branch: @branch, remote: remote)
33 |         elsif (s3_config = remote_config["s3"])
34 |           return S3Storage.new(uri: s3_config["uri"], creds_path: s3_config["creds"])
35 |         end
36 |         Storage.new
37 |       end
38 |     end
39 |   end
40 | end
41 | 
--------------------------------------------------------------------------------
/lib/xccache/command/remote/pull.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Remote < Command
 6 |       class Pull < Remote
 7 |         self.summary = "Pulling cache to local"
 8 | 
 9 |         def run
10 |           storage.pull
11 |         end
12 |       end
13 |     end
14 |   end
15 | end
16 | 
--------------------------------------------------------------------------------
/lib/xccache/command/remote/push.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Remote < Command
 6 |       class Push < Remote
 7 |         self.summary = "Pushing cache to remote"
 8 | 
 9 |         def run
10 |           storage.push
11 |         end
12 |       end
13 |     end
14 |   end
15 | end
16 | 
--------------------------------------------------------------------------------
/lib/xccache/command/rollback.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Rollback < Command
 7 |       self.summary = "Roll back prebuilt cache for packages"
 8 | 
 9 |       def run
10 |         Installer::Rollback.new(ctx: self).install!
11 |       end
12 |     end
13 |   end
14 | end
15 | 
--------------------------------------------------------------------------------
/lib/xccache/command/use.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Use < Command
 7 |       self.summary = "Use prebuilt cache for packages"
 8 |       def self.options
 9 |         [
10 |           *Options.install_options,
11 |         ].concat(super)
12 |       end
13 | 
14 |       def run
15 |         Installer::Use.new(ctx: self).install!
16 |       end
17 |     end
18 |   end
19 | end
20 | 
--------------------------------------------------------------------------------
/lib/xccache/core.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/core/cacheable.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module Cacheable
 3 |     def cacheable(*method_names)
 4 |       method_names.each do |method_name|
 5 |         const_get(__cacheable_module_name).class_eval do
 6 |           define_method(method_name) do |*args, **kwargs|
 7 |             @_cache ||= {}
 8 |             @_cache[method_name] ||= {}
 9 |             @_cache[method_name][args.hash | kwargs.hash] ||=
10 |               method(method_name).super_method.call(*args, **kwargs)
11 |           end
12 |         end
13 |       end
14 |     end
15 | 
16 |     def __cacheable_module_name
17 |       "#{name}_Cacheable".gsub(':', '_')
18 |     end
19 | 
20 |     def self.included(base)
21 |       base.extend(self)
22 | 
23 |       module_name = base.send(:__cacheable_module_name)
24 |       remove_const(module_name) if const_defined?(module_name)
25 |       base.prepend(const_set(module_name, Module.new))
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/core/config.rb:
--------------------------------------------------------------------------------
  1 | require "xcodeproj"
  2 | require "xccache/core/syntax/yml"
  3 | 
  4 | module XCCache
  5 |   class Config < YAMLRepresentable
  6 |     module Mixin
  7 |       def config
  8 |         Config.instance
  9 |       end
 10 |     end
 11 | 
 12 |     def self.instance
 13 |       @instance ||= new(Pathname("xccache.yml").expand_path)
 14 |     end
 15 | 
 16 |     attr_accessor :verbose, :ansi
 17 |     alias verbose? verbose
 18 |     alias ansi? ansi
 19 | 
 20 |     # To distinguish if it's within an installation, or standalone like `xccache pkg build`
 21 |     attr_accessor :in_installation
 22 |     alias in_installation? in_installation
 23 | 
 24 |     attr_writer :install_config
 25 | 
 26 |     def ensure_file!
 27 |       Template.new("xccache.yml").render(save_to: path) unless path.exist?
 28 |     end
 29 | 
 30 |     def install_config
 31 |       @install_config || "debug"
 32 |     end
 33 | 
 34 |     def sandbox
 35 |       @sandbox = Dir.prepare("xccache").expand_path
 36 |     end
 37 | 
 38 |     def spm_sandbox
 39 |       @spm_sandbox ||= Dir.prepare(sandbox / "packages").expand_path
 40 |     end
 41 | 
 42 |     def spm_local_pkgs_dir
 43 |       @spm_local_pkgs_dir ||= Dir.prepare(spm_sandbox / "local")
 44 |     end
 45 | 
 46 |     def spm_xcconfigs_dir
 47 |       @spm_xcconfigs_dir ||= Dir.prepare(spm_sandbox / "xcconfigs")
 48 |     end
 49 | 
 50 |     def spm_cache_dir
 51 |       @spm_cache_dir ||= Dir.prepare(Pathname("~/.xccache/#{install_config}").expand_path)
 52 |     end
 53 | 
 54 |     def spm_binaries_dir
 55 |       @spm_binaries_dir ||= Dir.prepare(spm_sandbox / "binaries")
 56 |     end
 57 | 
 58 |     def spm_build_dir
 59 |       @spm_build_dir ||= spm_umbrella_sandbox / ".build"
 60 |     end
 61 | 
 62 |     def spm_artifacts_dir
 63 |       @spm_artifacts_dir ||= spm_build_dir / "artifacts"
 64 |     end
 65 | 
 66 |     def spm_proxy_sandbox
 67 |       @spm_proxy_sandbox ||= Dir.prepare(spm_sandbox / "proxy")
 68 |     end
 69 | 
 70 |     def spm_umbrella_sandbox
 71 |       @spm_umbrella_sandbox ||= Dir.prepare(spm_sandbox / "umbrella")
 72 |     end
 73 | 
 74 |     def spm_metadata_dir
 75 |       @spm_metadata_dir ||= Dir.prepare(spm_sandbox / "metadata")
 76 |     end
 77 | 
 78 |     def lockfile
 79 |       @lockfile ||= Lockfile.new(Pathname("xccache.lock").expand_path)
 80 |     end
 81 | 
 82 |     def cachemap
 83 |       require "xccache/cache/cachemap"
 84 |       @cachemap ||= Cache::Cachemap.new(sandbox / "cachemap.json")
 85 |     end
 86 | 
 87 |     def projects
 88 |       @projects ||= Pathname(".").glob("*.xcodeproj").map do |p|
 89 |         Xcodeproj::Project.open(p)
 90 |       end
 91 |     end
 92 | 
 93 |     def project_targets
 94 |       projects.flat_map(&:targets)
 95 |     end
 96 | 
 97 |     def remote_config
 98 |       pick_per_install_config(raw["remote"] || {})
 99 |     end
100 | 
101 |     def ignore_list
102 |       raw["ignore"] || []
103 |     end
104 | 
105 |     def ignore?(item)
106 |       return true if ignore_local? && lockfile.local_pkg_slugs.include?(item.split("/").first)
107 |       ignore_list.any? { |p| File.fnmatch(p, item) }
108 |     end
109 | 
110 |     def ignore_local?
111 |       raw["ignore_local"]
112 |     end
113 | 
114 |     def ignore_build_errors?
115 |       raw["ignore_build_errors"]
116 |     end
117 | 
118 |     def keep_pkgs_in_project?
119 |       raw["keep_pkgs_in_project"]
120 |     end
121 | 
122 |     def default_sdk
123 |       raw["default_sdk"] || "iphonesimulator"
124 |     end
125 | 
126 |     private
127 | 
128 |     def pick_per_install_config(hash)
129 |       hash[install_config] || hash["default"] || {}
130 |     end
131 |   end
132 | end
133 | 
--------------------------------------------------------------------------------
/lib/xccache/core/error.rb:
--------------------------------------------------------------------------------
1 | module XCCache
2 |   class BaseError < StandardError
3 |   end
4 | 
5 |   class GeneralError < BaseError
6 |   end
7 | end
8 | 
--------------------------------------------------------------------------------
/lib/xccache/core/git.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Git
 3 |     attr_reader :root
 4 | 
 5 |     def initialize(root)
 6 |       @root = Pathname(root).expand_path
 7 |     end
 8 | 
 9 |     def run(*args, **kwargs)
10 |       Sh.run("git -C #{root}", *args, **kwargs)
11 |     end
12 | 
13 |     def sha
14 |       run("rev-parse --short HEAD", capture: true, log_cmd: false)[0].strip
15 |     end
16 | 
17 |     def clean?
18 |       status("--porcelain", capture: true, log_cmd: false)[0].empty?
19 |     end
20 | 
21 |     def init?
22 |       !root.glob(".git").empty?
23 |     end
24 | 
25 |     %i[init checkout fetch pull push clean add commit branch remote switch status].each do |name|
26 |       define_method(name) do |*args, **kwargs|
27 |         run(name, *args, **kwargs)
28 |       end
29 |     end
30 |   end
31 | end
32 | 
--------------------------------------------------------------------------------
/lib/xccache/core/hash.rb:
--------------------------------------------------------------------------------
 1 | class Hash
 2 |   def deep_merge(other, uniq_block: nil, sort_block: nil, &block)
 3 |     dup.deep_merge!(other, uniq_block: uniq_block, sort_block: sort_block, &block)
 4 |   end
 5 | 
 6 |   def deep_merge!(other, uniq_block: nil, sort_block: nil, &block)
 7 |     merge!(other) do |key, this_val, other_val|
 8 |       result = if this_val.is_a?(Hash) && other_val.is_a?(Hash)
 9 |                  this_val.deep_merge(other_val, uniq_block: uniq_block, sort_block: sort_block, &block)
10 |                elsif this_val.is_a?(Array) && other_val.is_a?(Array)
11 |                  this_val + other_val
12 |                elsif block_given?
13 |                  block.call(key, this_val, other_val)
14 |                else
15 |                  other_val
16 |                end
17 | 
18 |       # uniq by block, prefer updates
19 |       result = result.reverse.uniq(&uniq_block).reverse if uniq_block && result.is_a?(Array)
20 |       result = result.sort_by(&sort_block) if sort_block && result.is_a?(Array)
21 |       result
22 |     end
23 |   end
24 | end
25 | 
--------------------------------------------------------------------------------
/lib/xccache/core/live_log.rb:
--------------------------------------------------------------------------------
 1 | require "monitor"
 2 | require "tty-cursor"
 3 | require "tty-screen"
 4 | 
 5 | module XCCache
 6 |   class LiveLog
 7 |     include UI::Mixin
 8 |     CURSOR_LOCK = Monitor.new
 9 | 
10 |     attr_reader :output, :max_lines, :lines, :cursor, :tee
11 | 
12 |     def initialize(**options)
13 |       @output = options[:output] || $stdout
14 |       @max_lines = options[:max_lines] || 5
15 |       @n_sticky = 0
16 |       @lines = []
17 |       @cursor = TTY::Cursor
18 |       @screen = TTY::Screen
19 |       @tee = options[:tee]
20 |     end
21 | 
22 |     def clear
23 |       commit do
24 |         output.print(cursor.clear_lines(lines.count + @n_sticky))
25 |         @lines = []
26 |         @n_sticky = 0
27 |       end
28 |     end
29 | 
30 |     def puts(line, sticky: false)
31 |       commit do
32 |         output.print(cursor.clear_lines(lines.count + 1))
33 |         if sticky
34 |           @n_sticky += 1
35 |           output.puts(truncated(line))
36 |         else
37 |           lines.shift if lines.count >= max_lines
38 |           lines << truncated(line)
39 |         end
40 |         output.puts(lines) # print non-sticky content
41 |       end
42 |       File.open(tee, "a") { |f| f << "#{line}\n" } if tee
43 |     end
44 | 
45 |     def capture(header)
46 |       header_start = header.magenta.bold
47 |       header_success = "#{header} ✔".green.bold
48 |       header_error = "#{header} ✖".red.bold
49 |       puts(header_start, sticky: true)
50 |       yield if block_given?
51 |       clear
52 |       update_header(header_success)
53 |     rescue StandardError => e
54 |       update_header(header_error)
55 |       raise e
56 |     end
57 | 
58 |     private
59 | 
60 |     def update_header(header)
61 |       commit do
62 |         n = lines.count + @n_sticky
63 |         output.print(cursor.up(n) + header + cursor.column(0) + cursor.down(n))
64 |       end
65 |     end
66 | 
67 |     def commit
68 |       CURSOR_LOCK.synchronize do
69 |         yield
70 |         output.flush
71 |       end
72 |     end
73 | 
74 |     def truncated(msg)
75 |       msg.length > @screen.width ? "#{msg[...@screen.width - 3]}..." : msg
76 |     end
77 | 
78 |     def ui_cls
79 |       self
80 |     end
81 |   end
82 | end
83 | 
--------------------------------------------------------------------------------
/lib/xccache/core/lockfile.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/syntax/json"
 2 | 
 3 | module XCCache
 4 |   class Lockfile < JSONRepresentable
 5 |     class Pkg < Hash
 6 |       def self.from_h(h)
 7 |         Pkg.new.merge(h)
 8 |       end
 9 | 
10 |       def key
11 |         @key ||= ["repositoryURL", "path_from_root", "relative_path"].find { |x| key?(x) }
12 |       end
13 | 
14 |       def id
15 |         self[key]
16 |       end
17 | 
18 |       def local?
19 |         key != "repositoryURL"
20 |       end
21 | 
22 |       def slug
23 |         @slug ||= File.basename(id, ".*")
24 |       end
25 | 
26 |       def relative_path_from_dir(dir)
27 |         return id if key == "relative_path"
28 |         (Pathname.pwd / id).relative_path_from(dir) if key == "path_from_root"
29 |       end
30 | 
31 |       def local_absolute_path
32 |         Pathname.pwd / self["path_from_root"] if local?
33 |       end
34 |     end
35 | 
36 |     def hash_for_project(project)
37 |       raw[project.display_name] ||= {}
38 |     end
39 | 
40 |     def product_dependencies_by_targets
41 |       @product_dependencies_by_targets ||= raw.values.map { |h| h["dependencies"] }.reduce { |acc, h| acc.merge(h) }
42 |     end
43 | 
44 |     def deep_merge!(hash)
45 |       raw.deep_merge!(
46 |         hash,
47 |         uniq_block: proc { |h| h.is_a?(Hash) ? Pkg.from_h(h).id || h : h },
48 |         sort_block: proc { |x| x.to_s.downcase },
49 |       )
50 |       # After deep_merge, clear property cache
51 |       (instance_variables - %i[@path @raw]).each do |ivar|
52 |         remove_instance_variable(ivar)
53 |       end
54 |     end
55 | 
56 |     def pkgs
57 |       @pkgs ||= raw.values.flat_map { |h| h["packages"] || [] }.map { |h| Pkg.from_h(h) }
58 |     end
59 | 
60 |     def local_pkgs
61 |       @local_pkgs ||= pkgs.select(&:local?).uniq
62 |     end
63 | 
64 |     def local_pkg_slugs
65 |       @local_pkg_slugs ||= local_pkgs.map(&:slug).uniq
66 |     end
67 | 
68 |     def known_product_dependencies
69 |       raw.empty? ? [] : product_dependencies.reject { |d| File.dirname(d) == "__unknown__" }
70 |     end
71 | 
72 |     def product_dependencies
73 |       @product_dependencies ||= product_dependencies_by_targets.values.flatten.uniq
74 |     end
75 | 
76 |     def targets_data
77 |       @targets_data ||= product_dependencies_by_targets.transform_keys { |k| "#{k}.xccache" }
78 |     end
79 | 
80 |     def verify!
81 |       known_slugs = pkgs.map(&:slug)
82 |       unknown = product_dependencies.reject { |d| known_slugs.include?(File.dirname(d)) }
83 |       return if unknown.empty?
84 | 
85 |       UI.error! <<~DESC
86 |         Unknown product dependencies at #{path}:
87 | 
88 |         #{unknown.sort.map { |d| "  • #{d}" }.join("\n")}
89 | 
90 |         Refer to this doc for how to resolve this issue:
91 |           https://github.com/trinhngocthuyen/xccache/blob/main/docs/troubleshooting.md#unknown-product-dependencies
92 |       DESC
93 |     end
94 |   end
95 | end
96 | 
--------------------------------------------------------------------------------
/lib/xccache/core/log.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/config"
 2 | require "colored2"
 3 | 
 4 | module XCCache
 5 |   module UI
 6 |     @indent = 0
 7 | 
 8 |     module Mixin
 9 |       include Config::Mixin
10 |       attr_accessor :indent
11 | 
12 |       def section(title, timing: false)
13 |         start = Time.new if timing
14 |         ui_cls.puts(title)
15 |         self.indent += 2
16 |         res = yield if block_given?
17 |         self.indent -= 2
18 |         if timing
19 |           duration = (Time.new - start).to_i
20 |           duration = if duration < 60 then "#{duration}s"
21 |                      elsif duration < 60 * 60 then "#{duration / 60}m"
22 |                      else
23 |                        "#{duration / 3600}h"
24 |                      end
25 |           ui_cls.puts("-> Finished: #{title.dark} (#{duration})")
26 |         end
27 |         res
28 |       end
29 | 
30 |       def message(message)
31 |         ui_cls.puts(message) if config.verbose?
32 |       end
33 | 
34 |       def info(message)
35 |         ui_cls.puts(message)
36 |       end
37 | 
38 |       def warn(message)
39 |         ui_cls.puts(message.yellow)
40 |       end
41 | 
42 |       def error(message)
43 |         ui_cls.puts("[ERROR] #{message}".red)
44 |       end
45 | 
46 |       def error!(message)
47 |         error(message)
48 |         raise GeneralError, message
49 |       end
50 | 
51 |       def puts(message)
52 |         $stdout.puts("#{' ' * self.indent}#{message}")
53 |       end
54 | 
55 |       private
56 | 
57 |       def ui_cls
58 |         UI
59 |       end
60 |     end
61 | 
62 |     class << self
63 |       include Mixin
64 |     end
65 |   end
66 | end
67 | 
--------------------------------------------------------------------------------
/lib/xccache/core/parallel.rb:
--------------------------------------------------------------------------------
 1 | require "parallel"
 2 | 
 3 | class Array
 4 |   def parallel_map(options = {})
 5 |     # By default, use in_threads (IO-bound tasks)
 6 |     default = {}
 7 |     default[:in_threads] = Parallel.processor_count unless options.key?(:in_processes)
 8 |     Parallel.map(self, { **default, **options }) { |x| yield x if block_given? }
 9 |   end
10 | end
11 | 
--------------------------------------------------------------------------------
/lib/xccache/core/sh.rb:
--------------------------------------------------------------------------------
 1 | require "open3"
 2 | require "xccache/core/config"
 3 | require "xccache/core/log"
 4 | require "xccache/core/error"
 5 | 
 6 | module XCCache
 7 |   class Sh
 8 |     class ExecError < BaseError
 9 |     end
10 | 
11 |     class << self
12 |       include Config::Mixin
13 | 
14 |       def capture_output(cmd)
15 |         run(cmd, capture: true, log_cmd: false)[0].strip
16 |       end
17 | 
18 |       def run(*args, env: nil, **options)
19 |         cmd = args.join(" ")
20 | 
21 |         out, err = [], []
22 |         handle_out = options[:handle_out] || proc { |l| out << l }
23 |         handle_err = options[:handle_err] || proc { |l| err << l }
24 |         if (live_log = options[:live_log])
25 |           handle_out = proc { |l| live_log.puts(l) }
26 |           handle_err = proc { |l| live_log.puts(l) }
27 |           live_log.puts("$ #{cmd}") if options[:log_cmd] != false
28 |         elsif options[:log_cmd] != false
29 |           UI.message("$ #{cmd}".cyan.dark)
30 |         end
31 | 
32 |         use_popen = options[:capture] || options[:handle_out] || options[:handle_err] || options[:live_log]
33 |         return system(cmd) || (raise GeneralError, "Command '#{cmd}' failed") unless use_popen
34 | 
35 |         popen3_args = env ? [env, cmd] : [cmd]
36 |         Open3.popen3(*popen3_args) do |_stdin, stdout, stderr, wait_thr|
37 |           stdout_thread = Thread.new { stdout.each { |l| handle_out.call(l.strip) } }
38 |           stderr_thread = Thread.new { stderr.each { |l| handle_err.call(l.strip) } }
39 |           [stdout_thread, stderr_thread].each(&:join)
40 |           result = wait_thr.value
41 |           result.exitstatus
42 |           raise ExecError, "Command '#{cmd}' failed with status: #{result.exitstatus}" unless result.success?
43 |         end
44 |         [out.join("\n"), err.join("\n")]
45 |       end
46 | 
47 |       private
48 | 
49 |       def log_cmd(cmd, live_log: nil)
50 |         return live_log.puts("$ #{cmd}") if live_log
51 |         UI.message("$ #{cmd}".cyan.dark)
52 |       end
53 |     end
54 |   end
55 | end
56 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/hash.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class HashRepresentable
 3 |     attr_reader :path
 4 |     attr_accessor :raw
 5 | 
 6 |     def initialize(path, raw: nil)
 7 |       @path = path
 8 |       @raw = raw || load || {}
 9 |     end
10 | 
11 |     def reload
12 |       @raw = load || {}
13 |     end
14 | 
15 |     def load
16 |       raise NotImplementedError
17 |     end
18 | 
19 |     def merge!(other)
20 |       raw.merge!(other)
21 |     end
22 | 
23 |     def save(to: nil)
24 |       raise NotImplementedError
25 |     end
26 | 
27 |     def [](key)
28 |       raw[key]
29 |     end
30 | 
31 |     def []=(key, value)
32 |       raw[key] = value
33 |     end
34 |   end
35 | end
36 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/json.rb:
--------------------------------------------------------------------------------
 1 | require "json"
 2 | require_relative "hash"
 3 | 
 4 | module XCCache
 5 |   class JSONRepresentable < HashRepresentable
 6 |     def load
 7 |       JSON.parse(path.read) if path.exist?
 8 |     rescue StandardError
 9 |       {}
10 |     end
11 | 
12 |     def save(to: nil)
13 |       (to || path).write(JSON.pretty_generate(raw))
14 |     end
15 |   end
16 | end
17 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/plist.rb:
--------------------------------------------------------------------------------
 1 | require "cfpropertylist"
 2 | require_relative "hash"
 3 | 
 4 | module XCCache
 5 |   class PlistRepresentable < HashRepresentable
 6 |     def load
 7 |       plist = CFPropertyList::List.new(file: path)
 8 |       CFPropertyList.native_types(plist.value)
 9 |     rescue StandardError
10 |       {}
11 |     end
12 | 
13 |     def save(to: nil)
14 |       raise NotImplementedError
15 |     end
16 |   end
17 | end
18 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/yml.rb:
--------------------------------------------------------------------------------
 1 | require "yaml"
 2 | require_relative "hash"
 3 | 
 4 | module XCCache
 5 |   class YAMLRepresentable < HashRepresentable
 6 |     def load
 7 |       YAML.safe_load(path.read) if path.exist?
 8 |     rescue StandardError
 9 |       {}
10 |     end
11 | 
12 |     def save(to: nil)
13 |       (to || path).write(raw.to_yaml)
14 |     end
15 |   end
16 | end
17 | 
--------------------------------------------------------------------------------
/lib/xccache/core/system.rb:
--------------------------------------------------------------------------------
 1 | require "digest"
 2 | require "mkmf"
 3 | require "tmpdir"
 4 | 
 5 | class String
 6 |   def c99extidentifier
 7 |     gsub(/[^a-zA-Z0-9.]/, "_")
 8 |   end
 9 | end
10 | 
11 | class File
12 |   def self.which(bin)
13 |     find_executable0(bin)
14 |   end
15 | end
16 | 
17 | class Dir
18 |   def self.prepare(dir, clean: false, expand: false)
19 |     dir = Pathname(dir)
20 |     dir = dir.expand_path if expand
21 |     dir.rmtree if clean && dir.exist?
22 |     dir.mkpath
23 |     dir
24 |   end
25 | 
26 |   def self.create_tmpdir
27 |     dir = Pathname(Dir.mktmpdir("xccache"))
28 |     res = block_given? ? (yield dir) : dir
29 |     dir.rmtree if block_given?
30 |     res
31 |   end
32 | 
33 |   def self.git?(dir)
34 |     XCCache::Sh.capture_output("git -C #{dir} rev-parse --git-dir") == ".git"
35 |   end
36 | end
37 | 
38 | class Pathname
39 |   def symlink_to(dst)
40 |     dst = Pathname(dst)
41 |     dst.rmtree if dst.symlink?
42 |     dst.parent.mkpath
43 |     File.symlink(expand_path, dst)
44 |   end
45 | 
46 |   def copy(to: nil, to_dir: nil)
47 |     dst = to || (Pathname(to_dir) / basename)
48 |     dst.rmtree if dst.exist? || dst.symlink?
49 |     dst.parent.mkpath
50 |     FileUtils.copy_entry(self, dst)
51 |     dst
52 |   end
53 | 
54 |   def checksum
55 |     hasher = Digest::SHA256.new
56 |     glob("**/*").reject { |p| p.directory? || p.symlink? }.sort.each do |p|
57 |       p.open("rb") do |f|
58 |         while (chunk = f.read(65_536)) # Read 64KB chunks
59 |           hasher.update(chunk)
60 |         end
61 |       end
62 |     end
63 |     hasher.hexdigest[...8]
64 |   end
65 | end
66 | 
--------------------------------------------------------------------------------
/lib/xccache/installer.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/spm"
  2 | require "xccache/installer/integration"
  3 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
  4 | 
  5 | module XCCache
  6 |   class Installer
  7 |     include PkgMixin
  8 |     include IntegrationMixin
  9 | 
 10 |     def initialize(options = {})
 11 |       ctx = options[:ctx]
 12 |       raise GeneralError, "Missing context (Command) for #{self.class}" if ctx.nil?
 13 |       @umbrella_pkg = options[:umbrella_pkg]
 14 |       @install_options = ctx.install_options
 15 |       @build_options = ctx.build_options
 16 |     end
 17 | 
 18 |     def perform_install
 19 |       verify_projects!
 20 |       recreate_config_dirs
 21 |       projects.each { |project| migrate_umbrella_to_proxy(project) }
 22 |       UI.message("Using cache dir: #{config.spm_cache_dir}")
 23 |       config.ensure_file!
 24 |       config.in_installation = true
 25 |       sync_lockfile
 26 |       proxy_pkg.prepare(@install_options)
 27 | 
 28 |       yield if block_given?
 29 | 
 30 |       gen_supporting_files
 31 |       projects.each do |project|
 32 |         add_xccache_refs_to_project(project)
 33 |         inject_xcconfig_to_project(project)
 34 |       end
 35 |       gen_cachemap_viz
 36 |     end
 37 | 
 38 |     def sync_lockfile
 39 |       UI.info("Syncing lockfile")
 40 |       known_dependencies = lockfile.known_product_dependencies
 41 |       update_projects do |project|
 42 |         lockfile.deep_merge!(
 43 |           project.display_name => lockfile_hash_for_project(project, known_dependencies)
 44 |         )
 45 |       end
 46 |       lockfile.save
 47 |       lockfile.verify!
 48 |     end
 49 | 
 50 |     def lockfile
 51 |       config.lockfile
 52 |     end
 53 | 
 54 |     def projects
 55 |       config.projects
 56 |     end
 57 | 
 58 |     def save_projects
 59 |       yield if block_given?
 60 |       projects.each(&:save)
 61 |     end
 62 | 
 63 |     def update_projects
 64 |       projects.each do |project|
 65 |         yield project if block_given?
 66 |         project.save
 67 |       end
 68 |     end
 69 | 
 70 |     private
 71 | 
 72 |     def lockfile_hash_for_project(project, known_dependencies)
 73 |       deps_by_targets = project.targets.to_h do |target|
 74 |         deps = target.non_xccache_pkg_product_dependencies.map do |dep|
 75 |           next dep.full_name unless dep.pkg.nil?
 76 |           known = known_dependencies.find { |x| File.basename(x) == dep.product_name }
 77 |           UI.warn("-> Assuming #{known} for #{dep.full_name}".dark) if known
 78 |           known || dep.full_name
 79 |         end
 80 |         [target.name, deps.sort]
 81 |       end
 82 |       {
 83 |         "packages" => project.non_xccache_pkgs.map(&:to_h),
 84 |         "dependencies" => deps_by_targets,
 85 |         "platforms" => platforms_for_project(project),
 86 |       }
 87 |     end
 88 | 
 89 |     def platforms_for_project(project)
 90 |       project
 91 |         .targets.select(&:platform_name)
 92 |         .map { |t| [t.platform_name.to_s, t.deployment_target] }
 93 |         .sort.reverse.to_h # sort descendingly -> min value is picked for the hash
 94 |     end
 95 | 
 96 |     def verify_projects!
 97 |       raise "No projects detected. Are you running on the correct project directory?" if projects.empty?
 98 |     end
 99 | 
100 |     def add_xccache_refs_to_project(project)
101 |       group = project.xccache_config_group
102 |       add_file = proc { |p| group[p.basename.to_s] || group.new_file(p) }
103 |       add_file.call(config.spm_proxy_sandbox / "Package.swift")
104 |       add_file.call(config.lockfile.path)
105 |       add_file.call(config.path)
106 |       group.ensure_synced_group(name: "local-packages", path: config.spm_local_pkgs_dir)
107 |     end
108 | 
109 |     def inject_xcconfig_to_project(project)
110 |       group = project.xccache_config_group.ensure_synced_group(name: "xcconfigs", path: config.spm_xcconfigs_dir)
111 |       project.targets.each do |target|
112 |         xcconfig_path = config.spm_xcconfigs_dir / "#{target.name}.xcconfig"
113 |         target.build_configurations.each do |build_config|
114 |           if (existing = build_config.base_configuration_xcconfig)
115 |             next if existing.path == xcconfig_path
116 | 
117 |             relative_path = xcconfig_path.relative_path_from(existing.path.parent)
118 |             next if existing.includes.include?(relative_path.to_s)
119 | 
120 |             UI.info("Injecting base configuration for #{target} (#{build_config}) (at: #{existing.path})")
121 |             existing.path.write <<~DESC
122 |               #include "#{relative_path}" // Injected by xccache, for prebuilt macros support
123 |               #{existing.path.read.strip}
124 |             DESC
125 |           else
126 |             UI.info("Setting base configuration #{target} (#{build_config}) as #{xcconfig_path}")
127 |             build_config.base_configuration_reference_anchor = group
128 |             build_config.base_configuration_reference_relative_path = xcconfig_path.basename.to_s
129 |           end
130 |         end
131 |       end
132 |     end
133 | 
134 |     def migrate_umbrella_to_proxy(project)
135 |       return unless project.xccache_pkg&.slug == "umbrella"
136 | 
137 |       UI.info <<~DESC
138 |         Migrating from umbrella to proxy for project #{project.display_name}
139 |         You should notice changes in project files from xccache/package/umbrella -> xccache/package/proxy.
140 |         Don't worry, this is expected.
141 |       DESC
142 |         .yellow
143 | 
144 |       project.xccache_pkg.relative_path = "xccache/packages/proxy"
145 |       if (group = project.xccache_config_group) && (ref = group["Package.swift"])
146 |         ref.path = "xccache/packages/proxy/Package.swift"
147 |       end
148 |     end
149 | 
150 |     def recreate_config_dirs
151 |       [
152 |         config.spm_binaries_dir,
153 |         config.spm_local_pkgs_dir,
154 |         config.spm_xcconfigs_dir,
155 |         config.spm_metadata_dir,
156 |       ].each { |p| Dir.prepare(p, clean: true) }
157 |     end
158 |   end
159 | end
160 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/build.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Installer
 5 |     class Build < Installer
 6 |       def initialize(options = {})
 7 |         super
 8 |         @targets = options[:targets]
 9 |       end
10 | 
11 |       def install!
12 |         perform_install do
13 |           build(
14 |             targets: @targets,
15 |             out_dir: config.spm_cache_dir,
16 |             symlinks_dir: config.spm_binaries_dir,
17 |             checksum: true,
18 |             **@build_options,
19 |           )
20 |           proxy_pkg.gen_proxy # Regenerate proxy to apply new cache after build
21 |         end
22 |       end
23 |     end
24 |   end
25 | end
26 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration.rb:
--------------------------------------------------------------------------------
 1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 2 | 
 3 | module XCCache
 4 |   class Installer
 5 |     module IntegrationMixin
 6 |       include VizIntegrationMixin
 7 |       include DescsIntegrationMixin
 8 |       include BuildIntegrationMixin
 9 |       include SupportingFilesIntegrationMixin
10 |     end
11 |   end
12 | end
13 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/build.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module BuildIntegrationMixin
 4 |       def build(options = {})
 5 |         to_build = targets_to_build(options)
 6 |         return UI.warn("Detected no targets to build among cache-missed targets") if to_build.empty?
 7 | 
 8 |         UI.info("-> Targets to build: #{to_build.to_s.bold}")
 9 |         umbrella_pkg.build(**options, targets: to_build)
10 |       end
11 | 
12 |       def targets_to_build(options)
13 |         items = (options[:targets] || []).map { |x| File.basename(x) }
14 |         items = config.cachemap.missed.map { |x| File.basename(x) } if items.empty?
15 |         targets = items.map { |x| umbrella_pkg.get_target(x) }
16 | 
17 |         if options[:recursive]
18 |           UI.message("Will include cache-missed recursive targets")
19 |           targets += targets.flat_map do |t|
20 |             t.recursive_targets.select { |x| config.cachemap.missed?(x.full_name) }
21 |           end
22 |         end
23 |         # TODO: Sort by number of dependents
24 |         targets.map(&:full_name).uniq
25 |       end
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/descs.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module DescsIntegrationMixin
 4 |       def xccache_desc
 5 |         @xccache_desc ||= desc_of("xccache")
 6 |       end
 7 | 
 8 |       def targets_of_products(products)
 9 |         products = [products] if products.is_a?(String)
10 |         products.flat_map { |x| desc_of(x).targets_of_products(File.basename(x)) }
11 |       end
12 | 
13 |       def dependency_targets_of_products(products)
14 |         products = [products] if products.is_a?(String)
15 |         products.flat_map { |p| @dependency_targets_by_products[p] || [p] }.uniq
16 |       end
17 | 
18 |       def desc_of(d)
19 |         descs_by_name[d.split("/").first]
20 |       end
21 | 
22 |       def binary_targets
23 |         descs_by_name.values.flatten.uniq.flat_map(&:binary_targets)
24 |       end
25 |     end
26 |   end
27 | end
28 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/supporting_files.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module SupportingFilesIntegrationMixin
 4 |       def gen_supporting_files
 5 |         UI.section("Generating supporting files") do
 6 |           gen_xcconfigs
 7 |         end
 8 |       end
 9 | 
10 |       private
11 | 
12 |       def gen_xcconfigs
13 |         macros_config_by_targets.each do |target, hash|
14 |           xcconfig_path = config.spm_xcconfigs_dir / "#{target}.xcconfig"
15 |           UI.message("XCConfig of target #{target} at: #{xcconfig_path}")
16 |           Xcodeproj::Config.new(hash).save_as(xcconfig_path)
17 |         end
18 |       end
19 | 
20 |       def macros_config_by_targets
21 |         proxy_pkg.graph["macros"].to_h do |target, paths|
22 |           swift_flags = paths.map { |p| "-load-plugin-executable #{p}##{File.basename(p, '.*')}" }
23 |           hash = { "OTHER_SWIFT_FLAGS" => "$(inherited) #{swift_flags.join(' ')}" }
24 |           [File.basename(target, ".*"), hash]
25 |         end
26 |       end
27 |     end
28 |   end
29 | end
30 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/viz.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module VizIntegrationMixin
 4 |       def gen_cachemap_viz
 5 |         stats = config.cachemap.stats
 6 |         html_path = config.sandbox / "cachemap.html"
 7 |         js_path = Dir.prepare(config.sandbox / "assets") / "cachemap.js"
 8 |         css_path = config.sandbox / "assets" / "style.css"
 9 | 
10 |         root_dir = Pathname(".").expand_path
11 |         to_relative = proc do |p|
12 |           p.to_s.start_with?(root_dir.to_s) ? p.relative_path_from(root_dir).to_s : p.to_s
13 |         end
14 | 
15 |         UI.info("Cachemap visualization: #{html_path}")
16 |         Template.new("cachemap.html").render(
17 |           {
18 |             :root_dir => root_dir.to_s,
19 |             :root_dir_short => root_dir.basename.to_s,
20 |             :lockfile_path => config.lockfile.path.to_s,
21 |             :lockfile_path_short => to_relative.call(config.lockfile.path),
22 |             :binaries_dir => config.spm_binaries_dir.to_s,
23 |             :binaries_dir_short => to_relative.call(config.spm_binaries_dir),
24 |             :desc_hit => stats[:hit],
25 |             :desc_missed => stats[:missed],
26 |             :desc_ignored => stats[:ignored],
27 |           },
28 |           save_to: html_path
29 |         )
30 |         Template.new("cachemap.js").render(
31 |           { :json => JSON.pretty_generate(config.cachemap.depgraph_data) },
32 |           save_to: js_path
33 |         )
34 |         Template.new("cachemap.style.css").render(save_to: css_path)
35 |       end
36 |     end
37 |   end
38 | end
39 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/rollback.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     class Rollback < Installer
 4 |       def install!
 5 |         update_projects do |project|
 6 |           UI.section("Rolling back cache for project #{project.display_name}".bold.green) do
 7 |             rollback_for_project(project)
 8 |           end
 9 |         end
10 |       end
11 | 
12 |       private
13 | 
14 |       def rollback_for_project(project)
15 |         hash = lockfile.hash_for_project(project)
16 |         pkgs, deps_by_targets = hash["packages"], hash["dependencies"]
17 | 
18 |         # Add packages back to the project
19 |         pkgs.reject { |h| project.has_pkg?(h) }.each do |h|
20 |           project.add_pkg(h)
21 |         end
22 | 
23 |         # Add products back to `Link Binary with Libraries` of targets
24 |         deps_by_targets.each do |name, deps|
25 |           target = project.get_target(name)
26 |           deps.reject { |d| target.has_pkg_product_dependency?(d) }.each do |d|
27 |             target.add_pkg_product_dependency(d)
28 |           end
29 |         end
30 | 
31 |         # Remove .binary product from the project
32 |         project.targets.each(&:remove_xccache_product_dependencies)
33 |         project.xccache_pkg&.remove_from_project
34 |       end
35 |     end
36 |   end
37 | end
38 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/use.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Installer
 5 |     class Use < Installer
 6 |       def install!
 7 |         update_projects do |project|
 8 |           perform_install do
 9 |             UI.section("Using cache for project #{project.display_name}".bold.green) do
10 |               replace_binaries_for_project(project)
11 |             end
12 |           end
13 |         end
14 |       end
15 | 
16 |       private
17 | 
18 |       def replace_binaries_for_project(project)
19 |         project.add_xccache_pkg unless project.has_xccache_pkg?
20 |         project.targets.each do |target|
21 |           target.add_xccache_product_dependency unless target.has_xccache_product_dependency?
22 |           target.remove_pkg_product_dependencies { |d| d.pkg.nil? || !d.pkg.xccache_pkg? }
23 |         end
24 |         project.remove_pkgs(&:non_xccache_pkg?) unless config.keep_pkgs_in_project?
25 |       end
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/main.rb:
--------------------------------------------------------------------------------
1 | require "pry" if ENV["XCCACHE_IMPORT_PRY"] == "true"
2 | require "pathname"
3 | Dir["#{__dir__}/*.rb"].sort.each { |f| require f unless f == __FILE__ }
4 | 
--------------------------------------------------------------------------------
/lib/xccache/spm.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/build.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module SPM
 3 |     class Buildable
 4 |       attr_reader :name, :module_name, :pkg_dir, :pkg_desc, :sdk, :sdks, :config, :path, :tmpdir, :library_evolution,
 5 |                   :live_log
 6 |       alias library_evolution? library_evolution
 7 | 
 8 |       def initialize(options = {})
 9 |         @name = options[:name]
10 |         @module_name = @name.c99extidentifier
11 |         @pkg_dir = Pathname(options[:pkg_dir] || ".").expand_path
12 |         @pkg_desc = options[:pkg_desc]
13 |         @ctx_desc = options[:ctx_desc] # Context desc, could be an umbrella or a standalone pkg
14 |         @sdks = options[:sdks] || []
15 |         @sdk = options[:sdk] || @sdks&.first
16 |         @config = options[:config] || "debug"
17 |         @path = options[:path]
18 |         @tmpdir = options[:tmpdir]
19 |         @library_evolution = options[:library_evolution]
20 |         @sdks.each { |sdk| sdk.version = @ctx_desc.platforms[sdk.platform] } if @ctx_desc
21 |         @live_log = options[:live_log]
22 |       end
23 | 
24 |       def build(options = {})
25 |         raise NotImplementedError
26 |       end
27 | 
28 |       def swift_build(target: nil)
29 |         cmd = ["swift", "build"] + swift_build_args
30 |         cmd << "--package-path" << pkg_dir
31 |         cmd << "--target" << (target || name)
32 |         cmd << "--sdk" << sdk.sdk_path
33 |         sdk.swiftc_args.each { |arg| cmd << "-Xswiftc" << arg }
34 |         if library_evolution?
35 |           # Workaround for swiftinterface emission
36 |           # https://github.com/swiftlang/swift/issues/64669#issuecomment-1535335601
37 |           cmd << "-Xswiftc" << "-enable-library-evolution"
38 |           cmd << "-Xswiftc" << "-alias-module-names-in-module-interface"
39 |           cmd << "-Xswiftc" << "-emit-module-interface"
40 |           cmd << "-Xswiftc" << "-no-verify-emitted-module-interface"
41 |         end
42 |         sh(cmd)
43 |       end
44 | 
45 |       def sh(cmd)
46 |         Sh.run(cmd, live_log: live_log)
47 |       end
48 | 
49 |       def swift_build_args
50 |         [
51 |           "--configuration", config,
52 |           "--triple", sdk.triple(with_version: true),
53 |         ]
54 |       end
55 | 
56 |       def pkg_target
57 |         @pkg_target ||= pkg_desc.get_target(name)
58 |       end
59 |     end
60 |   end
61 | end
62 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/base.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Package
 6 |       class BaseObject < JSONRepresentable
 7 |         include Config::Mixin
 8 | 
 9 |         ATTRS = %i[root retrieve_pkg_desc].freeze
10 |         attr_accessor(*ATTRS)
11 | 
12 |         def name
13 |           raw["name"]
14 |         end
15 | 
16 |         def full_name
17 |           is_a?(Description) ? name : "#{pkg_slug}/#{name}"
18 |         end
19 | 
20 |         def inspect
21 |           to_s
22 |         end
23 | 
24 |         def display_name
25 |           name
26 |         end
27 | 
28 |         def to_s
29 |           "<#{self.class} name=#{display_name}>"
30 |         end
31 | 
32 |         def cast_to(cls)
33 |           o = cls.new(path, raw: raw)
34 |           ATTRS.each { |sym| o.send("#{sym}=", send(sym.to_s)) }
35 |           o
36 |         end
37 | 
38 |         def pkg_name
39 |           @pkg_name ||= root.name
40 |         end
41 | 
42 |         def pkg_slug
43 |           @pkg_slug ||= src_dir.basename.to_s
44 |         end
45 | 
46 |         def fetch(key, dtype)
47 |           raw[key].map do |h|
48 |             o = dtype.new(nil, raw: h)
49 |             o.root = root
50 |             o.retrieve_pkg_desc = retrieve_pkg_desc
51 |             o
52 |           end
53 |         end
54 | 
55 |         def pkg_desc_of(name)
56 |           retrieve_pkg_desc.call(name)
57 |         end
58 | 
59 |         def src_dir
60 |           @src_dir ||= Pathname(root.raw["path"]).parent
61 |         end
62 |       end
63 |     end
64 |   end
65 | end
66 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/dep.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm/desc/base"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Package
 6 |       class Dependency < BaseObject
 7 |         def display_name
 8 |           slug
 9 |         end
10 | 
11 |         def local?
12 |           raw.key?("fileSystem")
13 |         end
14 | 
15 |         def hash
16 |           @hash ||= local? ? raw["fileSystem"].first : raw["sourceControl"].first
17 |         end
18 | 
19 |         def slug
20 |           @slug ||=
21 |             if hash.key?("path")
22 |               File.basename(hash["path"])
23 |             elsif (location = hash["location"]) && location.key?("remote")
24 |               File.basename(location["remote"].flat_map(&:values)[0], ".*")
25 |             else
26 |               hash["identity"]
27 |             end
28 |         end
29 | 
30 |         def path
31 |           @path ||= Pathname(hash["path"]).expand_path if local?
32 |         end
33 | 
34 |         def pkg_desc
35 |           @pkg_desc ||= pkg_desc_of(slug)
36 |         end
37 |       end
38 |     end
39 |   end
40 | end
41 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/desc.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/spm/desc/base"
  2 | 
  3 | module XCCache
  4 |   module SPM
  5 |     class Package
  6 |       class Description < BaseObject
  7 |         include Cacheable
  8 | 
  9 |         def self.descs_in_metadata_dir(dir)
 10 |           descs = dir.glob("*.json").map { |p| Description.new(p) }
 11 |           [descs, combine_descs(descs)]
 12 |         end
 13 | 
 14 |         def root
 15 |           self
 16 |         end
 17 | 
 18 |         def metadata
 19 |           raw["_metadata"] ||= {}
 20 |         end
 21 | 
 22 |         def platforms
 23 |           @platforms ||= raw.fetch("platforms", []).to_h { |h| [h["platformName"].to_sym, h["version"]] }
 24 |         end
 25 | 
 26 |         def dependencies
 27 |           @dependencies ||= fetch("dependencies", Dependency)
 28 |         end
 29 | 
 30 |         def uniform_dependencies
 31 |           dependencies.filter_map(&:pkg_desc)
 32 |         end
 33 | 
 34 |         def products
 35 |           @products ||= fetch("products", Product)
 36 |         end
 37 | 
 38 |         def targets
 39 |           @targets ||= fetch("targets", Target).map(&:downcast)
 40 |         end
 41 | 
 42 |         def binary_targets
 43 |           @binary_targets ||= targets.select(&:binary?)
 44 |         end
 45 | 
 46 |         def has_target?(name)
 47 |           targets.any? { |t| t.name == name }
 48 |         end
 49 | 
 50 |         def get_target(name)
 51 |           targets.find { |t| t.name == name }
 52 |         end
 53 | 
 54 |         def targets_of_products(name)
 55 |           matched_products = products.select { |p| p.name == name }
 56 |           matched_products
 57 |             .flat_map { |p| targets.select { |t| p.target_names.include?(t.name) } }
 58 |         end
 59 | 
 60 |         def local?
 61 |           # Workaround: If the pkg dir is under the build checkouts dir -> remote
 62 |           !src_dir.to_s.start_with?((config.spm_build_dir / "checkouts").to_s)
 63 |         end
 64 | 
 65 |         def traverse
 66 |           nodes, edges, parents = [], [], {}
 67 |           to_visit = targets.dup
 68 |           visited = Set.new
 69 |           until to_visit.empty?
 70 |             cur = to_visit.pop
 71 |             next if visited.include?(cur)
 72 | 
 73 |             visited << cur
 74 |             nodes << cur
 75 |             yield cur if block_given?
 76 | 
 77 |             # For macro impl, we don't need their dependencies, just the tool binary
 78 |             # So, no need to care about swift-syntax dependencies
 79 |             next if cur.macro?
 80 |             cur.direct_dependency_targets.each do |t|
 81 |               to_visit << t
 82 |               edges << [cur, t]
 83 |               parents[t] ||= []
 84 |               parents[t] << cur
 85 |             end
 86 |           end
 87 |           [nodes, edges, parents]
 88 |         end
 89 | 
 90 |         def git
 91 |           @git ||= Git.new(src_dir) if Dir.git?(src_dir)
 92 |         end
 93 | 
 94 |         def self.combine_descs(descs)
 95 |           descs_by_name = descs.flat_map { |d| [[d.name, d], [d.pkg_slug, d]] }.to_h
 96 |           descs.each { |d| d.retrieve_pkg_desc = proc { |name| descs_by_name[name] } }
 97 |           descs_by_name
 98 |         end
 99 |       end
100 |     end
101 |   end
102 | end
103 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/product.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm/desc/base"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Package
 6 |       class Product < BaseObject
 7 |         include Cacheable
 8 |         cacheable :recursive_targets
 9 | 
10 |         def target_names
11 |           raw["targets"]
12 |         end
13 | 
14 |         def flatten_as_targets
15 |           targets
16 |         end
17 | 
18 |         def targets
19 |           @targets ||= root.targets.select { |t| target_names.include?(t.name) }
20 |         end
21 | 
22 |         def library?
23 |           type == :library
24 |         end
25 | 
26 |         def type
27 |           @type ||= raw["type"].keys.first.to_sym
28 |         end
29 | 
30 |         def recursive_targets(platform: nil)
31 |           targets + targets.flat_map { |t| t.recursive_targets(platform: platform) }.uniq
32 |         end
33 |       end
34 |     end
35 |   end
36 | end
37 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/target.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/spm/desc/base"
  2 | 
  3 | module XCCache
  4 |   module SPM
  5 |     class Package
  6 |       class Target < BaseObject
  7 |         include Cacheable
  8 |         cacheable :recursive_targets, :direct_dependency_targets, :direct_dependencies
  9 | 
 10 |         Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 11 | 
 12 |         def xccache?
 13 |           name.end_with?(".xccache")
 14 |         end
 15 | 
 16 |         def xccache_id
 17 |           macro? ? "#{full_name}.macro" : full_name
 18 |         end
 19 | 
 20 |         def type
 21 |           @type ||= raw["type"].to_sym
 22 |         end
 23 | 
 24 |         def downcast
 25 |           cls = {
 26 |             :binary => BinaryTarget,
 27 |             :macro => MacroTarget,
 28 |           }[type]
 29 |           cls.nil? ? self : cast_to(cls)
 30 |         end
 31 | 
 32 |         def module_name
 33 |           name.c99extidentifier
 34 |         end
 35 | 
 36 |         def resource_bundle_name
 37 |           "#{pkg_name}_#{name}.bundle"
 38 |         end
 39 | 
 40 |         def flatten_as_targets
 41 |           [self]
 42 |         end
 43 | 
 44 |         def sources_path
 45 |           @sources_path ||= begin
 46 |             path = raw["path"] || "Sources/#{name}"
 47 |             root.src_dir / path
 48 |           end
 49 |         end
 50 | 
 51 |         def use_clang?
 52 |           !header_paths.empty?
 53 |         end
 54 | 
 55 |         def header_paths(options = {})
 56 |           paths = []
 57 |           paths += public_header_paths if options.fetch(:public, true)
 58 |           paths += header_search_paths if options.fetch(:search, false)
 59 |           paths
 60 |             .flat_map { |p| p.glob("**/*.h*") }
 61 |             .map(&:realpath)
 62 |             .uniq
 63 |         end
 64 | 
 65 |         def settings
 66 |           raw["settings"]
 67 |         end
 68 | 
 69 |         def header_search_paths
 70 |           @header_search_paths ||=
 71 |             settings
 72 |             .filter_map { |h| h.fetch("kind", {})["headerSearchPath"] }
 73 |             .flat_map(&:values)
 74 |             .map { |p| sources_path / p }
 75 |         end
 76 | 
 77 |         def public_header_paths
 78 |           @public_header_paths ||= begin
 79 |             res = []
 80 |             implicit_path = sources_path / "include"
 81 |             res << implicit_path unless implicit_path.glob("**/*.h*").empty?
 82 |             res << (sources_path / raw["publicHeadersPath"]) if raw.key?("publicHeadersPath")
 83 |             res
 84 |           end
 85 |         end
 86 | 
 87 |         def resource_paths
 88 |           @resource_paths ||= begin
 89 |             res = raw.fetch("resources", []).map { |h| sources_path / h["path"] }
 90 |             # Refer to the following link for the implicit resources
 91 |             # https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package#Add-resource-files
 92 |             implicit = sources_path.glob("*.{xcassets,xib,storyboard,xcdatamodeld,lproj}")
 93 |             res + implicit
 94 |           end
 95 |         end
 96 | 
 97 |         def recursive_targets(platform: nil)
 98 |           children = direct_dependency_targets(platform: platform)
 99 |           children += children.flat_map { |t| t.macro? ? [t] : t.recursive_targets(platform: platform) }
100 |           children.uniq
101 |         end
102 | 
103 |         def direct_dependencies(platform: nil)
104 |           raw["dependencies"].flat_map do |hash|
105 |             dep_types = ["byName", "target", "product"]
106 |             if (dep_type = dep_types.intersection(hash.keys).first).nil?
107 |               raise GeneralError, "Unexpected dependency type. Must be one of #{dep_types}. Hash: #{hash}"
108 |             end
109 |             next [] unless match_platform?(hash[dep_type][-1], platform)
110 |             pkg_name = hash[dep_type][1] if dep_type == "product"
111 |             find_deps(hash[dep_type][0], pkg_name, dep_type)
112 |           end
113 |         end
114 | 
115 |         def direct_dependency_targets(platform: nil)
116 |           direct_dependencies(platform: platform).flat_map(&:flatten_as_targets).uniq
117 |         end
118 | 
119 |         def match_platform?(_condition, _platform)
120 |           true # FIXME: Handle this
121 |         end
122 | 
123 |         def macro?
124 |           type == :macro
125 |         end
126 | 
127 |         def binary?
128 |           type == :binary
129 |         end
130 | 
131 |         def binary_path
132 |           sources_path if binary?
133 |         end
134 | 
135 |         def local_binary_path
136 |           binary_path if binary? && root.local?
137 |         end
138 | 
139 |         def checksum
140 |           @checksum ||= root.git&.sha || sources_path.checksum
141 |         end
142 | 
143 |         private
144 | 
145 |         def find_deps(name, pkg_name, dep_type)
146 |           # If `dep_type` is `target` -> constrained within current pkg only
147 |           # If `dep_type` is `product` -> `pkg_name` must be present
148 |           # If `dep_type` is `byName` -> it's either from this pkg, or its children/dependencies
149 |           res = []
150 |           descs = pkg_name.nil? ? [root] + root.uniform_dependencies : [pkg_desc_of(pkg_name)]
151 |           descs.each do |desc|
152 |             by_target = -> { desc.targets.select { |t| t.name == name } }
153 |             by_product = -> { desc.products.select { |t| t.name == name } }
154 |             return by_target.call if dep_type == "target"
155 |             return by_product.call if dep_type == "product"
156 |             return res unless (res = by_target.call).empty?
157 |             return res unless (res = by_product.call).empty?
158 |           end
159 |           []
160 |         end
161 |       end
162 |     end
163 |   end
164 | end
165 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/target/binary.rb:
--------------------------------------------------------------------------------
1 | module XCCache
2 |   module SPM
3 |     class Package
4 |       class BinaryTarget < Target
5 |       end
6 |     end
7 |   end
8 | end
9 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/target/macro.rb:
--------------------------------------------------------------------------------
1 | module XCCache
2 |   module SPM
3 |     class Package
4 |       class MacroTarget < Target
5 |       end
6 |     end
7 |   end
8 | end
9 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/macro.rb:
--------------------------------------------------------------------------------
 1 | require_relative "build"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Macro < Buildable
 6 |       def initialize(options = {})
 7 |         super
 8 |         @library_evolution = false # swift-syntax is not compatible with library evolution
 9 |       end
10 | 
11 |       def build(_options = {})
12 |         # NOTE: Building macro binary is tricky...
13 |         # --------------------------------------------------------------------------------
14 |         # Consider this manifest config: .target(Macro) -> .macro(MacroImpl)
15 |         #   where `.target(Macro)` contains the interfaces
16 |         #   and `.target(MacroImpl)` contains the implementation
17 |         # --------------------------------------------------------------------------------
18 |         # Building `.macro(MacroImpl)` does not produce the tool binary (MacroImpl-tool)... Only `.o` files.
19 |         # Yet, linking those files are exhaustive due to many dependencies in swift-syntax
20 |         # Luckily, building `.target(Macro)` does produce the tool binary.
21 |         # -> WORKAROUND: Find the associated regular target and build it, then collect the tool binary
22 |         # ---------------------------------------------------------------------------------
23 |         associated_target = pkg_desc.targets.find { |t| t.direct_dependency_targets.include?(pkg_target) }
24 |         live_log.info(
25 |           "#{name.yellow.dark} is a macro target. " \
26 |           "Will build the associated target #{associated_target.name.dark} to get the tool binary."
27 |         )
28 |         swift_build(target: associated_target.name)
29 |         binary_path = products_dir / "#{module_name}-tool"
30 |         raise GeneralError, "Tool binary not exist at: #{binary_path}" unless binary_path.exist?
31 |         binary_path.copy(to: path)
32 |         FileUtils.chmod("+x", path)
33 |         live_log.info("-> Macro binary: #{path}")
34 |       end
35 | 
36 |       def products_dir
37 |         @products_dir ||= pkg_dir / ".build" / "arm64-apple-macosx" / config
38 |       end
39 |     end
40 |   end
41 | end
42 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/mixin.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module PkgMixin
 3 |     include Config::Mixin
 4 | 
 5 |     def umbrella_pkg
 6 |       proxy_pkg.umbrella
 7 |     end
 8 | 
 9 |     def proxy_pkg
10 |       @proxy_pkg ||= SPM::Package::Proxy.new(root_dir: config.spm_proxy_sandbox)
11 |     end
12 |   end
13 | end
14 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg/base.rb:
--------------------------------------------------------------------------------
  1 | require "json"
  2 | require "xccache/spm/xcframework/slice"
  3 | require "xccache/spm/xcframework/xcframework"
  4 | require "xccache/spm/xcframework/metadata"
  5 | require "xccache/spm/pkg/proxy"
  6 | require "xccache/swift/sdk"
  7 | 
  8 | module XCCache
  9 |   module SPM
 10 |     class Package
 11 |       include Config::Mixin
 12 |       include Proxy::Mixin
 13 | 
 14 |       include Cacheable
 15 |       cacheable :pkg_desc_of_target
 16 | 
 17 |       attr_reader :root_dir
 18 | 
 19 |       def initialize(options = {})
 20 |         @root_dir = Pathname(options[:root_dir] || ".").expand_path
 21 |       end
 22 | 
 23 |       def build(options = {})
 24 |         validate!
 25 |         targets = (options.delete(:targets) || []).map { |t| t.split("/")[-1] }
 26 |         raise GeneralError, "No targets were specified" if targets.empty?
 27 | 
 28 |         Dir.create_tmpdir do |tmpdir|
 29 |           targets.each_with_index do |t, i|
 30 |             target_tmpdir = Dir.prepare(tmpdir / t)
 31 |             log_dir = Dir.prepare(options[:log_dir] || target_tmpdir)
 32 |             live_log = LiveLog.new(tee: log_dir / "build_#{t}.log")
 33 |             live_log.capture("[#{i + 1}/#{targets.count}] Building target: #{t}") do
 34 |               build_target(**options, target: t, live_log: live_log, tmpdir: target_tmpdir)
 35 |             end
 36 |           rescue StandardError => e
 37 |             UI.error("Error: #{e}\n" + "For details, check out: #{live_log.tee}".yellow.bold)
 38 |             raise e unless Config.instance.ignore_build_errors?
 39 |           end
 40 |         end
 41 |       end
 42 | 
 43 |       def build_target(target: nil, sdks: nil, config: nil, out_dir: nil, **options)
 44 |         target_pkg_desc = pkg_desc_of_target(
 45 |           target,
 46 |           ensure_exist: true,
 47 |         )
 48 |         if target_pkg_desc.binary_targets.any? { |t| t.name == target }
 49 |           return UI.warn("Target #{target} is a binary target -> no need to build")
 50 |         end
 51 | 
 52 |         target = target_pkg_desc.get_target(target)
 53 | 
 54 |         out_dir = Pathname(out_dir || ".")
 55 |         out_dir /= target.name if options[:checksum]
 56 |         ext = target.macro? ? ".macro" : ".xcframework"
 57 |         basename = options[:checksum] ? "#{target.name}-#{target.checksum}" : target.name
 58 |         binary_path = out_dir / "#{basename}#{ext}"
 59 | 
 60 |         cls = target.macro? ? Macro : XCFramework
 61 |         cls.new(
 62 |           name: target.name,
 63 |           pkg_dir: root_dir,
 64 |           config: config,
 65 |           sdks: sdks,
 66 |           path: binary_path,
 67 |           tmpdir: options[:tmpdir],
 68 |           pkg_desc: target_pkg_desc,
 69 |           ctx_desc: pkg_desc || target_pkg_desc,
 70 |           library_evolution: options[:library_evolution],
 71 |           live_log: options[:live_log],
 72 |         ).build(**options)
 73 |         return if (symlinks_dir = options[:symlinks_dir]).nil?
 74 |         binary_path.symlink_to(symlinks_dir / target.name / "#{target.name}#{ext}")
 75 |       end
 76 | 
 77 |       def resolve
 78 |         return if @resolved
 79 |         xccache_proxy.run("resolve --pkg #{root_dir} --metadata #{metadata_dir}")
 80 |         @resolved = true
 81 |       end
 82 | 
 83 |       def pkg_desc
 84 |         descs_by_name[root_dir.basename.to_s]
 85 |       end
 86 | 
 87 |       def pkg_desc_of_target(name, **options)
 88 |         resolve
 89 |         desc = descs.find { |d| d.has_target?(name) }
 90 |         raise GeneralError, "Cannot find package with the given target #{name}" if options[:ensure_exist] && desc.nil?
 91 |         desc
 92 |       end
 93 | 
 94 |       def get_target(name)
 95 |         pkg_desc_of_target(name)&.get_target(name)
 96 |       end
 97 | 
 98 |       private
 99 | 
100 |       def validate!
101 |         return unless root_dir.glob("Package*.swift").empty?
102 |         raise GeneralError, "No Package.swift in #{root_dir}. Are you sure you're running on a package dir?"
103 |       end
104 | 
105 |       def metadata_dir
106 |         config.in_installation? ? config.spm_metadata_dir : root_dir / ".build/metadata"
107 |       end
108 | 
109 |       def descs
110 |         @descs ||= load_descs[0]
111 |       end
112 | 
113 |       def descs_by_name
114 |         @descs_by_name ||= load_descs[1]
115 |       end
116 | 
117 |       def load_descs
118 |         @descs, @descs_by_name = Description.descs_in_metadata_dir(metadata_dir)
119 |       end
120 |     end
121 |   end
122 | end
123 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg/proxy.rb:
--------------------------------------------------------------------------------
 1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 2 | require_relative "proxy_executable"
 3 | 
 4 | module XCCache
 5 |   module SPM
 6 |     class Package
 7 |       class Proxy < Package
 8 |         module Mixin
 9 |           def xccache_proxy
10 |             @xccache_proxy ||= Executable.new
11 |           end
12 |         end
13 | 
14 |         include Mixin
15 | 
16 |         def umbrella
17 |           @umbrella ||= Package.new(root_dir: config.spm_umbrella_sandbox)
18 |         end
19 | 
20 |         def prepare(options = {})
21 |           xccache_proxy.run("gen-umbrella")
22 |           umbrella.resolve
23 |           invalidate_cache(sdks: options[:sdks])
24 |           gen_proxy
25 |         end
26 | 
27 |         def gen_proxy
28 |           xccache_proxy.run("gen-proxy")
29 |           config.cachemap.update_from_graph(graph.reload)
30 |         end
31 | 
32 |         def invalidate_cache(sdks: [])
33 |           UI.message("Invalidating cache (sdks: #{sdks.map(&:to_s).join(', ')})")
34 | 
35 |           config.spm_cache_dir.glob("*/*.{xcframework,macro}").each do |p|
36 |             cmps = p.basename(".*").to_s.split("-")
37 |             name, checksum = cmps[...-1].join("-"), cmps[-1]
38 |             p_without_checksum = config.spm_binaries_dir / name / "#{name}#{p.extname}"
39 |             accept_cache = proc { p.symlink_to(p_without_checksum) }
40 |             reject_cache = proc { p_without_checksum.rmtree if p_without_checksum.exist? }
41 |             next reject_cache.call if (target = umbrella.get_target(name)).nil?
42 |             next reject_cache.call if target.checksum != checksum
43 |             # For macro, we just need the tool binary to exist
44 |             next accept_cache.call if target.macro?
45 | 
46 |             # For regular targets, the xcframework must satisfy the sdk constraints (ie. containing all the slices)
47 |             metadata = XCFramework::Metadata.new(p / "Info.plist")
48 |             expected_triples = sdks.map { |sdk| sdk.triple(with_vendor: false) }
49 |             missing_triples = expected_triples - metadata.triples
50 |             missing_triples.empty? ? accept_cache.call : reject_cache.call
51 |           end
52 |         end
53 | 
54 |         def graph
55 |           @graph ||= JSONRepresentable.new(root_dir / "graph.json")
56 |         end
57 |       end
58 |     end
59 |   end
60 | end
61 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg/proxy_executable.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module SPM
 3 |     class Package
 4 |       class Proxy < Package
 5 |         class Executable
 6 |           REPO_URL = "https://github.com/trinhngocthuyen/xccache-proxy".freeze
 7 |           VERSION_OR_SHA = "1.0.0".freeze
 8 | 
 9 |           def run(cmd)
10 |             env = { "FORCE_OUTPUT" => "console", "FORCE_COLOR" => "1" } if Config.instance.ansi?
11 |             cmd = cmd.is_a?(Array) ? [bin_path.to_s] + cmd : [bin_path.to_s, cmd]
12 |             cmd << "--verbose" if Config.instance.verbose?
13 |             Sh.run(cmd, env: env)
14 |           end
15 | 
16 |           def bin_path
17 |             @bin_path ||= lookup
18 |           end
19 | 
20 |           private
21 | 
22 |           def lookup
23 |             [
24 |               local_bin_path,
25 |               default_bin_path,
26 |             ].find(&:exist?) || download_or_build_from_source
27 |           end
28 | 
29 |           def default_use_downloaded?
30 |             VERSION_OR_SHA.include?(".")
31 |           end
32 | 
33 |           def download_or_build_from_source
34 |             default_use_downloaded? ? download : build_from_source
35 |           end
36 | 
37 |           def build_from_source
38 |             UI.section("Building xccache-proxy binary from source...".magenta) do
39 |               dir = Dir.prepare("~/.xccache/xccache-proxy", expand: true)
40 |               git = Git.new(dir)
41 |               git.init unless git.init?
42 |               git.remote("add", "origin", REPO_URL) unless git.remote(capture: true)[0].strip == "origin"
43 |               git.fetch("origin", VERSION_OR_SHA)
44 |               git.checkout("-f", "FETCH_HEAD", capture: true)
45 | 
46 |               Dir.chdir(dir) { Sh.run("make build CONFIGURATION=release") }
47 |               (dir / ".build" / "release" / "xccache-proxy").copy(to: default_bin_path)
48 |             end
49 |           end
50 | 
51 |           def download
52 |             UI.section("Downloading xccache-proxy binary from remote...".magenta) do
53 |               Dir.create_tmpdir do |dir|
54 |                 url = "#{REPO_URL}/releases/download/#{VERSION_OR_SHA}/xccache-proxy.zip"
55 |                 default_bin_path.parent.mkpath
56 |                 tmp_path = dir / File.basename(url)
57 |                 Sh.run("curl -fSL -o #{tmp_path} #{url} && unzip -d #{default_bin_path.parent} #{tmp_path}")
58 |                 FileUtils.chmod("+x", default_bin_path)
59 |               end
60 |             end
61 |             default_bin_path
62 |           end
63 | 
64 |           def default_bin_path
65 |             @default_bin_path ||= begin
66 |               dir = LIBEXEC / (default_use_downloaded? ? ".download" : ".build")
67 |               dir / "xccache-proxy-#{VERSION_OR_SHA}" / "xccache-proxy"
68 |             end
69 |           end
70 | 
71 |           def local_bin_path
72 |             @local_bin_path ||= LIBEXEC / ".local" / "xccache-proxy"
73 |           end
74 |         end
75 |       end
76 |     end
77 |   end
78 | end
79 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework.rb:
--------------------------------------------------------------------------------
1 | require_relative "build"
2 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
3 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework/metadata.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/syntax/plist"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class XCFramework
 6 |       class Metadata < PlistRepresentable
 7 |         class Library < Hash
 8 |           def id
 9 |             self["LibraryIdentifier"]
10 |           end
11 | 
12 |           def platform
13 |             self["SupportedPlatform"]
14 |           end
15 | 
16 |           def archs
17 |             self["SupportedArchitectures"]
18 |           end
19 | 
20 |           def simulator?
21 |             self["SupportedPlatformVariant"] == "simulator"
22 |           end
23 | 
24 |           def triples
25 |             @triples ||= archs.map do |arch|
26 |               simulator? ? "#{arch}-#{platform}-simulator" : "#{arch}-#{platform}"
27 |             end
28 |           end
29 |         end
30 | 
31 |         def available_libraries
32 |           @available_libraries ||= raw.fetch("AvailableLibraries", []).map { |h| Library.new.merge(h) }
33 |         end
34 | 
35 |         def triples
36 |           @triples ||= available_libraries.flat_map(&:triples)
37 |         end
38 |       end
39 |     end
40 |   end
41 | end
42 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework/slice.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/utils/template"
  2 | 
  3 | module XCCache
  4 |   module SPM
  5 |     class FrameworkSlice < Buildable
  6 |       def build(_options = {})
  7 |         live_log.puts("Building #{name}.framework (#{config}, #{sdk})".cyan, sticky: true)
  8 |         swift_build
  9 |         create_framework
 10 |       end
 11 | 
 12 |       private
 13 | 
 14 |       def override_resource_bundle_accessor
 15 |         # By default, Swift generates resource_bundle_accessor.swift for targets having resources
 16 |         # (Check .build/.build/DerivedSources/resource_bundle_accessor.swift)
 17 |         # This enables accessing the resource bundle via `Bundle.module`.
 18 |         # However, `Bundle.module` expects the resource bundle to be under the app bundle,
 19 |         # which is not the case for binary targets. Instead the bundle is under `Frameworks/.framework`
 20 |         # WORKAROUND:
 21 |         # - Overriding resource_bundle_accessor.swift to add `Frameworks/.framework` to the search list
 22 |         # - Compiling this file into an `.o` file before using `libtool` to create the framework binary
 23 |         live_log.info("Override resource_bundle_accessor")
 24 |         template_name = use_clang? ? "resource_bundle_accessor.m" : "resource_bundle_accessor.swift"
 25 |         source_path = tmpdir / File.basename(template_name)
 26 |         obj_path = products_dir / "#{module_name}.build" / "#{source_path.basename}.o"
 27 |         Template.new(template_name).render(
 28 |           { :pkg => pkg_target.pkg_name, :target => name, :module_name => module_name },
 29 |           save_to: source_path
 30 |         )
 31 | 
 32 |         if use_clang?
 33 |           cmd = ["xcrun", "clang"]
 34 |           cmd << "-x" << "objective-c"
 35 |           cmd << "-target" << sdk.triple(with_version: true) << "-isysroot" << sdk.sdk_path
 36 |           cmd << "-o" << obj_path.to_s
 37 |           cmd << "-c" << source_path
 38 |         else
 39 |           cmd = ["xcrun", "swiftc"]
 40 |           cmd << "-emit-library" << "-emit-object"
 41 |           cmd << "-module-name" << module_name
 42 |           cmd << "-target" << sdk.triple(with_version: true) << "-sdk" << sdk.sdk_path
 43 |           cmd << "-o" << obj_path.to_s
 44 |           cmd << source_path
 45 |         end
 46 |         sh(cmd)
 47 |       end
 48 | 
 49 |       def create_framework
 50 |         override_resource_bundle_accessor if resource_bundle_product_path.exist?
 51 |         create_info_plist
 52 |         create_framework_binary
 53 |         create_headers
 54 |         create_modules
 55 |         copy_resource_bundles if resource_bundle_product_path.exist?
 56 |       end
 57 | 
 58 |       def create_framework_binary
 59 |         # Write .o file list into a file
 60 |         obj_paths = products_dir.glob("#{module_name}.build/**/*.o")
 61 |         raise GeneralError, "Detected no object files for #{name}" if obj_paths.empty?
 62 | 
 63 |         objlist_path = tmpdir / "objects.txt"
 64 |         objlist_path.write(obj_paths.map(&:to_s).join("\n"))
 65 | 
 66 |         cmd = ["libtool", "-static"]
 67 |         cmd << "-o" << "#{path}/#{module_name}"
 68 |         cmd << "-filelist" << objlist_path.to_s
 69 |         sh(cmd)
 70 |         FileUtils.chmod("+x", path / module_name)
 71 |       end
 72 | 
 73 |       def create_info_plist
 74 |         Template.new("framework.info.plist").render(
 75 |           { :module_name => module_name },
 76 |           save_to: path / "Info.plist",
 77 |         )
 78 |       end
 79 | 
 80 |       def create_headers
 81 |         copy_headers
 82 |       end
 83 | 
 84 |       def create_modules
 85 |         copy_swiftmodules unless use_clang?
 86 | 
 87 |         live_log.info("Creating framework modulemap")
 88 |         Template.new("framework.modulemap").render(
 89 |           { :module_name => module_name, :target => name },
 90 |           save_to: modules_dir / "module.modulemap"
 91 |         )
 92 |       end
 93 | 
 94 |       def copy_headers
 95 |         live_log.info("Copying headers")
 96 |         swift_header_paths = products_dir.glob("#{module_name}.build/*-Swift.h")
 97 |         paths = swift_header_paths + pkg_target.header_paths
 98 |         paths.each { |p| process_header(p) }
 99 | 
100 |         umbrella_header_content = paths.map { |p| "#include <#{module_name}/#{p.basename}>" }.join("\n")
101 |         (headers_dir / "#{name}-umbrella.h").write(umbrella_header_content)
102 |       end
103 | 
104 |       def process_header(path)
105 |         handle_angle_bracket_import = proc do |statement, header|
106 |           next statement if header.include?("/")
107 | 
108 |           # NOTE: If importing a header with flat angle-bracket style (ex. `#import `)
109 |           # The header `foo.h` may belong to a dependency's headers.
110 |           # When packaging into xcframework, `#import ` no longer works because `foo.h`
111 |           # coz it's not visible within the framework's headers
112 |           # -> We need to explicitly specify the module it belongs to, ex. `#import `
113 |           targets = [pkg_target] + pkg_target.recursive_targets
114 |           target = targets.find { |t| t.header_paths.any? { |p| p.basename.to_s == header } }
115 |           next statement if target.nil?
116 | 
117 |           corrected_statement = statement.sub("<#{header}>", "<#{target.module_name}/#{header}>")
118 |           <<~CONTENT
119 |             // -------------------------------------------------------------------------------------------------
120 |             // NOTE: This import was corrected by xccache, from flat angle-bracket to nested angle-bracket style
121 |             // Original: `#{statement}`
122 |             #{corrected_statement}
123 |             // -------------------------------------------------------------------------------------------------
124 |           CONTENT
125 |         end
126 | 
127 |         content = path.read.gsub(/^ *#import <(.+)>/) { |m| handle_angle_bracket_import.call(m, $1) }
128 |         (headers_dir / path.basename).write(content)
129 |       end
130 | 
131 |       def copy_swiftmodules
132 |         live_log.info("Copying swiftmodules")
133 |         swiftmodule_dir = Dir.prepare("#{modules_dir}/#{module_name}.swiftmodule")
134 |         swiftinterfaces = products_dir.glob("#{module_name}.build/#{module_name}.swiftinterface")
135 |         to_copy = products_dir.glob("Modules/#{module_name}.*") + swiftinterfaces
136 |         to_copy.each do |p|
137 |           p.copy(to: swiftmodule_dir / p.basename.sub(module_name, sdk.triple))
138 |         end
139 |       end
140 | 
141 |       def copy_resource_bundles
142 |         resolve_resource_symlinks
143 |         live_log.info("Copying resource bundle to framework: #{resource_bundle_product_path.basename}")
144 |         resource_bundle_product_path.copy(to_dir: path)
145 |       end
146 | 
147 |       def resolve_resource_symlinks
148 |         # Well, Xcode seems to well handle symlinks in resources. In xcodebuild log, you would see something like:
149 |         #   CpResource: builtin-copy ... -resolve-src-symlinks
150 |         # But this is not the case if we build with `swift build`. Here, we have to manually handle it
151 |         resource_bundle_product_path.glob("**/*").select(&:symlink?).reject(&:exist?).each do |p|
152 |           UI.message("Resolve resource symlink: #{p}")
153 |           original = pkg_target.resource_paths.find { |rp| rp.symlink? && rp.readlink == p.readlink }
154 |           original&.realpath&.copy(to: p)
155 |         end
156 |       end
157 | 
158 |       def products_dir
159 |         @products_dir ||= pkg_dir / ".build" / sdk.triple / config
160 |       end
161 | 
162 |       def use_clang?
163 |         pkg_target.use_clang?
164 |       end
165 | 
166 |       def resource_bundle_product_path
167 |         @resource_bundle_product_path ||= products_dir / pkg_target.resource_bundle_name
168 |       end
169 | 
170 |       def headers_dir
171 |         @headers_dir ||= Dir.prepare(path / "Headers")
172 |       end
173 | 
174 |       def modules_dir
175 |         @modules_dir ||= Dir.prepare(path / "Modules")
176 |       end
177 |     end
178 |   end
179 | end
180 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework/xcframework.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm/build"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class XCFramework < Buildable
 6 |       attr_reader :slices
 7 | 
 8 |       def initialize(options = {})
 9 |         super
10 |         @slices ||= @sdks.map do |sdk|
11 |           FrameworkSlice.new(
12 |             **options,
13 |             sdks: [sdk],
14 |             path: Dir.prepare(tmpdir / sdk.triple / "#{module_name}.framework"),
15 |           )
16 |         end
17 |       end
18 | 
19 |       def build(merge_slices: false, **_options)
20 |         tmp_new_path = tmpdir / "new.xcframework"
21 |         tmp_existing_path = tmpdir / "existing.framework"
22 | 
23 |         slices.each(&:build)
24 |         create_xcframework(from: slices.map(&:path), to: tmp_new_path)
25 | 
26 |         path.copy(to: tmp_existing_path) if path.exist? && merge_slices
27 |         path.rmtree if path.exist?
28 | 
29 |         if merge_slices && tmp_existing_path.exist?
30 |           framework_paths =
31 |             [tmp_new_path, tmp_existing_path]
32 |             .flat_map { |p| p.glob("*/*.framework") }
33 |             .uniq { |p| p.parent.basename.to_s } # uniq by id (ex. ios-arm64), preferred new ones
34 |           create_xcframework(from: framework_paths, to: path)
35 |         else
36 |           path.parent.mkpath
37 |           tmp_new_path.copy(to: path)
38 |         end
39 |         live_log.info("-> XCFramework: #{path}")
40 |       end
41 | 
42 |       def create_xcframework(options = {})
43 |         live_log.info("Creating xcframework from slices")
44 |         cmd = ["xcodebuild", "-create-xcframework"]
45 |         cmd << "-allow-internal-distribution" unless library_evolution?
46 |         cmd << "-output" << options[:to]
47 |         options[:from].each { |p| cmd << "-framework" << p }
48 |         cmd << "> /dev/null" # Only care about errors
49 |         sh(cmd)
50 |       end
51 |     end
52 |   end
53 | end
54 | 
--------------------------------------------------------------------------------
/lib/xccache/storage.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/storage/base.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Storage
 3 |     include Config::Mixin
 4 | 
 5 |     def initialize(options = {}); end
 6 | 
 7 |     def pull
 8 |       print_warnings
 9 |     end
10 | 
11 |     def push
12 |       print_warnings
13 |     end
14 | 
15 |     private
16 | 
17 |     def print_warnings
18 |       UI.warn <<~DESC
19 |         Do nothing as remote cache is not set up yet.
20 | 
21 |         To set it up, specify `remote` in `xccache.yml`.
22 |         See: https://github.com/trinhngocthuyen/xccache/blob/main/docs/configuration.md#remote
23 |       DESC
24 |     end
25 |   end
26 | end
27 | 
--------------------------------------------------------------------------------
/lib/xccache/storage/git.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | 
 3 | module XCCache
 4 |   class GitStorage < Storage
 5 |     attr_reader :branch
 6 | 
 7 |     def initialize(options = {})
 8 |       super
 9 |       if (@remote = options[:remote])
10 |         schemes = ["http://", "https://", "git@"]
11 |         @remote = File.expand_path(@remote) unless schemes.any? { |x| @remote.start_with?(x) }
12 |         ensure_remote
13 |       end
14 |       @branch = options[:branch]
15 |     end
16 | 
17 |     def pull
18 |       git.fetch("--depth 1 origin #{branch}")
19 |       git.switch("--detach FETCH_HEAD", capture: true)
20 |       git.clean("-dfx", capture: true)
21 |       # Re-create local branch so that it has the latest from remote
22 |       git.branch("-D #{branch} || true", capture: true)
23 |       git.checkout("-b #{branch}", capture: true)
24 |     end
25 | 
26 |     def push
27 |       return UI.info("No changes to push, cache repo is clean".magenta) if git.clean?
28 | 
29 |       git.add(".")
30 |       git.commit("-m \"Update cache at #{Time.new}\"")
31 |       git.push("-u origin #{branch}")
32 |     end
33 | 
34 |     private
35 | 
36 |     def git
37 |       @git ||= Git.new(config.spm_cache_dir)
38 |     end
39 | 
40 |     def ensure_remote
41 |       git.init unless git.init?
42 |       existing = git.remote("get-url origin || true", capture: true, log_cmd: false)[0].strip
43 |       return if @remote == existing
44 |       return git.remote("add origin #{@remote}") if existing.empty?
45 |       git.remote("set-url origin #{@remote}")
46 |     end
47 |   end
48 | end
49 | 
--------------------------------------------------------------------------------
/lib/xccache/storage/s3.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | 
 3 | module XCCache
 4 |   class S3Storage < Storage
 5 |     def initialize(options = {})
 6 |       super
 7 |       @uri = options[:uri]
 8 |       @creds_path = Pathname(options[:creds_path] || "~/.xccache/s3.creds.json").expand_path
 9 |       creds = JSONRepresentable.new(@creds_path)
10 |       @access_key_id = creds["access_key_id"]
11 |       @secret_access_key = creds["secret_access_key"]
12 |     end
13 | 
14 |     def pull
15 |       s3_sync(src: @uri, dst: config.spm_cache_dir)
16 |     end
17 | 
18 |     def push
19 |       s3_sync(src: config.spm_cache_dir, dst: @uri)
20 |     end
21 | 
22 |     private
23 | 
24 |     def s3_sync(src: nil, dst: nil)
25 |       validate!
26 |       UI.info("Syncing cache from #{src.to_s.bold} to #{dst.to_s.bold}...")
27 |       env = {
28 |         "AWS_ACCESS_KEY_ID" => @access_key_id,
29 |         "AWS_SECRET_ACCESS_KEY" => @secret_access_key,
30 |       }
31 |       cmd = ["aws", "s3", "sync"]
32 |       cmd << "--exact-timestamps" << "--delete"
33 |       cmd << "--include" << "*.xcframework"
34 |       cmd << "--include" << "*.macro"
35 |       cmd << src << dst
36 |       Sh.run(cmd, env: env)
37 |     end
38 | 
39 |     def validate!
40 |       if File.which("awsss").nil?
41 |         raise GeneralError, "awscli is not installed. Please install it via brew: `brew install awscli`"
42 |       end
43 |       return unless @access_key_id.nil? || @secret_access_key.nil?
44 |       raise GeneralError, <<~DESC
45 |         Please ensure the credentials json at #{@creds_path}. Example:
46 |         {
47 |           "access_key_id": ,
48 |           "secret_access_key": 
49 |         }
50 |       DESC
51 |     end
52 |   end
53 | end
54 | 
--------------------------------------------------------------------------------
/lib/xccache/swift/sdk.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/sh"
 2 | 
 3 | module XCCache
 4 |   module Swift
 5 |     class Sdk
 6 |       attr_reader :name, :arch, :vendor, :platform
 7 |       attr_accessor :version
 8 | 
 9 |       NAME_TO_PLATFORM = {
10 |         :iphonesimulator => :ios,
11 |         :iphoneos => :ios,
12 |         :macos => :macos,
13 |         :watchos => :watchos,
14 |         :watchsimulator => :watchos,
15 |         :appletvos => :tvos,
16 |         :appletvsimulator => :tvos,
17 |         :xros => :xros,
18 |         :xrsimulator => :xros,
19 |       }.freeze
20 | 
21 |       def initialize(name, version: nil)
22 |         @name = name.to_sym
23 |         @vendor = "apple"
24 |         @arch = "arm64"
25 |         @platform = NAME_TO_PLATFORM.fetch(@name, @name)
26 |         @version = version
27 |         return if NAME_TO_PLATFORM.key?(@name)
28 |         raise GeneralError, "Unknown sdk: #{@name}. Must be one of #{NAME_TO_PLATFORM.keys}"
29 |       end
30 | 
31 |       def to_s
32 |         name.to_s
33 |       end
34 | 
35 |       def triple(with_vendor: true, with_version: false)
36 |         cmps = [arch]
37 |         cmps << vendor if with_vendor
38 |         cmps << (with_version && version ? "#{platform}#{version}" : platform.to_s)
39 |         cmps << "simulator" if simulator?
40 |         cmps.join("-")
41 |       end
42 | 
43 |       def sdk_name
44 |         name == :macos ? :macosx : name
45 |       end
46 | 
47 |       def sdk_path
48 |         # rubocop:disable Layout/LineLength
49 |         # /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
50 |         # rubocop:enable Layout/LineLength
51 |         @sdk_path ||= Pathname(Sh.capture_output("xcrun --sdk #{sdk_name} --show-sdk-path")).realpath
52 |       end
53 | 
54 |       def sdk_platform_developer_path
55 |         @sdk_platform_developer_path ||= sdk_path.parent.parent # iPhoneSimulator.platform/Developer
56 |       end
57 | 
58 |       def swiftc_args
59 |         developer_library_frameworks_path = sdk_platform_developer_path / "Library" / "Frameworks"
60 |         developer_usr_lib_path = sdk_platform_developer_path / "usr" / "lib"
61 |         [
62 |           "-F#{developer_library_frameworks_path}",
63 |           "-I#{developer_usr_lib_path}",
64 |         ]
65 |       end
66 | 
67 |       def simulator?
68 |         name.to_s.end_with?("simulator")
69 |       end
70 |     end
71 |   end
72 | end
73 | 
--------------------------------------------------------------------------------
/lib/xccache/swift/swiftc.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module Swift
 3 |     module Swiftc
 4 |       def self.version
 5 |         @version ||= begin
 6 |           m = /Apple Swift version ([\d\.]+)/.match(Sh.capture_output("xcrun swift -version"))
 7 |           m.nil? ? "6.0" : m[1]
 8 |         end
 9 |       end
10 | 
11 |       def self.version_without_patch
12 |         version.split(".")[...2].join(".")
13 |       end
14 |     end
15 |   end
16 | end
17 | 
--------------------------------------------------------------------------------
/lib/xccache/utils/template.rb:
--------------------------------------------------------------------------------
 1 | require "erb"
 2 | require "xccache/core/error"
 3 | 
 4 | module XCCache
 5 |   class Template
 6 |     attr_reader :name, :path
 7 | 
 8 |     def initialize(name)
 9 |       @name = name
10 |       @path = ROOT / "lib/xccache/assets/templates/#{name}.template"
11 |     end
12 | 
13 |     def render(hash = {}, save_to: nil)
14 |       raise GeneralError, "Template not found: #{name}" if path.nil?
15 | 
16 |       rendered = ERB.new(File.read(@path)).result_with_hash(hash)
17 |       Pathname(save_to).write(rendered) unless save_to.nil?
18 |       rendered
19 |     end
20 |   end
21 | end
22 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj.rb:
--------------------------------------------------------------------------------
1 | require "xccache/core"
2 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
3 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/build_configuration.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class XCBuildConfiguration
 7 |         def base_configuration_xcconfig
 8 |           path = base_configuration_xcconfig_path
 9 |           Config.new(path) if path
10 |         end
11 | 
12 |         def base_configuration_xcconfig_path
13 |           return base_configuration_reference.real_path if base_configuration_reference
14 |           return unless base_configuration_reference_anchor && base_configuration_reference_relative_path
15 |           project.dir / base_configuration_reference_anchor.path / base_configuration_reference_relative_path
16 |         end
17 |       end
18 |     end
19 |   end
20 | end
21 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/config.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Config
 5 |     def path
 6 |       @filepath
 7 |     end
 8 |   end
 9 | end
10 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/file_system_synchronized_root_group.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class PBXFileSystemSynchronizedRootGroup
 7 |         attribute :name, String
 8 | 
 9 |         def display_name
10 |           return name if name
11 |           return File.basename(path) if path
12 |           super
13 |         end
14 |       end
15 |     end
16 |   end
17 | end
18 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/group.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class PBXGroup
 7 |         def synced_groups
 8 |           children.grep(PBXFileSystemSynchronizedRootGroup)
 9 |         end
10 | 
11 |         def ensure_synced_group(name: nil, path: nil)
12 |           synced_groups.find { |g| g.name == name } || new_synced_group(name: name, path: path)
13 |         end
14 | 
15 |         def new_synced_group(name: nil, path: nil)
16 |           path = path.relative_path_from(project.dir) unless path.relative?
17 |           synced_group = project.new(PBXFileSystemSynchronizedRootGroup)
18 |           synced_group.path = path.to_s
19 |           synced_group.name = name || path.basename.to_s
20 |           self << synced_group
21 |           synced_group
22 |         end
23 |       end
24 |     end
25 |   end
26 | end
27 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/pkg.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       module PkgRefMixin
 7 |         def id
 8 |           local? ? (relative_path || path) : repositoryURL
 9 |         end
10 | 
11 |         def slug
12 |           File.basename(id, File.extname(id))
13 |         end
14 | 
15 |         def local?
16 |           is_a?(XCLocalSwiftPackageReference)
17 |         end
18 | 
19 |         def xccache_pkg?
20 |           local? && ["xccache/packages/umbrella", "xccache/packages/proxy"].include?(id)
21 |         end
22 | 
23 |         def non_xccache_pkg?
24 |           !xccache_pkg?
25 |         end
26 | 
27 |         def create_pkg_product_dependency_ref(product)
28 |           ref = project.new(XCSwiftPackageProductDependency)
29 |           ref.package = self
30 |           ref.product_name = product
31 |           ref
32 |         end
33 | 
34 |         def create_target_dependency_ref(product)
35 |           ref = project.new(PBXTargetDependency)
36 |           ref.name = product
37 |           ref.product_ref = create_pkg_product_dependency_ref(product)
38 |           ref
39 |         end
40 |       end
41 | 
42 |       class XCLocalSwiftPackageReference
43 |         include PkgRefMixin
44 | 
45 |         def ascii_plist_annotation
46 |           # Workaround: Xcode is using display_name while Xcodeproj is using File.basename(display_name)
47 |           # Here, the plugin forces to use display_name so that updates either by Xcode or Xcodeproj are consistent
48 |           " #{isa} \"#{display_name}\" "
49 |         end
50 | 
51 |         def to_h
52 |           {
53 |             "path_from_root" => absolute_path.relative_path_from(Pathname.pwd).to_s,
54 |           }
55 |         end
56 | 
57 |         def absolute_path
58 |           path.nil? ? project.dir / relative_path : path
59 |         end
60 |       end
61 | 
62 |       class XCRemoteSwiftPackageReference
63 |         include PkgRefMixin
64 | 
65 |         def to_h
66 |           { "repositoryURL" => repositoryURL, "requirement" => requirement }
67 |         end
68 |       end
69 |     end
70 |   end
71 | end
72 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/pkg_product_dependency.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class XCSwiftPackageProductDependency
 7 |         def full_name
 8 |           @full_name ||= "#{pkg&.slug || '__unknown__'}/#{product_name}"
 9 |         end
10 | 
11 |         def pkg
12 |           return package if package
13 |           return if @warned_missing_pkg
14 |           @warned_missing_pkg = true
15 |           Log.warn("Missing pkg of product dependency #{uuid}: #{to_hash}")
16 |         end
17 | 
18 |         def remove_alongside_related
19 |           target = referrers.find { |x| x.is_a?(PBXNativeTarget) }
20 |           Log.info(
21 |             "(-) Remove #{product_name.red} from product dependencies of target #{target.display_name.bold}"
22 |           )
23 |           target.dependencies.each { |x| x.remove_from_project if x.product_ref == self }
24 |           target.build_phases.each do |phase|
25 |             phase.files.select { |f| f.remove_from_project if f.product_ref == self }
26 |           end
27 |           remove_from_project
28 |         end
29 |       end
30 |     end
31 |   end
32 | end
33 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/project.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     Log = XCCache::UI
 6 | 
 7 |     def display_name
 8 |       relative_path.to_s
 9 |     end
10 | 
11 |     def relative_path
12 |       @relative_path ||= path.relative_path_from(Pathname(".").expand_path)
13 |     end
14 | 
15 |     def dir
16 |       path.parent
17 |     end
18 | 
19 |     def pkgs
20 |       root_object.package_references
21 |     end
22 | 
23 |     def xccache_pkg
24 |       pkgs.find(&:xccache_pkg?)
25 |     end
26 | 
27 |     def non_xccache_pkgs
28 |       pkgs.reject(&:xccache_pkg?)
29 |     end
30 | 
31 |     def has_pkg?(hash)
32 |       pkg_hash = XCCache::Lockfile::Pkg.from_h(hash)
33 |       pkgs.any? { |p| p.id == pkg_hash.id }
34 |     end
35 | 
36 |     def has_xccache_pkg?
37 |       pkgs.any?(&:xccache_pkg?)
38 |     end
39 | 
40 |     def add_pkg(hash)
41 |       pkg_hash = XCCache::Lockfile::Pkg.from_h(hash)
42 |       pkg_hash["relative_path"] = pkg_hash.relative_path_from_dir(dir).to_s if pkg_hash.key == "path_from_root"
43 | 
44 |       Log.info("Add package #{pkg_hash.id.bold} to project #{display_name.bold}")
45 |       cls = pkg_hash.local? ? XCLocalSwiftPackageReference : XCRemoteSwiftPackageReference
46 |       ref = new(cls)
47 |       custom_keys = ["path_from_root"]
48 |       pkg_hash.each { |k, v| ref.send("#{k}=", v) unless custom_keys.include?(k) }
49 |       root_object.package_references << ref
50 |       ref
51 |     end
52 | 
53 |     def add_xccache_pkg
54 |       sandbox_path = XCCache::Config.instance.spm_proxy_sandbox
55 |       add_pkg("relative_path" => sandbox_path.relative_path_from(path.parent).to_s)
56 |     end
57 | 
58 |     def remove_pkgs(&block)
59 |       pkgs.select(&block).each do |pkg|
60 |         Log.info("(-) Remove #{pkg.display_name.red} from package refs of project #{display_name.bold}")
61 |         pkg.remove_from_project
62 |       end
63 |     end
64 | 
65 |     def get_target(name)
66 |       targets.find { |t| t.name == name }
67 |     end
68 | 
69 |     def get_pkg(name)
70 |       pkgs.find { |p| p.slug == name }
71 |     end
72 | 
73 |     def xccache_config_group
74 |       self["xccache.config"] || new_group("xccache.config")
75 |     end
76 |   end
77 | end
78 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/target.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class PBXNativeTarget
 7 |         alias pkg_product_dependencies package_product_dependencies
 8 | 
 9 |         def non_xccache_pkg_product_dependencies
10 |           pkg_product_dependencies.reject { |d| d.pkg&.xccache_pkg? }
11 |         end
12 | 
13 |         def has_xccache_product_dependency?
14 |           pkg_product_dependencies.any? { |d| d.pkg&.xccache_pkg? }
15 |         end
16 | 
17 |         def has_pkg_product_dependency?(name)
18 |           pkg_product_dependencies.any? { |d| d.full_name == name }
19 |         end
20 | 
21 |         def add_pkg_product_dependency(name)
22 |           Log.info("(+) Add dependency #{name.blue} to target #{display_name.bold}")
23 |           pkg_name, product_name = name.split("/")
24 |           pkg = project.get_pkg(pkg_name)
25 |           pkg_product_dependencies << pkg.create_target_dependency_ref(product_name).product_ref
26 |         end
27 | 
28 |         def add_xccache_product_dependency
29 |           add_pkg_product_dependency("proxy/#{name}.xccache")
30 |         end
31 | 
32 |         def remove_xccache_product_dependencies
33 |           remove_pkg_product_dependencies { |d| d.pkg&.xccache_pkg? }
34 |         end
35 | 
36 |         def remove_pkg_product_dependencies(&block)
37 |           package_product_dependencies.select(&block).each(&:remove_alongside_related)
38 |         end
39 |       end
40 |     end
41 |   end
42 | end
43 | 
--------------------------------------------------------------------------------
/xccache.gemspec:
--------------------------------------------------------------------------------
 1 | lib = File.expand_path("lib", __dir__)
 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
 3 | 
 4 | Gem::Specification.new do |spec|
 5 |   spec.name          = "xccache"
 6 |   spec.version       = File.read("VERSION").strip
 7 |   spec.authors       = ["Thuyen Trinh"]
 8 |   spec.email         = ["trinhngocthuyen@gmail.com"]
 9 |   spec.description   = "A Ruby gem"
10 |   spec.summary       = spec.description
11 |   spec.homepage      = "https://github.com/trinhngocthuyen/xccache"
12 |   spec.license       = "MIT"
13 | 
14 |   spec.files         = Dir["{lib,bin}/**/*"]
15 |   spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16 |   spec.require_paths = ["lib"]
17 | 
18 |   spec.add_dependency "claide"
19 |   spec.add_dependency "parallel"
20 |   spec.add_dependency "tty-cursor"
21 |   spec.add_dependency "tty-screen"
22 |   spec.add_dependency "xcodeproj", ">= 1.26.0"
23 | end
24 | 
--------------------------------------------------------------------------------
32 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/packaging-as-xcframework.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Packaging as an xcframework
 4 | 
 5 | The steps to create an xcframework out of a collection of Swift sources are:
 6 | - (1) Creating a framework slice (ex. for iOS iphone simulator - `arm64-apple-ios-simulator`). The result of this step is a framework bundle, ex. `SwiftyBeaver.framework`.
 7 | - (2) Creating an xcframework out of framework slices using `xcodebuild -create-xcframework`.
 8 | 
 9 | There are some tricky actions in step (1) so that the framework bundle meets requirements in step (2). For example, in case of Swift frameworks, `xcodebuild -create-xcframework` requires a swiftinterface in the swiftmodule.
10 | 
11 | ## Creating a Framework Slice
12 | 
13 | By default, building a Swift package target (with `swift build`) does not produce a `.framework` bundle. We have to package it outselve from .o files, headers, swiftmodules, etc.
14 | 
15 | ```
16 | A.framework
17 |   |-- A (binary)
18 |   |-- Info.plist
19 |   |
20 |   |-- Headers /
21 |   |-- Modules /
22 |         |-- module.modulemap
23 |         |-- A.swiftmodule /
24 |               |-- arm64-apple-ios-simulator.swiftinterface
25 |               |-- arm64-apple-ios-simulator.swiftdoc
26 |               ...
27 | ```
28 | Steps to create a framework:
29 | - (1) Run `swift build --target A ...` to build the target. Build artifacts are stored under `.build/debug`.
30 | - (2) Create the framework binary using `libtool` from `.o` files in `.build/debug/A.build`:
31 | ```sh
32 | libtool -static -o A.framework/A .build/debug/A.build/**/*.o
33 | ```
34 | - (3) Copying swiftmodules & swiftinterfaces in `.build/debug/A.build` and `.build/debug/Modules` to `A.framework/Modules`.\
35 | Also, creating the modulemap `module.modulemap` under `A.framework/Modules` so that this framework is visible to ObjC code.
36 | - (4) Copying headers (if any) to `A.framework/Headers`.
37 | - (5) Copying the resource bundle (if any) (ex. in `.build/debug/A_A.bundle`) to the framework bundle.
38 | 
39 | ## Creating an xcframework from Framework Slices
40 | 
41 | ```sh
42 | xcodebuild -create-xcframework \
43 |   -framework arm64-apple-ios-simulator/SwiftyBeaver.framework \
44 |   -framework arm64-apple-ios/SwiftyBeaver.framework \
45 |   -output SwiftyBeaver.xcframework
46 | ```
47 | 
--------------------------------------------------------------------------------
/docs/under-the-hood/proxy-packages.md:
--------------------------------------------------------------------------------
 1 | [< Knowledge Base](../README.md)
 2 | 
 3 | # Under the Hood: Proxy Packages
 4 | 
 5 | The introduction of proxy packages were highlighted in this discussion: [Cache Re-design (v2)](https://github.com/trinhngocthuyen/xccache/discussions/83#discussion-8346379)
 6 | 
 7 | Each resolved package has an accompanying package called proxy package. This package has a very similar manifest to the resolved package. Both share the same checkout sources. The proxy package manifest is derived from its counterpart.
 8 | 
 9 | ```
10 | umbrella
11 | ├── .build/checkouts
12 |     ├── SwiftyBeaver / -- Package.swift
13 |     │     ├── Package.swift
14 |     │     ├── Sources
15 |     │
16 |     └── Alamofire
17 | 
18 | proxy
19 | ├── .proxies
20 |     ├── SwiftyBeaver
21 |     │     ├── Package.swift (updated)
22 |     │     ├── src (symlink to umbrella/.build/checkouts/SwiftyBeaver)
23 |     │
24 |     └── Alamofire
25 | ```
26 | Dependencies of a proxy package are proxy packages
27 | 
28 | Take Moya as an example. It depends on Alamofire, a remote git repo as follows.
29 | ```swift
30 | let package = Package(
31 |   name: "Moya",
32 |   dependencies: [
33 |     .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.0.0"))
34 |   ]
35 | )
36 | ```
37 | When translating to the proxy model, the manifest should be like this:
38 | ```swift
39 | let package = Package(
40 |   name: "Moya",
41 |   dependencies: [
42 |     .package(path: "../Alamofire")
43 |   ]
44 | )
45 | ```
46 | Proxy packages reside adjacent to each other in the directory structure.
47 | ```
48 | umbrella
49 | 
50 | proxy
51 | ├── .proxies
52 | │   ├── Alamofire
53 | │   │     └── Package.swift
54 | │   ├── Moya
55 | │   │     └── Package.swift
56 | │   └── SwiftyBeaver
57 | │         └── Package.swift
58 | │
59 | └── Package.swift (to be integrated to the project)
60 | ```
61 | 
62 | When having cache, the targets declaration in the manifest is altered to use the xcframework.
63 | ```swift
64 | let package = Package(
65 |   name: "Alamofire",
66 |   products: [
67 |     .library(
68 |       name: "Alamofire",
69 |       targets: ["Alamofire"]
70 |     ),
71 |   ],
72 |   targets: [
73 |     .binaryTarget( // <-- HERE
74 |       name: "Alamofire",
75 |       path: "../../../binaries/Alamofire/Alamofire.xcframework"
76 |     )
77 |   ]
78 | )
79 | ```
80 | 
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
 1 | # User settings
 2 | xcuserdata/
 3 | 
 4 | # Obj-C/Swift specific
 5 | *.hmap
 6 | 
 7 | # App packaging
 8 | *.ipa
 9 | *.dSYM.zip
10 | *.dSYM
11 | 
12 | # Playgrounds
13 | timeline.xctimeline
14 | playground.xcworkspace
15 | 
16 | # Swift Package Manager
17 | Packages/
18 | Package.pins
19 | Package.resolved
20 | .swiftpm
21 | 
22 | DerivedData/
23 | .build/
24 | Pods/
25 | *.xcworkspace
26 | 
27 | # fastlane
28 | fastlane/README.md
29 | fastlane/report.xml
30 | fastlane/Preview.html
31 | fastlane/screenshots/**/*.png
32 | fastlane/test_output
33 | 
34 | .spm.pods/
35 | .logs/
36 | .xcconfigs/
37 | xccache/
38 | 
--------------------------------------------------------------------------------
/examples/EX.xcodeproj/xcshareddata/xcschemes/EX.xcscheme:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 5 |    
 9 |       
10 |          
16 |             
22 |             
23 |          
24 |       
25 |    
26 |    
31 |       
32 |          
35 |          
36 |       
37 |       
38 |          
41 |             
47 |             
48 |          
49 |       
50 |    
51 |    
61 |       
63 |          
69 |          
70 |       
71 |    
72 |    
78 |       
80 |          
86 |          
87 |       
88 |    
89 |    
91 |    
92 |    
95 |    
96 | 
97 | 
--------------------------------------------------------------------------------
/examples/EX.xctestplan:
--------------------------------------------------------------------------------
 1 | {
 2 |   "configurations" : [
 3 |     {
 4 |       "id" : "DA9D28D2-8487-4286-9EC0-082D1A2FA0C3",
 5 |       "name" : "Test Scheme Action",
 6 |       "options" : {
 7 | 
 8 |       }
 9 |     }
10 |   ],
11 |   "defaultOptions" : {
12 |     "targetForVariableExpansion" : {
13 |       "containerPath" : "container:EX.xcodeproj",
14 |       "identifier" : "F577C62E2D96971200C83C96",
15 |       "name" : "EX"
16 |     }
17 |   },
18 |   "testTargets" : [
19 |     {
20 |       "parallelizable" : false,
21 |       "target" : {
22 |         "containerPath" : "container:EX.xcodeproj",
23 |         "identifier" : "F5AF0F9B2DAE09EF00AB812D",
24 |         "name" : "EXTests"
25 |       }
26 |     }
27 |   ],
28 |   "version" : 1
29 | }
30 | 
--------------------------------------------------------------------------------
/examples/EX/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 | 
--------------------------------------------------------------------------------
/examples/EX/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 | 
--------------------------------------------------------------------------------
/examples/EX/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 |   "info" : {
3 |     "author" : "xcode",
4 |     "version" : 1
5 |   }
6 | }
7 | 
--------------------------------------------------------------------------------
/examples/EX/ContentView.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | import DebugKit
 3 | import ResourceKit
 4 | 
 5 | struct ContentView: View {
 6 |   var body: some View {
 7 |     VStack {
 8 |       Text(ResourceKit.greetings() ?? "N/A").font(.title)
 9 |       Form {
10 |         Section("Resources") {
11 |           labledContent("ResourceKit.bundle", ResourceKit.bundle.relativePath)
12 |           labledContent("DebugKit.bundle", DebugKit.bundle.relativePath)
13 |           labledContent("DebugKit.token", DebugKit.loadToken())
14 |         }
15 |         .font(.footnote)
16 |       }
17 |     }
18 |   }
19 | 
20 |   private func labledContent(_ label: String, _ value: String) -> some View {
21 |     LabeledContent {
22 |       Text(value)
23 |         .multilineTextAlignment(.trailing)
24 |         .foregroundStyle(value.contains("Frameworks/") ? .green : .gray)
25 |     } label: {
26 |       Text(label)
27 |     }
28 |   }
29 | }
30 | 
31 | #Preview {
32 |   ContentView()
33 | }
34 | 
--------------------------------------------------------------------------------
/examples/EX/EX-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #include "SPMPlayground_ObjC.h"
2 | 
--------------------------------------------------------------------------------
/examples/EX/EXApp.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | 
 3 | @main
 4 | struct EXApp: App {
 5 |   var body: some Scene {
 6 |     WindowGroup {
 7 |       ContentView()
 8 |         .onAppear {
 9 |           playground()
10 |         }
11 |     }
12 |   }
13 | }
14 | 
--------------------------------------------------------------------------------
/examples/EX/Extensions.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | extension Bundle {
 4 |   var relativePath: String {
 5 |     return bundlePath
 6 |       .replacing(Bundle.main.bundlePath, with: "")
 7 |       .replacing(#/^\//#, with: "")
 8 |   }
 9 | }
10 | 
--------------------------------------------------------------------------------
/examples/EX/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 |   "info" : {
3 |     "author" : "xcode",
4 |     "version" : 1
5 |   }
6 | }
7 | 
--------------------------------------------------------------------------------
/examples/EX/SPMPlayground.swift:
--------------------------------------------------------------------------------
 1 | import SwiftyBeaver
 2 | import Moya
 3 | import Alamofire
 4 | import CoreUtils_Wrapper
 5 | import DebugKit
 6 | import ResourceKit
 7 | import Swizzler
 8 | import DisplayKit
 9 | import GoogleMaps
10 | import SDWebImage
11 | import KingfisherWebP
12 | import FirebaseCrashlytics
13 | import FacebookLogin
14 | import Wizard
15 | 
16 | func swift_playground() {
17 |   print(SwiftyBeaver.self)        // SwiftyBeaver
18 |   print(MoyaError.self)           // Moya
19 |   print(AFError.self)             // Alamofire
20 |   print(DebugKit.self)            // core-utils
21 |   print(CoreUtils_Wrapper.self)   // core-utils
22 |   print(Swizzler.self)            // core-utils
23 |   print(ResourceKit.self)         // core-utils
24 |   print(DisplayKit.self)          // core-utils
25 |   print(GMSAddress.self)          // GoogleMaps
26 |   print(SDImageCacheOptions.self) // SDWebImage)
27 |   print(WebPProcessor.self)       // KingfisherWebP
28 |   print(CrashlyticsReport.self)   // FirebaseCrashlytics
29 |   print(LoginResult.self)         // FacebookLogin
30 |   print(FBLoginButton.self)       // FBSDKLoginKit
31 | }
32 | 
33 | func macro_playground() {
34 |   print(#hexColor(0xff0000))
35 | }
36 | 
37 | func playground() {
38 |   swift_playground()
39 |   objc_playground()
40 |   macro_playground()
41 | }
42 | 
--------------------------------------------------------------------------------
/examples/EX/SPMPlayground_ObjC.h:
--------------------------------------------------------------------------------
 1 | #ifndef SPMPlayground_ObjC_h
 2 | #define SPMPlayground_ObjC_h
 3 | 
 4 | #import 
 5 | 
 6 | @interface ObjCPlayground: NSObject
 7 | 
 8 | void objc_playground(void);
 9 | 
10 | @end
11 | 
12 | #endif
13 | 
--------------------------------------------------------------------------------
/examples/EX/SPMPlayground_ObjC.m:
--------------------------------------------------------------------------------
 1 | #import "SPMPlayground_ObjC.h"
 2 | @import Foundation;
 3 | @import CoreUtils_Wrapper;
 4 | @import DebugKit;
 5 | @import ResourceKit;
 6 | 
 7 | @implementation ObjCPlayground
 8 | 
 9 | void objc_playground(void) {
10 |   check(CoreUtils_Wrapper.class);
11 |   check(DebugKit.class);
12 |   check(ResourceKit.class);
13 | }
14 | 
15 | void check(id object) {
16 |   NSLog(@"%@", object);
17 | }
18 | 
19 | @end
20 | 
--------------------------------------------------------------------------------
/examples/EXMac/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 | 
--------------------------------------------------------------------------------
/examples/EXMac/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "images" : [
 3 |     {
 4 |       "idiom" : "mac",
 5 |       "scale" : "1x",
 6 |       "size" : "16x16"
 7 |     },
 8 |     {
 9 |       "idiom" : "mac",
10 |       "scale" : "2x",
11 |       "size" : "16x16"
12 |     },
13 |     {
14 |       "idiom" : "mac",
15 |       "scale" : "1x",
16 |       "size" : "32x32"
17 |     },
18 |     {
19 |       "idiom" : "mac",
20 |       "scale" : "2x",
21 |       "size" : "32x32"
22 |     },
23 |     {
24 |       "idiom" : "mac",
25 |       "scale" : "1x",
26 |       "size" : "128x128"
27 |     },
28 |     {
29 |       "idiom" : "mac",
30 |       "scale" : "2x",
31 |       "size" : "128x128"
32 |     },
33 |     {
34 |       "idiom" : "mac",
35 |       "scale" : "1x",
36 |       "size" : "256x256"
37 |     },
38 |     {
39 |       "idiom" : "mac",
40 |       "scale" : "2x",
41 |       "size" : "256x256"
42 |     },
43 |     {
44 |       "idiom" : "mac",
45 |       "scale" : "1x",
46 |       "size" : "512x512"
47 |     },
48 |     {
49 |       "idiom" : "mac",
50 |       "scale" : "2x",
51 |       "size" : "512x512"
52 |     }
53 |   ],
54 |   "info" : {
55 |     "author" : "xcode",
56 |     "version" : 1
57 |   }
58 | }
59 | 
--------------------------------------------------------------------------------
/examples/EXMac/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 |   "info" : {
3 |     "author" : "xcode",
4 |     "version" : 1
5 |   }
6 | }
7 | 
--------------------------------------------------------------------------------
/examples/EXMac/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 | 
--------------------------------------------------------------------------------
/examples/EXMac/EXMac.entitlements:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 5 | 	com.apple.security.app-sandbox
 6 | 	
 7 | 	com.apple.security.files.user-selected.read-only
 8 | 	
 9 | 
10 | 
11 | 
--------------------------------------------------------------------------------
/examples/EXMac/EXMacApp.swift:
--------------------------------------------------------------------------------
 1 | import SwiftUI
 2 | import SwiftyBeaver
 3 | 
 4 | @main
 5 | struct EXMacApp: App {
 6 |   var body: some Scene {
 7 |     WindowGroup {
 8 |       ContentView()
 9 |     }
10 |   }
11 | }
12 | 
--------------------------------------------------------------------------------
/examples/EXTests/ResourceTests.swift:
--------------------------------------------------------------------------------
 1 | import Testing
 2 | import DebugKit
 3 | import ResourceKit
 4 | 
 5 | struct PkgResourceTests {
 6 |   @Test func checkPkgResources() {
 7 |     #expect(ResourceKit.greetings() == "Hi from xccache!")
 8 |     #expect(DebugKit.loadToken() == "12345")
 9 |   }
10 | }
11 | 
--------------------------------------------------------------------------------
/examples/EXTests/SPMPlayground.swift:
--------------------------------------------------------------------------------
 1 | import DebugKit
 2 | import ResourceKit
 3 | import TestKit
 4 | 
 5 | func playground() {
 6 |   print(DebugKit.self)      // DebugKit
 7 |   print(ResourceKit.self)   // ResourceKit
 8 |   print(TestKit.self)       // TestKit
 9 |   print(BaseTestCase.self)  // TestKit
10 | }
11 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Package.swift:
--------------------------------------------------------------------------------
 1 | // swift-tools-version: 6.0
 2 | 
 3 | import PackageDescription
 4 | 
 5 | let package = Package(
 6 |   name: "CoreUtils",
 7 |   platforms: [.iOS(.v17), .macOS(.v13)],
 8 |   products: [
 9 |     .library(name: "Swizzler", targets: ["Swizzler"]),
10 |     .library(name: "ResourceKit", targets: ["ResourceKit"]),
11 |     .library(name: "DebugKit", targets: ["DebugKit"]),
12 |     .library(name: "DisplayKit", targets: ["DisplayKit"]),
13 |     .library(name: "TestKit", targets: ["TestKit"]),
14 |   ],
15 |   dependencies: [
16 |     .package(url: "https://github.com/SwiftyBeaver/SwiftyBeaver.git", .upToNextMajor(from: "2.1.1")),
17 |     .package(url: "https://github.com/Moya/Moya", .upToNextMajor(from: "15.0.3")),
18 |     .package(path: "../wizard"),
19 |   ],
20 |   targets: [
21 |     .target(
22 |       name: "Swizzler",
23 |       dependencies: [
24 |         "CoreUtils-Wrapper"
25 |       ]
26 |     ),
27 |     .target(
28 |       name: "ResourceKit",
29 |       dependencies: ["CoreUtils-Wrapper"],
30 |       resources: [.copy("greetings.txt")]
31 |     ),
32 |     .target(
33 |       name: "DebugKit",
34 |       dependencies: [
35 |         "CoreUtils-Wrapper",
36 |         "Swizzler",
37 |         .product(name: "SwiftyBeaver", package: "SwiftyBeaver"),
38 |         .product(name: "Moya", package: "Moya"),
39 |       ],
40 |       path: "Sources/DebugKitObjC",
41 |       resources: [.copy("token.txt")],
42 |       publicHeadersPath: "Headers",
43 |       cSettings: [
44 |         .headerSearchPath("PrivateHeaders"),
45 |       ]
46 |     ),
47 |     .target(
48 |       name: "DisplayKit",
49 |       dependencies: [
50 |         .product(name: "Wizard", package: "wizard"),
51 |       ]
52 |     ),
53 |     .target(
54 |       name: "CoreUtils-Wrapper",
55 |       path: "Sources/Core"
56 |     ),
57 |     .target(name: "TestKit"),
58 |   ]
59 | )
60 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/Core/dummy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | 
3 | @objc public class CoreUtils_Wrapper: NSObject { }
4 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/DebugKit.m:
--------------------------------------------------------------------------------
 1 | #import 
 2 | #import 
 3 | #import 
 4 | #import 
 5 | 
 6 | @implementation DebugKit
 7 | + (NSBundle *)bundle {
 8 |   return SWIFTPM_MODULE_BUNDLE;
 9 | }
10 | + (NSString *)loadToken {
11 |   NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
12 |   NSString *tokenPath = [bundle pathForResource:@"token" ofType:@"txt"];
13 |   NSString *content = [NSString stringWithContentsOfFile:tokenPath encoding:NSUTF8StringEncoding error:nil];
14 |   return [content stringByReplacingOccurrencesOfString:@"\n" withString:@""];
15 | }
16 | + (void)diagnose {
17 |   [Swizzler swizzle:@"foo" with:@"bar" forClass:DebugKit.class];
18 |   [Diagnoser diagnoseDevice];
19 | }
20 | @end
21 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/Diagnose.m:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | @implementation Diagnoser
 4 | 
 5 | + (void)diagnoseDevice {
 6 |   NSLog(@"Diagnosing device...");
 7 | }
 8 | 
 9 | @end
10 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/Headers/DebugKit.h:
--------------------------------------------------------------------------------
 1 | #import 
 2 | // 👇 expect xccache to convert to nested angle-bracket style `#import `
 3 | #import 
 4 | 
 5 | NS_ASSUME_NONNULL_BEGIN
 6 | 
 7 | @interface DebugKit: NSObject
 8 | @property (class, nonatomic, readonly, strong) NSBundle *bundle;
 9 | + (NSString *)loadToken;
10 | + (void)diagnose;
11 | @end
12 | 
13 | NS_ASSUME_NONNULL_END
14 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/PrivateHeaders/Diagnose.h:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | NS_ASSUME_NONNULL_BEGIN
 4 | 
 5 | @interface Diagnoser: NSObject
 6 | + (void)diagnoseDevice;
 7 | @end
 8 | 
 9 | NS_ASSUME_NONNULL_END
10 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DebugKitObjc/token.txt:
--------------------------------------------------------------------------------
1 | 12345
2 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/DisplayKit/DisplayKit.swift:
--------------------------------------------------------------------------------
1 | import Wizard
2 | 
3 | public class DisplayKit {
4 |   public static let baseColor = #hexColor(0xff0000)
5 | }
6 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/ResourceKit/ResourceKit.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | @objc public class ResourceKit: NSObject {
 4 |   public static let bundle = Bundle.module
 5 |   public static func greetings() -> String? {
 6 |     guard let url = Bundle.module.url(forResource: "greetings", withExtension: "txt"),
 7 |           let content = try? String(contentsOf: url, encoding: .utf8)
 8 |     else { return nil }
 9 |     return content.replacing(#/\s*$/#, with: "") // Strip trailing spaces
10 |   }
11 | }
12 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/ResourceKit/greetings.txt:
--------------------------------------------------------------------------------
1 | Hi from xccache!
2 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/Swizzler/Swizzler.m:
--------------------------------------------------------------------------------
1 | #import "Swizzler.h"
2 | 
3 | @implementation Swizzler
4 | + (void)swizzle:(NSString* )m1 with:(NSString *)m2 forClass:(Class)cls {
5 |   NSLog(@"Swizzle %@ with %@", m1, m2);
6 | }
7 | @end
8 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/Swizzler/include/Swizzler.h:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | NS_ASSUME_NONNULL_BEGIN
 4 | 
 5 | @interface Swizzler: NSObject
 6 | + (void)swizzle:(NSString* )m1 with:(NSString *)m2 forClass:(Class)cls;
 7 | @end
 8 | 
 9 | NS_ASSUME_NONNULL_END
10 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/core-utils/Sources/TestKit/TestKit.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | import XCTest
3 | 
4 | @objc public class TestKit: NSObject { }
5 | 
6 | public class BaseTestCase: XCTestCase { }
7 | public protocol BaseTrait: Trait { }
8 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Package.swift:
--------------------------------------------------------------------------------
 1 | // swift-tools-version: 6.0
 2 | 
 3 | import PackageDescription
 4 | import CompilerPluginSupport
 5 | 
 6 | let package = Package(
 7 |   name: "Wizard",
 8 |   platforms: [.iOS(.v17), .macOS(.v13)],
 9 |   products: [
10 |     .library(
11 |       name: "Wizard",
12 |       targets: ["Wizard"]
13 |     ),
14 |     .executable(
15 |       name: "WizardPlayground",
16 |       targets: ["WizardPlayground"]
17 |     ),
18 |   ],
19 |   dependencies: [
20 |     .package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.0"),
21 |   ],
22 |   targets: [
23 |     .macro(
24 |       name: "WizardImpl",
25 |       dependencies: [
26 |         .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
27 |         .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
28 |       ]
29 |     ),
30 |     .target(name: "Wizard", dependencies: ["WizardImpl"]),
31 |     .executableTarget(name: "WizardPlayground", dependencies: ["Wizard"]),
32 |   ]
33 | )
34 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/Wizard/Wizard.swift:
--------------------------------------------------------------------------------
 1 | #if canImport(UIKit)
 2 | import UIKit
 3 | public typealias XColor = UIColor
 4 | 
 5 | #elseif canImport(AppKit)
 6 | import AppKit
 7 | public typealias XColor = NSColor
 8 | 
 9 | #else
10 | #error("Only usable with UIKit or AppKit")
11 | #endif
12 | 
13 | @freestanding(expression)
14 | public macro hexColor(_ intLiteral: IntegerLiteralType ) -> XColor = #externalMacro(module: "WizardImpl", type: "HexColorMacro")
15 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/WizardImpl/HexColorMacro.swift:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | import SwiftCompilerPlugin
 3 | import SwiftSyntax
 4 | import SwiftSyntaxBuilder
 5 | import SwiftSyntaxMacros
 6 | 
 7 | enum HexColorMacroError: Error {
 8 |   case notFoundHex
 9 | }
10 | 
11 | public struct HexColorMacro: ExpressionMacro {
12 |   public static func expansion(
13 |     of node: some FreestandingMacroExpansionSyntax,
14 |     in context: some MacroExpansionContext
15 |   ) throws -> ExprSyntax {
16 |     guard let arg = node.arguments.first,
17 |           let expr = arg.expression.as(IntegerLiteralExprSyntax.self)
18 |     else {
19 |       throw HexColorMacroError.notFoundHex
20 |     }
21 |     let hex = expr.literal.text
22 |     return """
23 |     XColor(
24 |       red: .init((\(raw: hex) >> 16) & 0xff) / 255,
25 |       green: .init((\(raw: hex) >> 8) & 0xff) / 255,
26 |       blue: .init((\(raw: hex) >> 0) & 0xff) / 255,
27 |       alpha: 1.0
28 |     )
29 |     """
30 |   }
31 | }
32 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/WizardImpl/WizardImpl.swift:
--------------------------------------------------------------------------------
 1 | import SwiftCompilerPlugin
 2 | import SwiftSyntax
 3 | import SwiftSyntaxBuilder
 4 | import SwiftSyntaxMacros
 5 | 
 6 | @main
 7 | struct HexColorMacroPlugin: CompilerPlugin {
 8 |   let providingMacros: [Macro.Type] = [
 9 |     HexColorMacro.self,
10 |   ]
11 | }
12 | 
--------------------------------------------------------------------------------
/examples/LocalPackages/wizard/Sources/WizardPlayground/main.swift:
--------------------------------------------------------------------------------
1 | import Wizard
2 | 
3 | let color = #hexColor(0xff0000)
4 | print(color)
5 | 
--------------------------------------------------------------------------------
/examples/Makefile:
--------------------------------------------------------------------------------
 1 | XCCACHE_ARGS := --verbose
 2 | 
 3 | format:
 4 | 	cd .. && make format
 5 | 
 6 | cache.use:
 7 | 	bundle exec xccache use $(XCCACHE_ARGS)
 8 | 
 9 | cache.build:
10 | 	bundle exec xccache build $(TARGET) $(XCCACHE_ARGS)
11 | 
12 | cache.init:
13 | 	bundle exec xccache init $(XCCACHE_ARGS)
14 | 
15 | cache.rollback:
16 | 	bundle exec xccache rollback $(XCCACHE_ARGS)
17 | 
18 | cache.viz:
19 | 	bundle exec xccache viz --out=xccache $(XCCACHE_ARGS)
20 | 
21 | build:
22 | 	cicd ios build
23 | 
24 | test:
25 | 	cicd ios test
26 | 
27 | check:
28 | 	rm -rf xccache/packages/umbrella/.build xccache/cachemap.json
29 | 	make cache.build build
30 | 
31 | ex.%:
32 | 	make $*
33 | 
--------------------------------------------------------------------------------
/examples/config.xcconfig:
--------------------------------------------------------------------------------
1 | #include? ".xcconfigs/hook.xcconfig"
2 | 
--------------------------------------------------------------------------------
/examples/xccache.lock:
--------------------------------------------------------------------------------
 1 | {
 2 |   "EX.xcodeproj": {
 3 |     "packages": [
 4 |       {
 5 |         "path_from_root": "LocalPackages/core-utils"
 6 |       },
 7 |       {
 8 |         "path_from_root": "LocalPackages/wizard"
 9 |       },
10 |       {
11 |         "repositoryURL": "https://github.com/facebook/facebook-ios-sdk",
12 |         "requirement": {
13 |           "kind": "upToNextMajorVersion",
14 |           "minimumVersion": "9.0.0"
15 |         }
16 |       },
17 |       {
18 |         "repositoryURL": "https://github.com/firebase/firebase-ios-sdk",
19 |         "requirement": {
20 |           "kind": "upToNextMajorVersion",
21 |           "minimumVersion": "11.4.0"
22 |         }
23 |       },
24 |       {
25 |         "repositoryURL": "https://github.com/googlemaps/ios-maps-sdk",
26 |         "requirement": {
27 |           "kind": "upToNextMajorVersion",
28 |           "minimumVersion": "9.4.0"
29 |         }
30 |       },
31 |       {
32 |         "repositoryURL": "https://github.com/Moya/Moya",
33 |         "requirement": {
34 |           "kind": "upToNextMajorVersion",
35 |           "minimumVersion": "15.0.3"
36 |         }
37 |       },
38 |       {
39 |         "repositoryURL": "https://github.com/SDWebImage/SDWebImage",
40 |         "requirement": {
41 |           "kind": "upToNextMajorVersion",
42 |           "minimumVersion": "5.21.0"
43 |         }
44 |       },
45 |       {
46 |         "repositoryURL": "https://github.com/SnapKit/SnapKit",
47 |         "requirement": {
48 |           "kind": "upToNextMajorVersion",
49 |           "minimumVersion": "5.7.1"
50 |         }
51 |       },
52 |       {
53 |         "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver.git",
54 |         "requirement": {
55 |           "kind": "upToNextMajorVersion",
56 |           "minimumVersion": "2.1.1"
57 |         }
58 |       },
59 |       {
60 |         "repositoryURL": "https://github.com/yeatse/KingfisherWebP",
61 |         "requirement": {
62 |           "kind": "upToNextMajorVersion",
63 |           "minimumVersion": "1.6.0"
64 |         }
65 |       }
66 |     ],
67 |     "dependencies": {
68 |       "EX": [
69 |         "core-utils/DebugKit",
70 |         "core-utils/DisplayKit",
71 |         "core-utils/ResourceKit",
72 |         "core-utils/Swizzler",
73 |         "facebook-ios-sdk/FacebookLogin",
74 |         "firebase-ios-sdk/FirebaseCrashlytics",
75 |         "ios-maps-sdk/GoogleMaps",
76 |         "KingfisherWebP/KingfisherWebP",
77 |         "Moya/Moya",
78 |         "SDWebImage/SDWebImage",
79 |         "SnapKit/SnapKit-Dynamic",
80 |         "SwiftyBeaver/SwiftyBeaver",
81 |         "wizard/Wizard"
82 |       ],
83 |       "EXTests": [
84 |         "core-utils/TestKit"
85 |       ],
86 |       "EXMac": [
87 |         "SwiftyBeaver/SwiftyBeaver"
88 |       ]
89 |     },
90 |     "platforms": {
91 |       "ios": "17.6",
92 |       "osx": "15.4"
93 |     }
94 |   }
95 | }
--------------------------------------------------------------------------------
/examples/xccache.yml:
--------------------------------------------------------------------------------
 1 | # ignore_local: true
 2 | ignore: []
 3 | default_sdk: iphonesimulator
 4 | remote:
 5 |   # debug:
 6 |   #   git: https://github.com/trinhngocthuyen/.cache.git
 7 |   # release:
 8 |   #   s3:
 9 |   #     uri: "s3://xccache/release"
10 | 
--------------------------------------------------------------------------------
/lib/xccache.rb:
--------------------------------------------------------------------------------
1 | require "xccache/main"
2 | 
3 | module XCCache
4 |   ROOT = Pathname(__dir__).parent
5 |   LIBEXEC = ROOT / "libexec"
6 | end
7 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/cachemap.html.template:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   
 6 |   Cachemap Visualization
 7 |   
 8 |   
 9 |   
10 |   
11 |   
12 |   
13 | 
14 | 
15 |   
50 |   
51 |   
54 | 
55 | 
56 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/cachemap.js.template:
--------------------------------------------------------------------------------
 1 | const graph = JSON.parse(`
 2 | <%= json %>
 3 | `);
 4 | 
 5 | // ------------------------------------------------
 6 | 
 7 | const COLORS = {
 8 |   'hit': '#339966',
 9 |   'missed': '#ff6f00',
10 |   'ignored': '#888',
11 |   'NA': '#888',
12 | }
13 | const cy = cytoscape({
14 |   container: $('#cy'),
15 |   elements: ([...graph.nodes, ...graph.edges]).map(x => ({data: x})),
16 |   style: [
17 |     {
18 |       selector: 'node',
19 |       style: {
20 |         'label': (e) => e.id().split("/")[1],
21 |         'color': '#fff',
22 |         'text-valign': 'center',
23 |         'text-halign': 'center',
24 |         'font-size': '14px',
25 |         'shape': 'roundrectangle',
26 |         'width': (e) => Math.max(50, e.id().split('/')[1].length * 8),
27 |         'background-color': (e) => COLORS[e.data('cache') || 'NA'],
28 |       }
29 |     },
30 |     {
31 |       selector: 'node:selected',
32 |       style: {
33 |         'font-weight': 'bold',
34 |         'border-width': 3,
35 |         'border-color': '#333',
36 |       }
37 |     },
38 |     {
39 |       selector: 'node[type="agg"]',
40 |       style: {
41 |         'background-color': '#333',
42 |       }
43 |     },
44 |     {
45 |       selector: 'edge',
46 |       style: {
47 |         'width': 1,
48 |         'target-arrow-shape': 'triangle',
49 |         'curve-style': 'bezier',
50 |         'line-color': '#ccc',
51 |         'target-arrow-color': '#ccc',
52 |       }
53 |     },
54 |   ],
55 |   layout: {
56 |     name: 'fcose',
57 |     animationDuration: 200,
58 |     nodeRepulsion: 10000,
59 |     idealEdgeLength: 120,
60 |     gravity: 0.25,
61 |   }
62 | });
63 | 
64 | cy.on('select', 'node', function(event) {
65 |   const node = event.target;
66 |   node.displayDetails();
67 |   node.neighborhood().add(node).focus();
68 | });
69 | 
70 | cy.on('tap', function(event) {
71 |   if (event.target == cy) {
72 |     $('.node-info').css('display', 'none');
73 |     cy.elements().animateStyle({'opacity': 1, 'line-color': '#ccc', 'target-arrow-color': '#ccc'});
74 |   }
75 | });
76 | 
77 | // -----------------------------------------------------------------
78 | 
79 | cytoscape('collection', 'animateStyle', function(style) {
80 |   this.animate({style: style, duration: 200, easing: 'ease-out'})
81 | });
82 | cytoscape('collection', 'focus', function() {
83 |   this.animateStyle({'opacity': 1, 'line-color': '#666', 'target-arrow-color': '#666'});
84 |   cy.elements().not(this).animateStyle({'opacity': 0.15, 'line-color': '#ccc', 'target-arrow-color': '#ccc'});
85 | });
86 | cytoscape('collection', 'displayDetails', function() {
87 |   $('.node-info').css('display', 'block');
88 |   const info = $('.node-info .info');
89 |   info.find('.target').html(this.id());
90 |   info.find('.checksum').html(this.data('checksum') || 'NA');
91 |   info.find('.binary')
92 |     .html((this.data('binary') || 'NA').split('/').slice(-1))
93 |     .attr({'href': this.data('binary') || ''});
94 |   info.find('.others').html(`Node degree: ${this.degree()} (${this.indegree()} in, ${this.outdegree()} out)`);
95 | });
96 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/cachemap.style.css.template:
--------------------------------------------------------------------------------
 1 | :root {
 2 |   --primary-color: #1492A0;
 3 |   --bg-color: color-mix(in srgb, var(--primary-color), white 80%);
 4 | }
 5 | body {
 6 |   font-family: Helvetica, Arial, sans-serif;
 7 |   font-size: 12px;
 8 |   margin: 0;
 9 |   line-height: 1.6;
10 | }
11 | a { color: var(--primary-color) }
12 | a:hover { color: #339966; }
13 | .fa-solid { color: var(--primary-color) }
14 | .container {
15 |   display: flex;
16 |   height: 100vh;
17 | }
18 | #cy {
19 |   flex: 1;
20 | }
21 | #sidebar {
22 |   position: relative;
23 |   background-color: var(--bg-color);
24 |   width: 250px;
25 |   transition: all 0.3s ease;
26 | }
27 | .sidebar-content {
28 |   width: calc(250px - 32px);
29 |   padding: 16px;
30 |   transform: translateX(0px);
31 |   transition: all 0.3s ease;
32 | }
33 | #sidebar.collapsed {
34 |   width: 0;
35 | }
36 | #sidebar.collapsed .sidebar-content{
37 |   transform: translateX(-250px);
38 | }
39 | #sidebar.collapsed .toggle-btn {
40 |   right: -36px;
41 |   transform: rotate(180deg);
42 | }
43 | .toggle-btn {
44 |   position: absolute;
45 |   top: 20px;
46 |   right: 20px;
47 |   z-index: 999;
48 |   cursor: pointer;
49 |   width: 16px;
50 |   height: 16px;
51 |   fill: var(--primary-color);
52 |   transition: all 0.3s;
53 | }
54 | #sidebar .title {
55 |   color: var(--primary-color);
56 |   font-size: 16px;
57 |   margin-top: 0;
58 | }
59 | #sidebar section {
60 |   padding: 16px 0;
61 | }
62 | #sidebar .section-header {
63 |   color: color-mix(in srgb, var(--primary-color), grey 20%);
64 |   font-weight: bold;
65 |   margin-block-end: 4px;
66 | }
67 | .node-info {
68 |   display: none;
69 | }
70 | .metadata .info {
71 |   font-size: 10px;
72 | }
73 | .info {
74 |   color: #888;
75 | }
76 | .info .value {
77 |   color: #666;
78 | }
79 | .footnote {
80 |   color: #888;
81 |   position: absolute;
82 |   left: 16px;
83 |   bottom: 8px;
84 | }
85 | .node {
86 |   border-radius: 3px;
87 |   padding: 1px 3px;
88 |   color: white;
89 |   background-color: var(--color)
90 | }
91 | .desc { color: var(--color) }
92 | .hit { --color: #339966 }
93 | .missed { --color: #ff6f00 }
94 | .ignored { --color: #888 }
95 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/framework.info.plist.template:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 5 | 	AvailableLibraries
 6 | 	
 7 | 		
 8 | 			BinaryPath
 9 | 			<%= module_name %>.framework/<%= module_name %>
10 | 			LibraryPath
11 | 			<%= module_name %>.framework
12 | 		
13 | 	
14 |   CFBundleExecutable
15 | 	<%= module_name %>
16 |   CFBundleName
17 |   <%= module_name %>
18 |   CFBundleIdentifier
19 |   com.xccache.<%= module_name %>
20 | 	CFBundlePackageType
21 | 	XFWK
22 | 	XCFrameworkFormatVersion
23 | 	1.0
24 | 
25 | 
26 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/framework.modulemap.template:
--------------------------------------------------------------------------------
1 | framework module <%= module_name %> {
2 |   umbrella header "<%= target %>-umbrella.h"
3 | 
4 |   export *
5 |   module * { export * }
6 | }
7 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/resource_bundle_accessor.m.template:
--------------------------------------------------------------------------------
 1 | #import 
 2 | 
 3 | @interface BundleFinder_<%= module_name %> : NSObject
 4 | @end
 5 | 
 6 | @implementation BundleFinder_<%= module_name %>
 7 | @end
 8 | 
 9 | NSBundle* <%= module_name %>_SWIFTPM_MODULE_BUNDLE() {
10 |   NSString *bundleName = @"<%= pkg %>_<%= target %>";
11 |   NSArray *candidates = @[
12 |     NSBundle.mainBundle.resourceURL,
13 |     [NSBundle bundleForClass:[BundleFinder_<%= module_name %> class]].resourceURL,
14 |     NSBundle.mainBundle.bundleURL,
15 |     [NSBundle.mainBundle.bundleURL URLByAppendingPathComponent:@"Frameworks/<%= target %>.framework"]
16 |   ];
17 | 
18 |   for (NSURL *candidate in candidates) {
19 |     NSURL *bundlePath = [candidate URLByAppendingPathComponent:[bundleName stringByAppendingString:@".bundle"]];
20 |     NSBundle *bundle = [NSBundle bundleWithURL:bundlePath];
21 |     if (bundle) {
22 |       return bundle;
23 |     }
24 |   }
25 |   [NSException raise:NSInternalInconsistencyException format:@"Unable to find bundle named %@", bundleName];
26 |   return nil;
27 | }
28 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/resource_bundle_accessor.swift.template:
--------------------------------------------------------------------------------
 1 | import Foundation
 2 | 
 3 | private class BundleFinder {}
 4 | 
 5 | extension Bundle {
 6 |   @available(iOS 8.0, *)
 7 |   static let module: Bundle = {
 8 |     let bundleName = "<%= pkg %>_<%= target %>"
 9 |     let candidates = [
10 |       Bundle.main.resourceURL,
11 |       Bundle(for: BundleFinder.self).resourceURL,
12 |       Bundle.main.bundleURL,
13 |       Bundle.main.bundleURL.appendingPathComponent("Frameworks/<%= target %>.framework")
14 |     ]
15 | 
16 |     for candidate in candidates {
17 |       let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
18 |       if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
19 |         return bundle
20 |       }
21 |     }
22 |     fatalError("unable to find bundle named \(bundleName)")
23 |   }()
24 | }
25 | 
--------------------------------------------------------------------------------
/lib/xccache/assets/templates/xccache.yml.template:
--------------------------------------------------------------------------------
1 | # Check out this doc for details
2 | # https://github.com/trinhngocthuyen/xccache/blob/main/docs/configuration.md
3 | 
--------------------------------------------------------------------------------
/lib/xccache/cache/cachemap.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core"
 2 | 
 3 | module XCCache
 4 |   module Cache
 5 |     class Cachemap < JSONRepresentable
 6 |       def depgraph_data
 7 |         raw["depgraph"] ||= {}
 8 |       end
 9 | 
10 |       def cache_data
11 |         raw["cache"] ||= {}
12 |       end
13 | 
14 |       def missed?(name)
15 |         missed.include?(name)
16 |       end
17 | 
18 |       def missed
19 |         get_cache_data(:missed)
20 |       end
21 | 
22 |       def stats
23 |         %i[hit missed ignored].to_h do |type|
24 |           count, total_count = get_cache_data(type).count, cache_data.count
25 |           percent = total_count.positive? ? (count.to_f * 100 / total_count).round : 0
26 |           [type, "#{percent}% (#{count}/#{total_count})"]
27 |         end
28 |       end
29 | 
30 |       def print_stats
31 |         verbose = Config.instance.verbose?
32 |         colors = { :hit => "green", :missed => "yellow" }
33 |         descs = %i[hit missed ignored].to_h do |type|
34 |           colorize = proc { |s| colors.key?(type) ? s.send(colors[type]).dark : s.dark }
35 |           items = get_cache_data(type)
36 |           percent = cache_data.count.positive? ? items.count.to_f / cache_data.count * 100 : 0
37 |           desc = "#{type} #{percent.round}% (#{items.count}/#{cache_data.count})"
38 |           desc = desc.capitalize if verbose
39 |           desc = "#{desc} #{colorize.call(items.to_s)}" if verbose && !items.empty?
40 |           [type, desc]
41 |         end
42 |         if verbose
43 |           UI.info <<~DESC
44 |             -------------------------------------------------------------------
45 |             Cache stats
46 |             #{descs.values.map { |s| "• #{s}" }.join("\n")}
47 |             -------------------------------------------------------------------
48 |           DESC
49 |         else
50 |           UI.info <<~DESC
51 |             -------------------------------------------------------------------
52 |             Cache stats: #{descs.values.join(', ')}
53 |             To see the full stats, use --verbose in the xccache command
54 |             -------------------------------------------------------------------
55 |           DESC
56 |         end
57 |       end
58 | 
59 |       def get_cache_data(type)
60 |         cache_data.select { |_, v| v == type }.keys
61 |       end
62 | 
63 |       def update_from_graph(graph)
64 |         cache_data =
65 |           graph["cache"]
66 |           .reject { |k, _| k.end_with?(".xccache") }
67 |           .to_h do |k, v|
68 |             next [k, :hit] if v
69 |             next [k, :ignored] if Config.instance.ignore?(k)
70 |             [k, :missed]
71 |           end
72 | 
73 |         deps = graph["deps"]
74 |         edges = deps.flat_map { |k, xs| xs.map { |v| { :source => k, :target => v } } }
75 |         nodes = deps.keys.map do |k|
76 |           {
77 |             :id => k,
78 |             :cache => cache_data[k],
79 |             :type => ("agg" if k.end_with?(".xccache")),
80 |             :binary => graph["cache"][k],
81 |           }
82 |         end
83 |         self.raw = {
84 |           "cache" => cache_data,
85 |           "depgraph" => { "nodes" => nodes, "edges" => edges },
86 |         }
87 |         save
88 |         print_stats
89 |       end
90 |     end
91 |   end
92 | end
93 | 
--------------------------------------------------------------------------------
/lib/xccache/command.rb:
--------------------------------------------------------------------------------
 1 | require "claide"
 2 | require "xccache/core/config"
 3 | require "xccache/swift/sdk"
 4 | 
 5 | module XCCache
 6 |   class Command < CLAide::Command
 7 |     include Config::Mixin
 8 |     Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 9 | 
10 |     self.abstract_command = true
11 |     self.default_subcommand = "use"
12 |     self.summary = "xccache - a build caching tool"
13 | 
14 |     attr_reader :install_options, :build_options
15 | 
16 |     def initialize(argv)
17 |       super
18 |       set_ansi_mode
19 |       config.verbose = verbose unless verbose.nil?
20 |       config.install_config = argv.option("config", "debug")
21 |       @install_options = {
22 |         :sdks => str_to_sdks(argv.option("sdk")),
23 |         :config => config.install_config,
24 |       }
25 |       @build_options = {
26 |         **@install_options,
27 |         :log_dir => argv.option("log-dir"),
28 |         :recursive => argv.flag?("recursive"),
29 |         :merge_slices => argv.flag?("merge-slices", true),
30 |         :library_evolution => argv.flag?("library-evolution"),
31 |       }
32 |     end
33 | 
34 |     def str_to_sdks(str)
35 |       (str || config.default_sdk).split(",").map { |s| Swift::Sdk.new(s) }
36 |     end
37 | 
38 |     private
39 | 
40 |     def set_ansi_mode
41 |       config.ansi = ansi_output?
42 |       return if ansi_output?
43 |       Colored2.disable!
44 |       String.send(:define_method, :colorize) { |s, _| s }
45 |     end
46 |   end
47 | end
48 | 
--------------------------------------------------------------------------------
/lib/xccache/command/base.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Options
 6 |       SDK = ["--sdk=foo,bar", "SDKs (iphonesimulator, iphoneos, macos, etc.)"].freeze
 7 |       CONFIG = ["--config=foo", "Configuration (debug, release) (default: debug)"].freeze
 8 |       LOG_DIR = ["--log-dir=foo", "Build log directory"].freeze
 9 |       MERGE_SLICES = [
10 |         "--merge-slices/--no-merge-slices",
11 |         "Whether to merge with existing slices/sdks in the xcframework (default: true)",
12 |       ].freeze
13 |       LIBRARY_EVOLUTION = [
14 |         "--library-evolution/--no-library-evolution",
15 |         "Whether to enable library evolution (build for distribution) (default: false)",
16 |       ].freeze
17 | 
18 |       def self.install_options
19 |         [SDK, CONFIG]
20 |       end
21 | 
22 |       def self.build_options
23 |         install_options + [LOG_DIR, MERGE_SLICES, LIBRARY_EVOLUTION]
24 |       end
25 |     end
26 |   end
27 | end
28 | 
--------------------------------------------------------------------------------
/lib/xccache/command/build.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Build < Command
 7 |       self.summary = "Build packages to xcframeworks"
 8 |       def self.options
 9 |         [
10 |           *Options.build_options,
11 |           ["--recursive", "Whether to build their recursive targets if cache-missed (default: false)"],
12 |         ].concat(super)
13 |       end
14 |       self.arguments = [
15 |         CLAide::Argument.new("TARGET", false, true),
16 |       ]
17 | 
18 |       def initialize(argv)
19 |         super
20 |         @targets = argv.arguments!
21 |       end
22 | 
23 |       def run
24 |         Installer::Build.new(ctx: self, targets: @targets).install!
25 |       end
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/command/cache.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | require "xccache/command/cache/clean"
 3 | require "xccache/command/cache/list"
 4 | 
 5 | module XCCache
 6 |   class Command
 7 |     class Cache < Command
 8 |       self.abstract_command = true
 9 |       self.summary = "Working with cache (list, clean, etc.)"
10 |     end
11 |   end
12 | end
13 | 
--------------------------------------------------------------------------------
/lib/xccache/command/cache/clean.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Command
 3 |     class Cache < Command
 4 |       class Clean < Cache
 5 |         self.summary = "Cleaning/purging cache"
 6 |         self.arguments = [CLAide::Argument.new("TARGET", false, true)]
 7 |         def self.options
 8 |           [
 9 |             ["--all", "Whether to remove all cache (default: false)"],
10 |             ["--dry", "Dry run - don't remove cache, just show what shall be removed (default: false)"],
11 |           ].concat(super)
12 |         end
13 | 
14 |         def initialize(argv)
15 |           super
16 |           @all = argv.flag?("all")
17 |           @dry = argv.flag?("dry")
18 |           @targets = argv.arguments!
19 |         end
20 | 
21 |         def run
22 |           to_remove = @targets.flat_map { |t| config.spm_cache_dir.glob("#{t}/*") }
23 |           to_remove = config.spm_cache_dir.glob("*/*") if @all
24 |           to_remove.each do |p|
25 |             UI.info("Removing #{p.basename.to_s.yellow}")
26 |             p.rmtree unless @dry
27 |           end
28 |         end
29 |       end
30 |     end
31 |   end
32 | end
33 | 
--------------------------------------------------------------------------------
/lib/xccache/command/cache/list.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Command
 3 |     class Cache < Command
 4 |       class List < Cache
 5 |         self.summary = "Listing cache"
 6 | 
 7 |         def run
 8 |           target_paths = config.spm_cache_dir.glob("*")
 9 |           target_paths.each do |target_path|
10 |             next if (paths = target_path.glob("*")).empty?
11 |             descs = paths.map { |p| "  #{p.basename.to_s.green}" }
12 |             UI.info <<~DESC
13 |               #{target_path.basename.to_s.cyan}:
14 |               #{descs.join('\n')}
15 |             DESC
16 |           end
17 |         end
18 |       end
19 |     end
20 |   end
21 | end
22 | 
--------------------------------------------------------------------------------
/lib/xccache/command/off.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Off < Command
 7 |       self.summary = "Force-switch to source mode for specific targets"
 8 |       self.arguments = [
 9 |         CLAide::Argument.new("TARGET", false, true),
10 |       ]
11 | 
12 |       def initialize(argv)
13 |         super
14 |         @targets = argv.arguments!
15 |       end
16 | 
17 |       def run
18 |         return if @targets.empty?
19 | 
20 |         UI.info("Will force-use source mode for targets: #{@targets}")
21 |         @targets.each { |t| config.ignore_list << "*/#{t}" }
22 |         Installer::Use.new(ctx: self).install!
23 |       end
24 |     end
25 |   end
26 | end
27 | 
--------------------------------------------------------------------------------
/lib/xccache/command/pkg.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | require "xccache/command/pkg/build"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Pkg < Command
 7 |       self.abstract_command = true
 8 |       self.summary = "Working with Swift packages"
 9 |     end
10 |   end
11 | end
12 | 
--------------------------------------------------------------------------------
/lib/xccache/command/pkg/build.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Pkg < Command
 6 |       class Build < Pkg
 7 |         self.summary = "Build a Swift package into an xcframework"
 8 |         def self.options
 9 |           [
10 |             *Options.build_options,
11 |             ["--out=foo", "Output directory for the xcframework"],
12 |             ["--checksum/no-checksum", "Whether to include checksum to the binary name"],
13 |           ].concat(super)
14 |         end
15 |         self.arguments = [
16 |           CLAide::Argument.new("TARGET", false, true),
17 |         ]
18 | 
19 |         def initialize(argv)
20 |           super
21 |           @targets = argv.arguments!
22 |           @out_dir = argv.option("out")
23 |           @include_checksum = argv.flag?("checksum")
24 |         end
25 | 
26 |         def run
27 |           pkg = SPM::Package.new
28 |           pkg.build(
29 |             targets: @targets,
30 |             config: @config,
31 |             out_dir: @out_dir,
32 |             checksum: @include_checksum,
33 |             **@build_options,
34 |           )
35 |         end
36 |       end
37 |     end
38 |   end
39 | end
40 | 
--------------------------------------------------------------------------------
/lib/xccache/command/remote.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | require "xccache/storage"
 3 | require "xccache/command/remote/pull"
 4 | require "xccache/command/remote/push"
 5 | 
 6 | module XCCache
 7 |   class Command
 8 |     class Remote < Command
 9 |       self.abstract_command = true
10 |       self.summary = "Working with remote cache"
11 |       def self.options
12 |         [
13 |           Options::CONFIG,
14 |           ["--branch=foo", "Cache branch (if using git) (default: main)"],
15 |         ].concat(super)
16 |       end
17 | 
18 |       def initialize(argv)
19 |         super
20 |         @branch = argv.option("branch", "main")
21 |       end
22 | 
23 |       def storage
24 |         @storage ||= create_storage
25 |       end
26 | 
27 |       private
28 | 
29 |       def create_storage
30 |         remote_config = config.remote_config
31 |         if (remote = remote_config["git"])
32 |           return GitStorage.new(branch: @branch, remote: remote)
33 |         elsif (s3_config = remote_config["s3"])
34 |           return S3Storage.new(uri: s3_config["uri"], creds_path: s3_config["creds"])
35 |         end
36 |         Storage.new
37 |       end
38 |     end
39 |   end
40 | end
41 | 
--------------------------------------------------------------------------------
/lib/xccache/command/remote/pull.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Remote < Command
 6 |       class Pull < Remote
 7 |         self.summary = "Pulling cache to local"
 8 | 
 9 |         def run
10 |           storage.pull
11 |         end
12 |       end
13 |     end
14 |   end
15 | end
16 | 
--------------------------------------------------------------------------------
/lib/xccache/command/remote/push.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Command
 5 |     class Remote < Command
 6 |       class Push < Remote
 7 |         self.summary = "Pushing cache to remote"
 8 | 
 9 |         def run
10 |           storage.push
11 |         end
12 |       end
13 |     end
14 |   end
15 | end
16 | 
--------------------------------------------------------------------------------
/lib/xccache/command/rollback.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Rollback < Command
 7 |       self.summary = "Roll back prebuilt cache for packages"
 8 | 
 9 |       def run
10 |         Installer::Rollback.new(ctx: self).install!
11 |       end
12 |     end
13 |   end
14 | end
15 | 
--------------------------------------------------------------------------------
/lib/xccache/command/use.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/installer"
 2 | require_relative "base"
 3 | 
 4 | module XCCache
 5 |   class Command
 6 |     class Use < Command
 7 |       self.summary = "Use prebuilt cache for packages"
 8 |       def self.options
 9 |         [
10 |           *Options.install_options,
11 |         ].concat(super)
12 |       end
13 | 
14 |       def run
15 |         Installer::Use.new(ctx: self).install!
16 |       end
17 |     end
18 |   end
19 | end
20 | 
--------------------------------------------------------------------------------
/lib/xccache/core.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/core/cacheable.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module Cacheable
 3 |     def cacheable(*method_names)
 4 |       method_names.each do |method_name|
 5 |         const_get(__cacheable_module_name).class_eval do
 6 |           define_method(method_name) do |*args, **kwargs|
 7 |             @_cache ||= {}
 8 |             @_cache[method_name] ||= {}
 9 |             @_cache[method_name][args.hash | kwargs.hash] ||=
10 |               method(method_name).super_method.call(*args, **kwargs)
11 |           end
12 |         end
13 |       end
14 |     end
15 | 
16 |     def __cacheable_module_name
17 |       "#{name}_Cacheable".gsub(':', '_')
18 |     end
19 | 
20 |     def self.included(base)
21 |       base.extend(self)
22 | 
23 |       module_name = base.send(:__cacheable_module_name)
24 |       remove_const(module_name) if const_defined?(module_name)
25 |       base.prepend(const_set(module_name, Module.new))
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/core/config.rb:
--------------------------------------------------------------------------------
  1 | require "xcodeproj"
  2 | require "xccache/core/syntax/yml"
  3 | 
  4 | module XCCache
  5 |   class Config < YAMLRepresentable
  6 |     module Mixin
  7 |       def config
  8 |         Config.instance
  9 |       end
 10 |     end
 11 | 
 12 |     def self.instance
 13 |       @instance ||= new(Pathname("xccache.yml").expand_path)
 14 |     end
 15 | 
 16 |     attr_accessor :verbose, :ansi
 17 |     alias verbose? verbose
 18 |     alias ansi? ansi
 19 | 
 20 |     # To distinguish if it's within an installation, or standalone like `xccache pkg build`
 21 |     attr_accessor :in_installation
 22 |     alias in_installation? in_installation
 23 | 
 24 |     attr_writer :install_config
 25 | 
 26 |     def ensure_file!
 27 |       Template.new("xccache.yml").render(save_to: path) unless path.exist?
 28 |     end
 29 | 
 30 |     def install_config
 31 |       @install_config || "debug"
 32 |     end
 33 | 
 34 |     def sandbox
 35 |       @sandbox = Dir.prepare("xccache").expand_path
 36 |     end
 37 | 
 38 |     def spm_sandbox
 39 |       @spm_sandbox ||= Dir.prepare(sandbox / "packages").expand_path
 40 |     end
 41 | 
 42 |     def spm_local_pkgs_dir
 43 |       @spm_local_pkgs_dir ||= Dir.prepare(spm_sandbox / "local")
 44 |     end
 45 | 
 46 |     def spm_xcconfigs_dir
 47 |       @spm_xcconfigs_dir ||= Dir.prepare(spm_sandbox / "xcconfigs")
 48 |     end
 49 | 
 50 |     def spm_cache_dir
 51 |       @spm_cache_dir ||= Dir.prepare(Pathname("~/.xccache/#{install_config}").expand_path)
 52 |     end
 53 | 
 54 |     def spm_binaries_dir
 55 |       @spm_binaries_dir ||= Dir.prepare(spm_sandbox / "binaries")
 56 |     end
 57 | 
 58 |     def spm_build_dir
 59 |       @spm_build_dir ||= spm_umbrella_sandbox / ".build"
 60 |     end
 61 | 
 62 |     def spm_artifacts_dir
 63 |       @spm_artifacts_dir ||= spm_build_dir / "artifacts"
 64 |     end
 65 | 
 66 |     def spm_proxy_sandbox
 67 |       @spm_proxy_sandbox ||= Dir.prepare(spm_sandbox / "proxy")
 68 |     end
 69 | 
 70 |     def spm_umbrella_sandbox
 71 |       @spm_umbrella_sandbox ||= Dir.prepare(spm_sandbox / "umbrella")
 72 |     end
 73 | 
 74 |     def spm_metadata_dir
 75 |       @spm_metadata_dir ||= Dir.prepare(spm_sandbox / "metadata")
 76 |     end
 77 | 
 78 |     def lockfile
 79 |       @lockfile ||= Lockfile.new(Pathname("xccache.lock").expand_path)
 80 |     end
 81 | 
 82 |     def cachemap
 83 |       require "xccache/cache/cachemap"
 84 |       @cachemap ||= Cache::Cachemap.new(sandbox / "cachemap.json")
 85 |     end
 86 | 
 87 |     def projects
 88 |       @projects ||= Pathname(".").glob("*.xcodeproj").map do |p|
 89 |         Xcodeproj::Project.open(p)
 90 |       end
 91 |     end
 92 | 
 93 |     def project_targets
 94 |       projects.flat_map(&:targets)
 95 |     end
 96 | 
 97 |     def remote_config
 98 |       pick_per_install_config(raw["remote"] || {})
 99 |     end
100 | 
101 |     def ignore_list
102 |       raw["ignore"] || []
103 |     end
104 | 
105 |     def ignore?(item)
106 |       return true if ignore_local? && lockfile.local_pkg_slugs.include?(item.split("/").first)
107 |       ignore_list.any? { |p| File.fnmatch(p, item) }
108 |     end
109 | 
110 |     def ignore_local?
111 |       raw["ignore_local"]
112 |     end
113 | 
114 |     def ignore_build_errors?
115 |       raw["ignore_build_errors"]
116 |     end
117 | 
118 |     def keep_pkgs_in_project?
119 |       raw["keep_pkgs_in_project"]
120 |     end
121 | 
122 |     def default_sdk
123 |       raw["default_sdk"] || "iphonesimulator"
124 |     end
125 | 
126 |     private
127 | 
128 |     def pick_per_install_config(hash)
129 |       hash[install_config] || hash["default"] || {}
130 |     end
131 |   end
132 | end
133 | 
--------------------------------------------------------------------------------
/lib/xccache/core/error.rb:
--------------------------------------------------------------------------------
1 | module XCCache
2 |   class BaseError < StandardError
3 |   end
4 | 
5 |   class GeneralError < BaseError
6 |   end
7 | end
8 | 
--------------------------------------------------------------------------------
/lib/xccache/core/git.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Git
 3 |     attr_reader :root
 4 | 
 5 |     def initialize(root)
 6 |       @root = Pathname(root).expand_path
 7 |     end
 8 | 
 9 |     def run(*args, **kwargs)
10 |       Sh.run("git -C #{root}", *args, **kwargs)
11 |     end
12 | 
13 |     def sha
14 |       run("rev-parse --short HEAD", capture: true, log_cmd: false)[0].strip
15 |     end
16 | 
17 |     def clean?
18 |       status("--porcelain", capture: true, log_cmd: false)[0].empty?
19 |     end
20 | 
21 |     def init?
22 |       !root.glob(".git").empty?
23 |     end
24 | 
25 |     %i[init checkout fetch pull push clean add commit branch remote switch status].each do |name|
26 |       define_method(name) do |*args, **kwargs|
27 |         run(name, *args, **kwargs)
28 |       end
29 |     end
30 |   end
31 | end
32 | 
--------------------------------------------------------------------------------
/lib/xccache/core/hash.rb:
--------------------------------------------------------------------------------
 1 | class Hash
 2 |   def deep_merge(other, uniq_block: nil, sort_block: nil, &block)
 3 |     dup.deep_merge!(other, uniq_block: uniq_block, sort_block: sort_block, &block)
 4 |   end
 5 | 
 6 |   def deep_merge!(other, uniq_block: nil, sort_block: nil, &block)
 7 |     merge!(other) do |key, this_val, other_val|
 8 |       result = if this_val.is_a?(Hash) && other_val.is_a?(Hash)
 9 |                  this_val.deep_merge(other_val, uniq_block: uniq_block, sort_block: sort_block, &block)
10 |                elsif this_val.is_a?(Array) && other_val.is_a?(Array)
11 |                  this_val + other_val
12 |                elsif block_given?
13 |                  block.call(key, this_val, other_val)
14 |                else
15 |                  other_val
16 |                end
17 | 
18 |       # uniq by block, prefer updates
19 |       result = result.reverse.uniq(&uniq_block).reverse if uniq_block && result.is_a?(Array)
20 |       result = result.sort_by(&sort_block) if sort_block && result.is_a?(Array)
21 |       result
22 |     end
23 |   end
24 | end
25 | 
--------------------------------------------------------------------------------
/lib/xccache/core/live_log.rb:
--------------------------------------------------------------------------------
 1 | require "monitor"
 2 | require "tty-cursor"
 3 | require "tty-screen"
 4 | 
 5 | module XCCache
 6 |   class LiveLog
 7 |     include UI::Mixin
 8 |     CURSOR_LOCK = Monitor.new
 9 | 
10 |     attr_reader :output, :max_lines, :lines, :cursor, :tee
11 | 
12 |     def initialize(**options)
13 |       @output = options[:output] || $stdout
14 |       @max_lines = options[:max_lines] || 5
15 |       @n_sticky = 0
16 |       @lines = []
17 |       @cursor = TTY::Cursor
18 |       @screen = TTY::Screen
19 |       @tee = options[:tee]
20 |     end
21 | 
22 |     def clear
23 |       commit do
24 |         output.print(cursor.clear_lines(lines.count + @n_sticky))
25 |         @lines = []
26 |         @n_sticky = 0
27 |       end
28 |     end
29 | 
30 |     def puts(line, sticky: false)
31 |       commit do
32 |         output.print(cursor.clear_lines(lines.count + 1))
33 |         if sticky
34 |           @n_sticky += 1
35 |           output.puts(truncated(line))
36 |         else
37 |           lines.shift if lines.count >= max_lines
38 |           lines << truncated(line)
39 |         end
40 |         output.puts(lines) # print non-sticky content
41 |       end
42 |       File.open(tee, "a") { |f| f << "#{line}\n" } if tee
43 |     end
44 | 
45 |     def capture(header)
46 |       header_start = header.magenta.bold
47 |       header_success = "#{header} ✔".green.bold
48 |       header_error = "#{header} ✖".red.bold
49 |       puts(header_start, sticky: true)
50 |       yield if block_given?
51 |       clear
52 |       update_header(header_success)
53 |     rescue StandardError => e
54 |       update_header(header_error)
55 |       raise e
56 |     end
57 | 
58 |     private
59 | 
60 |     def update_header(header)
61 |       commit do
62 |         n = lines.count + @n_sticky
63 |         output.print(cursor.up(n) + header + cursor.column(0) + cursor.down(n))
64 |       end
65 |     end
66 | 
67 |     def commit
68 |       CURSOR_LOCK.synchronize do
69 |         yield
70 |         output.flush
71 |       end
72 |     end
73 | 
74 |     def truncated(msg)
75 |       msg.length > @screen.width ? "#{msg[...@screen.width - 3]}..." : msg
76 |     end
77 | 
78 |     def ui_cls
79 |       self
80 |     end
81 |   end
82 | end
83 | 
--------------------------------------------------------------------------------
/lib/xccache/core/lockfile.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/syntax/json"
 2 | 
 3 | module XCCache
 4 |   class Lockfile < JSONRepresentable
 5 |     class Pkg < Hash
 6 |       def self.from_h(h)
 7 |         Pkg.new.merge(h)
 8 |       end
 9 | 
10 |       def key
11 |         @key ||= ["repositoryURL", "path_from_root", "relative_path"].find { |x| key?(x) }
12 |       end
13 | 
14 |       def id
15 |         self[key]
16 |       end
17 | 
18 |       def local?
19 |         key != "repositoryURL"
20 |       end
21 | 
22 |       def slug
23 |         @slug ||= File.basename(id, ".*")
24 |       end
25 | 
26 |       def relative_path_from_dir(dir)
27 |         return id if key == "relative_path"
28 |         (Pathname.pwd / id).relative_path_from(dir) if key == "path_from_root"
29 |       end
30 | 
31 |       def local_absolute_path
32 |         Pathname.pwd / self["path_from_root"] if local?
33 |       end
34 |     end
35 | 
36 |     def hash_for_project(project)
37 |       raw[project.display_name] ||= {}
38 |     end
39 | 
40 |     def product_dependencies_by_targets
41 |       @product_dependencies_by_targets ||= raw.values.map { |h| h["dependencies"] }.reduce { |acc, h| acc.merge(h) }
42 |     end
43 | 
44 |     def deep_merge!(hash)
45 |       raw.deep_merge!(
46 |         hash,
47 |         uniq_block: proc { |h| h.is_a?(Hash) ? Pkg.from_h(h).id || h : h },
48 |         sort_block: proc { |x| x.to_s.downcase },
49 |       )
50 |       # After deep_merge, clear property cache
51 |       (instance_variables - %i[@path @raw]).each do |ivar|
52 |         remove_instance_variable(ivar)
53 |       end
54 |     end
55 | 
56 |     def pkgs
57 |       @pkgs ||= raw.values.flat_map { |h| h["packages"] || [] }.map { |h| Pkg.from_h(h) }
58 |     end
59 | 
60 |     def local_pkgs
61 |       @local_pkgs ||= pkgs.select(&:local?).uniq
62 |     end
63 | 
64 |     def local_pkg_slugs
65 |       @local_pkg_slugs ||= local_pkgs.map(&:slug).uniq
66 |     end
67 | 
68 |     def known_product_dependencies
69 |       raw.empty? ? [] : product_dependencies.reject { |d| File.dirname(d) == "__unknown__" }
70 |     end
71 | 
72 |     def product_dependencies
73 |       @product_dependencies ||= product_dependencies_by_targets.values.flatten.uniq
74 |     end
75 | 
76 |     def targets_data
77 |       @targets_data ||= product_dependencies_by_targets.transform_keys { |k| "#{k}.xccache" }
78 |     end
79 | 
80 |     def verify!
81 |       known_slugs = pkgs.map(&:slug)
82 |       unknown = product_dependencies.reject { |d| known_slugs.include?(File.dirname(d)) }
83 |       return if unknown.empty?
84 | 
85 |       UI.error! <<~DESC
86 |         Unknown product dependencies at #{path}:
87 | 
88 |         #{unknown.sort.map { |d| "  • #{d}" }.join("\n")}
89 | 
90 |         Refer to this doc for how to resolve this issue:
91 |           https://github.com/trinhngocthuyen/xccache/blob/main/docs/troubleshooting.md#unknown-product-dependencies
92 |       DESC
93 |     end
94 |   end
95 | end
96 | 
--------------------------------------------------------------------------------
/lib/xccache/core/log.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/config"
 2 | require "colored2"
 3 | 
 4 | module XCCache
 5 |   module UI
 6 |     @indent = 0
 7 | 
 8 |     module Mixin
 9 |       include Config::Mixin
10 |       attr_accessor :indent
11 | 
12 |       def section(title, timing: false)
13 |         start = Time.new if timing
14 |         ui_cls.puts(title)
15 |         self.indent += 2
16 |         res = yield if block_given?
17 |         self.indent -= 2
18 |         if timing
19 |           duration = (Time.new - start).to_i
20 |           duration = if duration < 60 then "#{duration}s"
21 |                      elsif duration < 60 * 60 then "#{duration / 60}m"
22 |                      else
23 |                        "#{duration / 3600}h"
24 |                      end
25 |           ui_cls.puts("-> Finished: #{title.dark} (#{duration})")
26 |         end
27 |         res
28 |       end
29 | 
30 |       def message(message)
31 |         ui_cls.puts(message) if config.verbose?
32 |       end
33 | 
34 |       def info(message)
35 |         ui_cls.puts(message)
36 |       end
37 | 
38 |       def warn(message)
39 |         ui_cls.puts(message.yellow)
40 |       end
41 | 
42 |       def error(message)
43 |         ui_cls.puts("[ERROR] #{message}".red)
44 |       end
45 | 
46 |       def error!(message)
47 |         error(message)
48 |         raise GeneralError, message
49 |       end
50 | 
51 |       def puts(message)
52 |         $stdout.puts("#{' ' * self.indent}#{message}")
53 |       end
54 | 
55 |       private
56 | 
57 |       def ui_cls
58 |         UI
59 |       end
60 |     end
61 | 
62 |     class << self
63 |       include Mixin
64 |     end
65 |   end
66 | end
67 | 
--------------------------------------------------------------------------------
/lib/xccache/core/parallel.rb:
--------------------------------------------------------------------------------
 1 | require "parallel"
 2 | 
 3 | class Array
 4 |   def parallel_map(options = {})
 5 |     # By default, use in_threads (IO-bound tasks)
 6 |     default = {}
 7 |     default[:in_threads] = Parallel.processor_count unless options.key?(:in_processes)
 8 |     Parallel.map(self, { **default, **options }) { |x| yield x if block_given? }
 9 |   end
10 | end
11 | 
--------------------------------------------------------------------------------
/lib/xccache/core/sh.rb:
--------------------------------------------------------------------------------
 1 | require "open3"
 2 | require "xccache/core/config"
 3 | require "xccache/core/log"
 4 | require "xccache/core/error"
 5 | 
 6 | module XCCache
 7 |   class Sh
 8 |     class ExecError < BaseError
 9 |     end
10 | 
11 |     class << self
12 |       include Config::Mixin
13 | 
14 |       def capture_output(cmd)
15 |         run(cmd, capture: true, log_cmd: false)[0].strip
16 |       end
17 | 
18 |       def run(*args, env: nil, **options)
19 |         cmd = args.join(" ")
20 | 
21 |         out, err = [], []
22 |         handle_out = options[:handle_out] || proc { |l| out << l }
23 |         handle_err = options[:handle_err] || proc { |l| err << l }
24 |         if (live_log = options[:live_log])
25 |           handle_out = proc { |l| live_log.puts(l) }
26 |           handle_err = proc { |l| live_log.puts(l) }
27 |           live_log.puts("$ #{cmd}") if options[:log_cmd] != false
28 |         elsif options[:log_cmd] != false
29 |           UI.message("$ #{cmd}".cyan.dark)
30 |         end
31 | 
32 |         use_popen = options[:capture] || options[:handle_out] || options[:handle_err] || options[:live_log]
33 |         return system(cmd) || (raise GeneralError, "Command '#{cmd}' failed") unless use_popen
34 | 
35 |         popen3_args = env ? [env, cmd] : [cmd]
36 |         Open3.popen3(*popen3_args) do |_stdin, stdout, stderr, wait_thr|
37 |           stdout_thread = Thread.new { stdout.each { |l| handle_out.call(l.strip) } }
38 |           stderr_thread = Thread.new { stderr.each { |l| handle_err.call(l.strip) } }
39 |           [stdout_thread, stderr_thread].each(&:join)
40 |           result = wait_thr.value
41 |           result.exitstatus
42 |           raise ExecError, "Command '#{cmd}' failed with status: #{result.exitstatus}" unless result.success?
43 |         end
44 |         [out.join("\n"), err.join("\n")]
45 |       end
46 | 
47 |       private
48 | 
49 |       def log_cmd(cmd, live_log: nil)
50 |         return live_log.puts("$ #{cmd}") if live_log
51 |         UI.message("$ #{cmd}".cyan.dark)
52 |       end
53 |     end
54 |   end
55 | end
56 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/hash.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class HashRepresentable
 3 |     attr_reader :path
 4 |     attr_accessor :raw
 5 | 
 6 |     def initialize(path, raw: nil)
 7 |       @path = path
 8 |       @raw = raw || load || {}
 9 |     end
10 | 
11 |     def reload
12 |       @raw = load || {}
13 |     end
14 | 
15 |     def load
16 |       raise NotImplementedError
17 |     end
18 | 
19 |     def merge!(other)
20 |       raw.merge!(other)
21 |     end
22 | 
23 |     def save(to: nil)
24 |       raise NotImplementedError
25 |     end
26 | 
27 |     def [](key)
28 |       raw[key]
29 |     end
30 | 
31 |     def []=(key, value)
32 |       raw[key] = value
33 |     end
34 |   end
35 | end
36 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/json.rb:
--------------------------------------------------------------------------------
 1 | require "json"
 2 | require_relative "hash"
 3 | 
 4 | module XCCache
 5 |   class JSONRepresentable < HashRepresentable
 6 |     def load
 7 |       JSON.parse(path.read) if path.exist?
 8 |     rescue StandardError
 9 |       {}
10 |     end
11 | 
12 |     def save(to: nil)
13 |       (to || path).write(JSON.pretty_generate(raw))
14 |     end
15 |   end
16 | end
17 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/plist.rb:
--------------------------------------------------------------------------------
 1 | require "cfpropertylist"
 2 | require_relative "hash"
 3 | 
 4 | module XCCache
 5 |   class PlistRepresentable < HashRepresentable
 6 |     def load
 7 |       plist = CFPropertyList::List.new(file: path)
 8 |       CFPropertyList.native_types(plist.value)
 9 |     rescue StandardError
10 |       {}
11 |     end
12 | 
13 |     def save(to: nil)
14 |       raise NotImplementedError
15 |     end
16 |   end
17 | end
18 | 
--------------------------------------------------------------------------------
/lib/xccache/core/syntax/yml.rb:
--------------------------------------------------------------------------------
 1 | require "yaml"
 2 | require_relative "hash"
 3 | 
 4 | module XCCache
 5 |   class YAMLRepresentable < HashRepresentable
 6 |     def load
 7 |       YAML.safe_load(path.read) if path.exist?
 8 |     rescue StandardError
 9 |       {}
10 |     end
11 | 
12 |     def save(to: nil)
13 |       (to || path).write(raw.to_yaml)
14 |     end
15 |   end
16 | end
17 | 
--------------------------------------------------------------------------------
/lib/xccache/core/system.rb:
--------------------------------------------------------------------------------
 1 | require "digest"
 2 | require "mkmf"
 3 | require "tmpdir"
 4 | 
 5 | class String
 6 |   def c99extidentifier
 7 |     gsub(/[^a-zA-Z0-9.]/, "_")
 8 |   end
 9 | end
10 | 
11 | class File
12 |   def self.which(bin)
13 |     find_executable0(bin)
14 |   end
15 | end
16 | 
17 | class Dir
18 |   def self.prepare(dir, clean: false, expand: false)
19 |     dir = Pathname(dir)
20 |     dir = dir.expand_path if expand
21 |     dir.rmtree if clean && dir.exist?
22 |     dir.mkpath
23 |     dir
24 |   end
25 | 
26 |   def self.create_tmpdir
27 |     dir = Pathname(Dir.mktmpdir("xccache"))
28 |     res = block_given? ? (yield dir) : dir
29 |     dir.rmtree if block_given?
30 |     res
31 |   end
32 | 
33 |   def self.git?(dir)
34 |     XCCache::Sh.capture_output("git -C #{dir} rev-parse --git-dir") == ".git"
35 |   end
36 | end
37 | 
38 | class Pathname
39 |   def symlink_to(dst)
40 |     dst = Pathname(dst)
41 |     dst.rmtree if dst.symlink?
42 |     dst.parent.mkpath
43 |     File.symlink(expand_path, dst)
44 |   end
45 | 
46 |   def copy(to: nil, to_dir: nil)
47 |     dst = to || (Pathname(to_dir) / basename)
48 |     dst.rmtree if dst.exist? || dst.symlink?
49 |     dst.parent.mkpath
50 |     FileUtils.copy_entry(self, dst)
51 |     dst
52 |   end
53 | 
54 |   def checksum
55 |     hasher = Digest::SHA256.new
56 |     glob("**/*").reject { |p| p.directory? || p.symlink? }.sort.each do |p|
57 |       p.open("rb") do |f|
58 |         while (chunk = f.read(65_536)) # Read 64KB chunks
59 |           hasher.update(chunk)
60 |         end
61 |       end
62 |     end
63 |     hasher.hexdigest[...8]
64 |   end
65 | end
66 | 
--------------------------------------------------------------------------------
/lib/xccache/installer.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/spm"
  2 | require "xccache/installer/integration"
  3 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
  4 | 
  5 | module XCCache
  6 |   class Installer
  7 |     include PkgMixin
  8 |     include IntegrationMixin
  9 | 
 10 |     def initialize(options = {})
 11 |       ctx = options[:ctx]
 12 |       raise GeneralError, "Missing context (Command) for #{self.class}" if ctx.nil?
 13 |       @umbrella_pkg = options[:umbrella_pkg]
 14 |       @install_options = ctx.install_options
 15 |       @build_options = ctx.build_options
 16 |     end
 17 | 
 18 |     def perform_install
 19 |       verify_projects!
 20 |       recreate_config_dirs
 21 |       projects.each { |project| migrate_umbrella_to_proxy(project) }
 22 |       UI.message("Using cache dir: #{config.spm_cache_dir}")
 23 |       config.ensure_file!
 24 |       config.in_installation = true
 25 |       sync_lockfile
 26 |       proxy_pkg.prepare(@install_options)
 27 | 
 28 |       yield if block_given?
 29 | 
 30 |       gen_supporting_files
 31 |       projects.each do |project|
 32 |         add_xccache_refs_to_project(project)
 33 |         inject_xcconfig_to_project(project)
 34 |       end
 35 |       gen_cachemap_viz
 36 |     end
 37 | 
 38 |     def sync_lockfile
 39 |       UI.info("Syncing lockfile")
 40 |       known_dependencies = lockfile.known_product_dependencies
 41 |       update_projects do |project|
 42 |         lockfile.deep_merge!(
 43 |           project.display_name => lockfile_hash_for_project(project, known_dependencies)
 44 |         )
 45 |       end
 46 |       lockfile.save
 47 |       lockfile.verify!
 48 |     end
 49 | 
 50 |     def lockfile
 51 |       config.lockfile
 52 |     end
 53 | 
 54 |     def projects
 55 |       config.projects
 56 |     end
 57 | 
 58 |     def save_projects
 59 |       yield if block_given?
 60 |       projects.each(&:save)
 61 |     end
 62 | 
 63 |     def update_projects
 64 |       projects.each do |project|
 65 |         yield project if block_given?
 66 |         project.save
 67 |       end
 68 |     end
 69 | 
 70 |     private
 71 | 
 72 |     def lockfile_hash_for_project(project, known_dependencies)
 73 |       deps_by_targets = project.targets.to_h do |target|
 74 |         deps = target.non_xccache_pkg_product_dependencies.map do |dep|
 75 |           next dep.full_name unless dep.pkg.nil?
 76 |           known = known_dependencies.find { |x| File.basename(x) == dep.product_name }
 77 |           UI.warn("-> Assuming #{known} for #{dep.full_name}".dark) if known
 78 |           known || dep.full_name
 79 |         end
 80 |         [target.name, deps.sort]
 81 |       end
 82 |       {
 83 |         "packages" => project.non_xccache_pkgs.map(&:to_h),
 84 |         "dependencies" => deps_by_targets,
 85 |         "platforms" => platforms_for_project(project),
 86 |       }
 87 |     end
 88 | 
 89 |     def platforms_for_project(project)
 90 |       project
 91 |         .targets.select(&:platform_name)
 92 |         .map { |t| [t.platform_name.to_s, t.deployment_target] }
 93 |         .sort.reverse.to_h # sort descendingly -> min value is picked for the hash
 94 |     end
 95 | 
 96 |     def verify_projects!
 97 |       raise "No projects detected. Are you running on the correct project directory?" if projects.empty?
 98 |     end
 99 | 
100 |     def add_xccache_refs_to_project(project)
101 |       group = project.xccache_config_group
102 |       add_file = proc { |p| group[p.basename.to_s] || group.new_file(p) }
103 |       add_file.call(config.spm_proxy_sandbox / "Package.swift")
104 |       add_file.call(config.lockfile.path)
105 |       add_file.call(config.path)
106 |       group.ensure_synced_group(name: "local-packages", path: config.spm_local_pkgs_dir)
107 |     end
108 | 
109 |     def inject_xcconfig_to_project(project)
110 |       group = project.xccache_config_group.ensure_synced_group(name: "xcconfigs", path: config.spm_xcconfigs_dir)
111 |       project.targets.each do |target|
112 |         xcconfig_path = config.spm_xcconfigs_dir / "#{target.name}.xcconfig"
113 |         target.build_configurations.each do |build_config|
114 |           if (existing = build_config.base_configuration_xcconfig)
115 |             next if existing.path == xcconfig_path
116 | 
117 |             relative_path = xcconfig_path.relative_path_from(existing.path.parent)
118 |             next if existing.includes.include?(relative_path.to_s)
119 | 
120 |             UI.info("Injecting base configuration for #{target} (#{build_config}) (at: #{existing.path})")
121 |             existing.path.write <<~DESC
122 |               #include "#{relative_path}" // Injected by xccache, for prebuilt macros support
123 |               #{existing.path.read.strip}
124 |             DESC
125 |           else
126 |             UI.info("Setting base configuration #{target} (#{build_config}) as #{xcconfig_path}")
127 |             build_config.base_configuration_reference_anchor = group
128 |             build_config.base_configuration_reference_relative_path = xcconfig_path.basename.to_s
129 |           end
130 |         end
131 |       end
132 |     end
133 | 
134 |     def migrate_umbrella_to_proxy(project)
135 |       return unless project.xccache_pkg&.slug == "umbrella"
136 | 
137 |       UI.info <<~DESC
138 |         Migrating from umbrella to proxy for project #{project.display_name}
139 |         You should notice changes in project files from xccache/package/umbrella -> xccache/package/proxy.
140 |         Don't worry, this is expected.
141 |       DESC
142 |         .yellow
143 | 
144 |       project.xccache_pkg.relative_path = "xccache/packages/proxy"
145 |       if (group = project.xccache_config_group) && (ref = group["Package.swift"])
146 |         ref.path = "xccache/packages/proxy/Package.swift"
147 |       end
148 |     end
149 | 
150 |     def recreate_config_dirs
151 |       [
152 |         config.spm_binaries_dir,
153 |         config.spm_local_pkgs_dir,
154 |         config.spm_xcconfigs_dir,
155 |         config.spm_metadata_dir,
156 |       ].each { |p| Dir.prepare(p, clean: true) }
157 |     end
158 |   end
159 | end
160 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/build.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Installer
 5 |     class Build < Installer
 6 |       def initialize(options = {})
 7 |         super
 8 |         @targets = options[:targets]
 9 |       end
10 | 
11 |       def install!
12 |         perform_install do
13 |           build(
14 |             targets: @targets,
15 |             out_dir: config.spm_cache_dir,
16 |             symlinks_dir: config.spm_binaries_dir,
17 |             checksum: true,
18 |             **@build_options,
19 |           )
20 |           proxy_pkg.gen_proxy # Regenerate proxy to apply new cache after build
21 |         end
22 |       end
23 |     end
24 |   end
25 | end
26 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration.rb:
--------------------------------------------------------------------------------
 1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 2 | 
 3 | module XCCache
 4 |   class Installer
 5 |     module IntegrationMixin
 6 |       include VizIntegrationMixin
 7 |       include DescsIntegrationMixin
 8 |       include BuildIntegrationMixin
 9 |       include SupportingFilesIntegrationMixin
10 |     end
11 |   end
12 | end
13 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/build.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module BuildIntegrationMixin
 4 |       def build(options = {})
 5 |         to_build = targets_to_build(options)
 6 |         return UI.warn("Detected no targets to build among cache-missed targets") if to_build.empty?
 7 | 
 8 |         UI.info("-> Targets to build: #{to_build.to_s.bold}")
 9 |         umbrella_pkg.build(**options, targets: to_build)
10 |       end
11 | 
12 |       def targets_to_build(options)
13 |         items = (options[:targets] || []).map { |x| File.basename(x) }
14 |         items = config.cachemap.missed.map { |x| File.basename(x) } if items.empty?
15 |         targets = items.map { |x| umbrella_pkg.get_target(x) }
16 | 
17 |         if options[:recursive]
18 |           UI.message("Will include cache-missed recursive targets")
19 |           targets += targets.flat_map do |t|
20 |             t.recursive_targets.select { |x| config.cachemap.missed?(x.full_name) }
21 |           end
22 |         end
23 |         # TODO: Sort by number of dependents
24 |         targets.map(&:full_name).uniq
25 |       end
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/descs.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module DescsIntegrationMixin
 4 |       def xccache_desc
 5 |         @xccache_desc ||= desc_of("xccache")
 6 |       end
 7 | 
 8 |       def targets_of_products(products)
 9 |         products = [products] if products.is_a?(String)
10 |         products.flat_map { |x| desc_of(x).targets_of_products(File.basename(x)) }
11 |       end
12 | 
13 |       def dependency_targets_of_products(products)
14 |         products = [products] if products.is_a?(String)
15 |         products.flat_map { |p| @dependency_targets_by_products[p] || [p] }.uniq
16 |       end
17 | 
18 |       def desc_of(d)
19 |         descs_by_name[d.split("/").first]
20 |       end
21 | 
22 |       def binary_targets
23 |         descs_by_name.values.flatten.uniq.flat_map(&:binary_targets)
24 |       end
25 |     end
26 |   end
27 | end
28 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/supporting_files.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module SupportingFilesIntegrationMixin
 4 |       def gen_supporting_files
 5 |         UI.section("Generating supporting files") do
 6 |           gen_xcconfigs
 7 |         end
 8 |       end
 9 | 
10 |       private
11 | 
12 |       def gen_xcconfigs
13 |         macros_config_by_targets.each do |target, hash|
14 |           xcconfig_path = config.spm_xcconfigs_dir / "#{target}.xcconfig"
15 |           UI.message("XCConfig of target #{target} at: #{xcconfig_path}")
16 |           Xcodeproj::Config.new(hash).save_as(xcconfig_path)
17 |         end
18 |       end
19 | 
20 |       def macros_config_by_targets
21 |         proxy_pkg.graph["macros"].to_h do |target, paths|
22 |           swift_flags = paths.map { |p| "-load-plugin-executable #{p}##{File.basename(p, '.*')}" }
23 |           hash = { "OTHER_SWIFT_FLAGS" => "$(inherited) #{swift_flags.join(' ')}" }
24 |           [File.basename(target, ".*"), hash]
25 |         end
26 |       end
27 |     end
28 |   end
29 | end
30 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/integration/viz.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     module VizIntegrationMixin
 4 |       def gen_cachemap_viz
 5 |         stats = config.cachemap.stats
 6 |         html_path = config.sandbox / "cachemap.html"
 7 |         js_path = Dir.prepare(config.sandbox / "assets") / "cachemap.js"
 8 |         css_path = config.sandbox / "assets" / "style.css"
 9 | 
10 |         root_dir = Pathname(".").expand_path
11 |         to_relative = proc do |p|
12 |           p.to_s.start_with?(root_dir.to_s) ? p.relative_path_from(root_dir).to_s : p.to_s
13 |         end
14 | 
15 |         UI.info("Cachemap visualization: #{html_path}")
16 |         Template.new("cachemap.html").render(
17 |           {
18 |             :root_dir => root_dir.to_s,
19 |             :root_dir_short => root_dir.basename.to_s,
20 |             :lockfile_path => config.lockfile.path.to_s,
21 |             :lockfile_path_short => to_relative.call(config.lockfile.path),
22 |             :binaries_dir => config.spm_binaries_dir.to_s,
23 |             :binaries_dir_short => to_relative.call(config.spm_binaries_dir),
24 |             :desc_hit => stats[:hit],
25 |             :desc_missed => stats[:missed],
26 |             :desc_ignored => stats[:ignored],
27 |           },
28 |           save_to: html_path
29 |         )
30 |         Template.new("cachemap.js").render(
31 |           { :json => JSON.pretty_generate(config.cachemap.depgraph_data) },
32 |           save_to: js_path
33 |         )
34 |         Template.new("cachemap.style.css").render(save_to: css_path)
35 |       end
36 |     end
37 |   end
38 | end
39 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/rollback.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Installer
 3 |     class Rollback < Installer
 4 |       def install!
 5 |         update_projects do |project|
 6 |           UI.section("Rolling back cache for project #{project.display_name}".bold.green) do
 7 |             rollback_for_project(project)
 8 |           end
 9 |         end
10 |       end
11 | 
12 |       private
13 | 
14 |       def rollback_for_project(project)
15 |         hash = lockfile.hash_for_project(project)
16 |         pkgs, deps_by_targets = hash["packages"], hash["dependencies"]
17 | 
18 |         # Add packages back to the project
19 |         pkgs.reject { |h| project.has_pkg?(h) }.each do |h|
20 |           project.add_pkg(h)
21 |         end
22 | 
23 |         # Add products back to `Link Binary with Libraries` of targets
24 |         deps_by_targets.each do |name, deps|
25 |           target = project.get_target(name)
26 |           deps.reject { |d| target.has_pkg_product_dependency?(d) }.each do |d|
27 |             target.add_pkg_product_dependency(d)
28 |           end
29 |         end
30 | 
31 |         # Remove .binary product from the project
32 |         project.targets.each(&:remove_xccache_product_dependencies)
33 |         project.xccache_pkg&.remove_from_project
34 |       end
35 |     end
36 |   end
37 | end
38 | 
--------------------------------------------------------------------------------
/lib/xccache/installer/use.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm"
 2 | 
 3 | module XCCache
 4 |   class Installer
 5 |     class Use < Installer
 6 |       def install!
 7 |         update_projects do |project|
 8 |           perform_install do
 9 |             UI.section("Using cache for project #{project.display_name}".bold.green) do
10 |               replace_binaries_for_project(project)
11 |             end
12 |           end
13 |         end
14 |       end
15 | 
16 |       private
17 | 
18 |       def replace_binaries_for_project(project)
19 |         project.add_xccache_pkg unless project.has_xccache_pkg?
20 |         project.targets.each do |target|
21 |           target.add_xccache_product_dependency unless target.has_xccache_product_dependency?
22 |           target.remove_pkg_product_dependencies { |d| d.pkg.nil? || !d.pkg.xccache_pkg? }
23 |         end
24 |         project.remove_pkgs(&:non_xccache_pkg?) unless config.keep_pkgs_in_project?
25 |       end
26 |     end
27 |   end
28 | end
29 | 
--------------------------------------------------------------------------------
/lib/xccache/main.rb:
--------------------------------------------------------------------------------
1 | require "pry" if ENV["XCCACHE_IMPORT_PRY"] == "true"
2 | require "pathname"
3 | Dir["#{__dir__}/*.rb"].sort.each { |f| require f unless f == __FILE__ }
4 | 
--------------------------------------------------------------------------------
/lib/xccache/spm.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/build.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module SPM
 3 |     class Buildable
 4 |       attr_reader :name, :module_name, :pkg_dir, :pkg_desc, :sdk, :sdks, :config, :path, :tmpdir, :library_evolution,
 5 |                   :live_log
 6 |       alias library_evolution? library_evolution
 7 | 
 8 |       def initialize(options = {})
 9 |         @name = options[:name]
10 |         @module_name = @name.c99extidentifier
11 |         @pkg_dir = Pathname(options[:pkg_dir] || ".").expand_path
12 |         @pkg_desc = options[:pkg_desc]
13 |         @ctx_desc = options[:ctx_desc] # Context desc, could be an umbrella or a standalone pkg
14 |         @sdks = options[:sdks] || []
15 |         @sdk = options[:sdk] || @sdks&.first
16 |         @config = options[:config] || "debug"
17 |         @path = options[:path]
18 |         @tmpdir = options[:tmpdir]
19 |         @library_evolution = options[:library_evolution]
20 |         @sdks.each { |sdk| sdk.version = @ctx_desc.platforms[sdk.platform] } if @ctx_desc
21 |         @live_log = options[:live_log]
22 |       end
23 | 
24 |       def build(options = {})
25 |         raise NotImplementedError
26 |       end
27 | 
28 |       def swift_build(target: nil)
29 |         cmd = ["swift", "build"] + swift_build_args
30 |         cmd << "--package-path" << pkg_dir
31 |         cmd << "--target" << (target || name)
32 |         cmd << "--sdk" << sdk.sdk_path
33 |         sdk.swiftc_args.each { |arg| cmd << "-Xswiftc" << arg }
34 |         if library_evolution?
35 |           # Workaround for swiftinterface emission
36 |           # https://github.com/swiftlang/swift/issues/64669#issuecomment-1535335601
37 |           cmd << "-Xswiftc" << "-enable-library-evolution"
38 |           cmd << "-Xswiftc" << "-alias-module-names-in-module-interface"
39 |           cmd << "-Xswiftc" << "-emit-module-interface"
40 |           cmd << "-Xswiftc" << "-no-verify-emitted-module-interface"
41 |         end
42 |         sh(cmd)
43 |       end
44 | 
45 |       def sh(cmd)
46 |         Sh.run(cmd, live_log: live_log)
47 |       end
48 | 
49 |       def swift_build_args
50 |         [
51 |           "--configuration", config,
52 |           "--triple", sdk.triple(with_version: true),
53 |         ]
54 |       end
55 | 
56 |       def pkg_target
57 |         @pkg_target ||= pkg_desc.get_target(name)
58 |       end
59 |     end
60 |   end
61 | end
62 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/base.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Package
 6 |       class BaseObject < JSONRepresentable
 7 |         include Config::Mixin
 8 | 
 9 |         ATTRS = %i[root retrieve_pkg_desc].freeze
10 |         attr_accessor(*ATTRS)
11 | 
12 |         def name
13 |           raw["name"]
14 |         end
15 | 
16 |         def full_name
17 |           is_a?(Description) ? name : "#{pkg_slug}/#{name}"
18 |         end
19 | 
20 |         def inspect
21 |           to_s
22 |         end
23 | 
24 |         def display_name
25 |           name
26 |         end
27 | 
28 |         def to_s
29 |           "<#{self.class} name=#{display_name}>"
30 |         end
31 | 
32 |         def cast_to(cls)
33 |           o = cls.new(path, raw: raw)
34 |           ATTRS.each { |sym| o.send("#{sym}=", send(sym.to_s)) }
35 |           o
36 |         end
37 | 
38 |         def pkg_name
39 |           @pkg_name ||= root.name
40 |         end
41 | 
42 |         def pkg_slug
43 |           @pkg_slug ||= src_dir.basename.to_s
44 |         end
45 | 
46 |         def fetch(key, dtype)
47 |           raw[key].map do |h|
48 |             o = dtype.new(nil, raw: h)
49 |             o.root = root
50 |             o.retrieve_pkg_desc = retrieve_pkg_desc
51 |             o
52 |           end
53 |         end
54 | 
55 |         def pkg_desc_of(name)
56 |           retrieve_pkg_desc.call(name)
57 |         end
58 | 
59 |         def src_dir
60 |           @src_dir ||= Pathname(root.raw["path"]).parent
61 |         end
62 |       end
63 |     end
64 |   end
65 | end
66 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/dep.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm/desc/base"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Package
 6 |       class Dependency < BaseObject
 7 |         def display_name
 8 |           slug
 9 |         end
10 | 
11 |         def local?
12 |           raw.key?("fileSystem")
13 |         end
14 | 
15 |         def hash
16 |           @hash ||= local? ? raw["fileSystem"].first : raw["sourceControl"].first
17 |         end
18 | 
19 |         def slug
20 |           @slug ||=
21 |             if hash.key?("path")
22 |               File.basename(hash["path"])
23 |             elsif (location = hash["location"]) && location.key?("remote")
24 |               File.basename(location["remote"].flat_map(&:values)[0], ".*")
25 |             else
26 |               hash["identity"]
27 |             end
28 |         end
29 | 
30 |         def path
31 |           @path ||= Pathname(hash["path"]).expand_path if local?
32 |         end
33 | 
34 |         def pkg_desc
35 |           @pkg_desc ||= pkg_desc_of(slug)
36 |         end
37 |       end
38 |     end
39 |   end
40 | end
41 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/desc.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/spm/desc/base"
  2 | 
  3 | module XCCache
  4 |   module SPM
  5 |     class Package
  6 |       class Description < BaseObject
  7 |         include Cacheable
  8 | 
  9 |         def self.descs_in_metadata_dir(dir)
 10 |           descs = dir.glob("*.json").map { |p| Description.new(p) }
 11 |           [descs, combine_descs(descs)]
 12 |         end
 13 | 
 14 |         def root
 15 |           self
 16 |         end
 17 | 
 18 |         def metadata
 19 |           raw["_metadata"] ||= {}
 20 |         end
 21 | 
 22 |         def platforms
 23 |           @platforms ||= raw.fetch("platforms", []).to_h { |h| [h["platformName"].to_sym, h["version"]] }
 24 |         end
 25 | 
 26 |         def dependencies
 27 |           @dependencies ||= fetch("dependencies", Dependency)
 28 |         end
 29 | 
 30 |         def uniform_dependencies
 31 |           dependencies.filter_map(&:pkg_desc)
 32 |         end
 33 | 
 34 |         def products
 35 |           @products ||= fetch("products", Product)
 36 |         end
 37 | 
 38 |         def targets
 39 |           @targets ||= fetch("targets", Target).map(&:downcast)
 40 |         end
 41 | 
 42 |         def binary_targets
 43 |           @binary_targets ||= targets.select(&:binary?)
 44 |         end
 45 | 
 46 |         def has_target?(name)
 47 |           targets.any? { |t| t.name == name }
 48 |         end
 49 | 
 50 |         def get_target(name)
 51 |           targets.find { |t| t.name == name }
 52 |         end
 53 | 
 54 |         def targets_of_products(name)
 55 |           matched_products = products.select { |p| p.name == name }
 56 |           matched_products
 57 |             .flat_map { |p| targets.select { |t| p.target_names.include?(t.name) } }
 58 |         end
 59 | 
 60 |         def local?
 61 |           # Workaround: If the pkg dir is under the build checkouts dir -> remote
 62 |           !src_dir.to_s.start_with?((config.spm_build_dir / "checkouts").to_s)
 63 |         end
 64 | 
 65 |         def traverse
 66 |           nodes, edges, parents = [], [], {}
 67 |           to_visit = targets.dup
 68 |           visited = Set.new
 69 |           until to_visit.empty?
 70 |             cur = to_visit.pop
 71 |             next if visited.include?(cur)
 72 | 
 73 |             visited << cur
 74 |             nodes << cur
 75 |             yield cur if block_given?
 76 | 
 77 |             # For macro impl, we don't need their dependencies, just the tool binary
 78 |             # So, no need to care about swift-syntax dependencies
 79 |             next if cur.macro?
 80 |             cur.direct_dependency_targets.each do |t|
 81 |               to_visit << t
 82 |               edges << [cur, t]
 83 |               parents[t] ||= []
 84 |               parents[t] << cur
 85 |             end
 86 |           end
 87 |           [nodes, edges, parents]
 88 |         end
 89 | 
 90 |         def git
 91 |           @git ||= Git.new(src_dir) if Dir.git?(src_dir)
 92 |         end
 93 | 
 94 |         def self.combine_descs(descs)
 95 |           descs_by_name = descs.flat_map { |d| [[d.name, d], [d.pkg_slug, d]] }.to_h
 96 |           descs.each { |d| d.retrieve_pkg_desc = proc { |name| descs_by_name[name] } }
 97 |           descs_by_name
 98 |         end
 99 |       end
100 |     end
101 |   end
102 | end
103 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/product.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm/desc/base"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Package
 6 |       class Product < BaseObject
 7 |         include Cacheable
 8 |         cacheable :recursive_targets
 9 | 
10 |         def target_names
11 |           raw["targets"]
12 |         end
13 | 
14 |         def flatten_as_targets
15 |           targets
16 |         end
17 | 
18 |         def targets
19 |           @targets ||= root.targets.select { |t| target_names.include?(t.name) }
20 |         end
21 | 
22 |         def library?
23 |           type == :library
24 |         end
25 | 
26 |         def type
27 |           @type ||= raw["type"].keys.first.to_sym
28 |         end
29 | 
30 |         def recursive_targets(platform: nil)
31 |           targets + targets.flat_map { |t| t.recursive_targets(platform: platform) }.uniq
32 |         end
33 |       end
34 |     end
35 |   end
36 | end
37 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/target.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/spm/desc/base"
  2 | 
  3 | module XCCache
  4 |   module SPM
  5 |     class Package
  6 |       class Target < BaseObject
  7 |         include Cacheable
  8 |         cacheable :recursive_targets, :direct_dependency_targets, :direct_dependencies
  9 | 
 10 |         Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 11 | 
 12 |         def xccache?
 13 |           name.end_with?(".xccache")
 14 |         end
 15 | 
 16 |         def xccache_id
 17 |           macro? ? "#{full_name}.macro" : full_name
 18 |         end
 19 | 
 20 |         def type
 21 |           @type ||= raw["type"].to_sym
 22 |         end
 23 | 
 24 |         def downcast
 25 |           cls = {
 26 |             :binary => BinaryTarget,
 27 |             :macro => MacroTarget,
 28 |           }[type]
 29 |           cls.nil? ? self : cast_to(cls)
 30 |         end
 31 | 
 32 |         def module_name
 33 |           name.c99extidentifier
 34 |         end
 35 | 
 36 |         def resource_bundle_name
 37 |           "#{pkg_name}_#{name}.bundle"
 38 |         end
 39 | 
 40 |         def flatten_as_targets
 41 |           [self]
 42 |         end
 43 | 
 44 |         def sources_path
 45 |           @sources_path ||= begin
 46 |             path = raw["path"] || "Sources/#{name}"
 47 |             root.src_dir / path
 48 |           end
 49 |         end
 50 | 
 51 |         def use_clang?
 52 |           !header_paths.empty?
 53 |         end
 54 | 
 55 |         def header_paths(options = {})
 56 |           paths = []
 57 |           paths += public_header_paths if options.fetch(:public, true)
 58 |           paths += header_search_paths if options.fetch(:search, false)
 59 |           paths
 60 |             .flat_map { |p| p.glob("**/*.h*") }
 61 |             .map(&:realpath)
 62 |             .uniq
 63 |         end
 64 | 
 65 |         def settings
 66 |           raw["settings"]
 67 |         end
 68 | 
 69 |         def header_search_paths
 70 |           @header_search_paths ||=
 71 |             settings
 72 |             .filter_map { |h| h.fetch("kind", {})["headerSearchPath"] }
 73 |             .flat_map(&:values)
 74 |             .map { |p| sources_path / p }
 75 |         end
 76 | 
 77 |         def public_header_paths
 78 |           @public_header_paths ||= begin
 79 |             res = []
 80 |             implicit_path = sources_path / "include"
 81 |             res << implicit_path unless implicit_path.glob("**/*.h*").empty?
 82 |             res << (sources_path / raw["publicHeadersPath"]) if raw.key?("publicHeadersPath")
 83 |             res
 84 |           end
 85 |         end
 86 | 
 87 |         def resource_paths
 88 |           @resource_paths ||= begin
 89 |             res = raw.fetch("resources", []).map { |h| sources_path / h["path"] }
 90 |             # Refer to the following link for the implicit resources
 91 |             # https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package#Add-resource-files
 92 |             implicit = sources_path.glob("*.{xcassets,xib,storyboard,xcdatamodeld,lproj}")
 93 |             res + implicit
 94 |           end
 95 |         end
 96 | 
 97 |         def recursive_targets(platform: nil)
 98 |           children = direct_dependency_targets(platform: platform)
 99 |           children += children.flat_map { |t| t.macro? ? [t] : t.recursive_targets(platform: platform) }
100 |           children.uniq
101 |         end
102 | 
103 |         def direct_dependencies(platform: nil)
104 |           raw["dependencies"].flat_map do |hash|
105 |             dep_types = ["byName", "target", "product"]
106 |             if (dep_type = dep_types.intersection(hash.keys).first).nil?
107 |               raise GeneralError, "Unexpected dependency type. Must be one of #{dep_types}. Hash: #{hash}"
108 |             end
109 |             next [] unless match_platform?(hash[dep_type][-1], platform)
110 |             pkg_name = hash[dep_type][1] if dep_type == "product"
111 |             find_deps(hash[dep_type][0], pkg_name, dep_type)
112 |           end
113 |         end
114 | 
115 |         def direct_dependency_targets(platform: nil)
116 |           direct_dependencies(platform: platform).flat_map(&:flatten_as_targets).uniq
117 |         end
118 | 
119 |         def match_platform?(_condition, _platform)
120 |           true # FIXME: Handle this
121 |         end
122 | 
123 |         def macro?
124 |           type == :macro
125 |         end
126 | 
127 |         def binary?
128 |           type == :binary
129 |         end
130 | 
131 |         def binary_path
132 |           sources_path if binary?
133 |         end
134 | 
135 |         def local_binary_path
136 |           binary_path if binary? && root.local?
137 |         end
138 | 
139 |         def checksum
140 |           @checksum ||= root.git&.sha || sources_path.checksum
141 |         end
142 | 
143 |         private
144 | 
145 |         def find_deps(name, pkg_name, dep_type)
146 |           # If `dep_type` is `target` -> constrained within current pkg only
147 |           # If `dep_type` is `product` -> `pkg_name` must be present
148 |           # If `dep_type` is `byName` -> it's either from this pkg, or its children/dependencies
149 |           res = []
150 |           descs = pkg_name.nil? ? [root] + root.uniform_dependencies : [pkg_desc_of(pkg_name)]
151 |           descs.each do |desc|
152 |             by_target = -> { desc.targets.select { |t| t.name == name } }
153 |             by_product = -> { desc.products.select { |t| t.name == name } }
154 |             return by_target.call if dep_type == "target"
155 |             return by_product.call if dep_type == "product"
156 |             return res unless (res = by_target.call).empty?
157 |             return res unless (res = by_product.call).empty?
158 |           end
159 |           []
160 |         end
161 |       end
162 |     end
163 |   end
164 | end
165 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/target/binary.rb:
--------------------------------------------------------------------------------
1 | module XCCache
2 |   module SPM
3 |     class Package
4 |       class BinaryTarget < Target
5 |       end
6 |     end
7 |   end
8 | end
9 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/desc/target/macro.rb:
--------------------------------------------------------------------------------
1 | module XCCache
2 |   module SPM
3 |     class Package
4 |       class MacroTarget < Target
5 |       end
6 |     end
7 |   end
8 | end
9 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/macro.rb:
--------------------------------------------------------------------------------
 1 | require_relative "build"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class Macro < Buildable
 6 |       def initialize(options = {})
 7 |         super
 8 |         @library_evolution = false # swift-syntax is not compatible with library evolution
 9 |       end
10 | 
11 |       def build(_options = {})
12 |         # NOTE: Building macro binary is tricky...
13 |         # --------------------------------------------------------------------------------
14 |         # Consider this manifest config: .target(Macro) -> .macro(MacroImpl)
15 |         #   where `.target(Macro)` contains the interfaces
16 |         #   and `.target(MacroImpl)` contains the implementation
17 |         # --------------------------------------------------------------------------------
18 |         # Building `.macro(MacroImpl)` does not produce the tool binary (MacroImpl-tool)... Only `.o` files.
19 |         # Yet, linking those files are exhaustive due to many dependencies in swift-syntax
20 |         # Luckily, building `.target(Macro)` does produce the tool binary.
21 |         # -> WORKAROUND: Find the associated regular target and build it, then collect the tool binary
22 |         # ---------------------------------------------------------------------------------
23 |         associated_target = pkg_desc.targets.find { |t| t.direct_dependency_targets.include?(pkg_target) }
24 |         live_log.info(
25 |           "#{name.yellow.dark} is a macro target. " \
26 |           "Will build the associated target #{associated_target.name.dark} to get the tool binary."
27 |         )
28 |         swift_build(target: associated_target.name)
29 |         binary_path = products_dir / "#{module_name}-tool"
30 |         raise GeneralError, "Tool binary not exist at: #{binary_path}" unless binary_path.exist?
31 |         binary_path.copy(to: path)
32 |         FileUtils.chmod("+x", path)
33 |         live_log.info("-> Macro binary: #{path}")
34 |       end
35 | 
36 |       def products_dir
37 |         @products_dir ||= pkg_dir / ".build" / "arm64-apple-macosx" / config
38 |       end
39 |     end
40 |   end
41 | end
42 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/mixin.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module PkgMixin
 3 |     include Config::Mixin
 4 | 
 5 |     def umbrella_pkg
 6 |       proxy_pkg.umbrella
 7 |     end
 8 | 
 9 |     def proxy_pkg
10 |       @proxy_pkg ||= SPM::Package::Proxy.new(root_dir: config.spm_proxy_sandbox)
11 |     end
12 |   end
13 | end
14 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg/base.rb:
--------------------------------------------------------------------------------
  1 | require "json"
  2 | require "xccache/spm/xcframework/slice"
  3 | require "xccache/spm/xcframework/xcframework"
  4 | require "xccache/spm/xcframework/metadata"
  5 | require "xccache/spm/pkg/proxy"
  6 | require "xccache/swift/sdk"
  7 | 
  8 | module XCCache
  9 |   module SPM
 10 |     class Package
 11 |       include Config::Mixin
 12 |       include Proxy::Mixin
 13 | 
 14 |       include Cacheable
 15 |       cacheable :pkg_desc_of_target
 16 | 
 17 |       attr_reader :root_dir
 18 | 
 19 |       def initialize(options = {})
 20 |         @root_dir = Pathname(options[:root_dir] || ".").expand_path
 21 |       end
 22 | 
 23 |       def build(options = {})
 24 |         validate!
 25 |         targets = (options.delete(:targets) || []).map { |t| t.split("/")[-1] }
 26 |         raise GeneralError, "No targets were specified" if targets.empty?
 27 | 
 28 |         Dir.create_tmpdir do |tmpdir|
 29 |           targets.each_with_index do |t, i|
 30 |             target_tmpdir = Dir.prepare(tmpdir / t)
 31 |             log_dir = Dir.prepare(options[:log_dir] || target_tmpdir)
 32 |             live_log = LiveLog.new(tee: log_dir / "build_#{t}.log")
 33 |             live_log.capture("[#{i + 1}/#{targets.count}] Building target: #{t}") do
 34 |               build_target(**options, target: t, live_log: live_log, tmpdir: target_tmpdir)
 35 |             end
 36 |           rescue StandardError => e
 37 |             UI.error("Error: #{e}\n" + "For details, check out: #{live_log.tee}".yellow.bold)
 38 |             raise e unless Config.instance.ignore_build_errors?
 39 |           end
 40 |         end
 41 |       end
 42 | 
 43 |       def build_target(target: nil, sdks: nil, config: nil, out_dir: nil, **options)
 44 |         target_pkg_desc = pkg_desc_of_target(
 45 |           target,
 46 |           ensure_exist: true,
 47 |         )
 48 |         if target_pkg_desc.binary_targets.any? { |t| t.name == target }
 49 |           return UI.warn("Target #{target} is a binary target -> no need to build")
 50 |         end
 51 | 
 52 |         target = target_pkg_desc.get_target(target)
 53 | 
 54 |         out_dir = Pathname(out_dir || ".")
 55 |         out_dir /= target.name if options[:checksum]
 56 |         ext = target.macro? ? ".macro" : ".xcframework"
 57 |         basename = options[:checksum] ? "#{target.name}-#{target.checksum}" : target.name
 58 |         binary_path = out_dir / "#{basename}#{ext}"
 59 | 
 60 |         cls = target.macro? ? Macro : XCFramework
 61 |         cls.new(
 62 |           name: target.name,
 63 |           pkg_dir: root_dir,
 64 |           config: config,
 65 |           sdks: sdks,
 66 |           path: binary_path,
 67 |           tmpdir: options[:tmpdir],
 68 |           pkg_desc: target_pkg_desc,
 69 |           ctx_desc: pkg_desc || target_pkg_desc,
 70 |           library_evolution: options[:library_evolution],
 71 |           live_log: options[:live_log],
 72 |         ).build(**options)
 73 |         return if (symlinks_dir = options[:symlinks_dir]).nil?
 74 |         binary_path.symlink_to(symlinks_dir / target.name / "#{target.name}#{ext}")
 75 |       end
 76 | 
 77 |       def resolve
 78 |         return if @resolved
 79 |         xccache_proxy.run("resolve --pkg #{root_dir} --metadata #{metadata_dir}")
 80 |         @resolved = true
 81 |       end
 82 | 
 83 |       def pkg_desc
 84 |         descs_by_name[root_dir.basename.to_s]
 85 |       end
 86 | 
 87 |       def pkg_desc_of_target(name, **options)
 88 |         resolve
 89 |         desc = descs.find { |d| d.has_target?(name) }
 90 |         raise GeneralError, "Cannot find package with the given target #{name}" if options[:ensure_exist] && desc.nil?
 91 |         desc
 92 |       end
 93 | 
 94 |       def get_target(name)
 95 |         pkg_desc_of_target(name)&.get_target(name)
 96 |       end
 97 | 
 98 |       private
 99 | 
100 |       def validate!
101 |         return unless root_dir.glob("Package*.swift").empty?
102 |         raise GeneralError, "No Package.swift in #{root_dir}. Are you sure you're running on a package dir?"
103 |       end
104 | 
105 |       def metadata_dir
106 |         config.in_installation? ? config.spm_metadata_dir : root_dir / ".build/metadata"
107 |       end
108 | 
109 |       def descs
110 |         @descs ||= load_descs[0]
111 |       end
112 | 
113 |       def descs_by_name
114 |         @descs_by_name ||= load_descs[1]
115 |       end
116 | 
117 |       def load_descs
118 |         @descs, @descs_by_name = Description.descs_in_metadata_dir(metadata_dir)
119 |       end
120 |     end
121 |   end
122 | end
123 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg/proxy.rb:
--------------------------------------------------------------------------------
 1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
 2 | require_relative "proxy_executable"
 3 | 
 4 | module XCCache
 5 |   module SPM
 6 |     class Package
 7 |       class Proxy < Package
 8 |         module Mixin
 9 |           def xccache_proxy
10 |             @xccache_proxy ||= Executable.new
11 |           end
12 |         end
13 | 
14 |         include Mixin
15 | 
16 |         def umbrella
17 |           @umbrella ||= Package.new(root_dir: config.spm_umbrella_sandbox)
18 |         end
19 | 
20 |         def prepare(options = {})
21 |           xccache_proxy.run("gen-umbrella")
22 |           umbrella.resolve
23 |           invalidate_cache(sdks: options[:sdks])
24 |           gen_proxy
25 |         end
26 | 
27 |         def gen_proxy
28 |           xccache_proxy.run("gen-proxy")
29 |           config.cachemap.update_from_graph(graph.reload)
30 |         end
31 | 
32 |         def invalidate_cache(sdks: [])
33 |           UI.message("Invalidating cache (sdks: #{sdks.map(&:to_s).join(', ')})")
34 | 
35 |           config.spm_cache_dir.glob("*/*.{xcframework,macro}").each do |p|
36 |             cmps = p.basename(".*").to_s.split("-")
37 |             name, checksum = cmps[...-1].join("-"), cmps[-1]
38 |             p_without_checksum = config.spm_binaries_dir / name / "#{name}#{p.extname}"
39 |             accept_cache = proc { p.symlink_to(p_without_checksum) }
40 |             reject_cache = proc { p_without_checksum.rmtree if p_without_checksum.exist? }
41 |             next reject_cache.call if (target = umbrella.get_target(name)).nil?
42 |             next reject_cache.call if target.checksum != checksum
43 |             # For macro, we just need the tool binary to exist
44 |             next accept_cache.call if target.macro?
45 | 
46 |             # For regular targets, the xcframework must satisfy the sdk constraints (ie. containing all the slices)
47 |             metadata = XCFramework::Metadata.new(p / "Info.plist")
48 |             expected_triples = sdks.map { |sdk| sdk.triple(with_vendor: false) }
49 |             missing_triples = expected_triples - metadata.triples
50 |             missing_triples.empty? ? accept_cache.call : reject_cache.call
51 |           end
52 |         end
53 | 
54 |         def graph
55 |           @graph ||= JSONRepresentable.new(root_dir / "graph.json")
56 |         end
57 |       end
58 |     end
59 |   end
60 | end
61 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/pkg/proxy_executable.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module SPM
 3 |     class Package
 4 |       class Proxy < Package
 5 |         class Executable
 6 |           REPO_URL = "https://github.com/trinhngocthuyen/xccache-proxy".freeze
 7 |           VERSION_OR_SHA = "1.0.0".freeze
 8 | 
 9 |           def run(cmd)
10 |             env = { "FORCE_OUTPUT" => "console", "FORCE_COLOR" => "1" } if Config.instance.ansi?
11 |             cmd = cmd.is_a?(Array) ? [bin_path.to_s] + cmd : [bin_path.to_s, cmd]
12 |             cmd << "--verbose" if Config.instance.verbose?
13 |             Sh.run(cmd, env: env)
14 |           end
15 | 
16 |           def bin_path
17 |             @bin_path ||= lookup
18 |           end
19 | 
20 |           private
21 | 
22 |           def lookup
23 |             [
24 |               local_bin_path,
25 |               default_bin_path,
26 |             ].find(&:exist?) || download_or_build_from_source
27 |           end
28 | 
29 |           def default_use_downloaded?
30 |             VERSION_OR_SHA.include?(".")
31 |           end
32 | 
33 |           def download_or_build_from_source
34 |             default_use_downloaded? ? download : build_from_source
35 |           end
36 | 
37 |           def build_from_source
38 |             UI.section("Building xccache-proxy binary from source...".magenta) do
39 |               dir = Dir.prepare("~/.xccache/xccache-proxy", expand: true)
40 |               git = Git.new(dir)
41 |               git.init unless git.init?
42 |               git.remote("add", "origin", REPO_URL) unless git.remote(capture: true)[0].strip == "origin"
43 |               git.fetch("origin", VERSION_OR_SHA)
44 |               git.checkout("-f", "FETCH_HEAD", capture: true)
45 | 
46 |               Dir.chdir(dir) { Sh.run("make build CONFIGURATION=release") }
47 |               (dir / ".build" / "release" / "xccache-proxy").copy(to: default_bin_path)
48 |             end
49 |           end
50 | 
51 |           def download
52 |             UI.section("Downloading xccache-proxy binary from remote...".magenta) do
53 |               Dir.create_tmpdir do |dir|
54 |                 url = "#{REPO_URL}/releases/download/#{VERSION_OR_SHA}/xccache-proxy.zip"
55 |                 default_bin_path.parent.mkpath
56 |                 tmp_path = dir / File.basename(url)
57 |                 Sh.run("curl -fSL -o #{tmp_path} #{url} && unzip -d #{default_bin_path.parent} #{tmp_path}")
58 |                 FileUtils.chmod("+x", default_bin_path)
59 |               end
60 |             end
61 |             default_bin_path
62 |           end
63 | 
64 |           def default_bin_path
65 |             @default_bin_path ||= begin
66 |               dir = LIBEXEC / (default_use_downloaded? ? ".download" : ".build")
67 |               dir / "xccache-proxy-#{VERSION_OR_SHA}" / "xccache-proxy"
68 |             end
69 |           end
70 | 
71 |           def local_bin_path
72 |             @local_bin_path ||= LIBEXEC / ".local" / "xccache-proxy"
73 |           end
74 |         end
75 |       end
76 |     end
77 |   end
78 | end
79 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework.rb:
--------------------------------------------------------------------------------
1 | require_relative "build"
2 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
3 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework/metadata.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/syntax/plist"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class XCFramework
 6 |       class Metadata < PlistRepresentable
 7 |         class Library < Hash
 8 |           def id
 9 |             self["LibraryIdentifier"]
10 |           end
11 | 
12 |           def platform
13 |             self["SupportedPlatform"]
14 |           end
15 | 
16 |           def archs
17 |             self["SupportedArchitectures"]
18 |           end
19 | 
20 |           def simulator?
21 |             self["SupportedPlatformVariant"] == "simulator"
22 |           end
23 | 
24 |           def triples
25 |             @triples ||= archs.map do |arch|
26 |               simulator? ? "#{arch}-#{platform}-simulator" : "#{arch}-#{platform}"
27 |             end
28 |           end
29 |         end
30 | 
31 |         def available_libraries
32 |           @available_libraries ||= raw.fetch("AvailableLibraries", []).map { |h| Library.new.merge(h) }
33 |         end
34 | 
35 |         def triples
36 |           @triples ||= available_libraries.flat_map(&:triples)
37 |         end
38 |       end
39 |     end
40 |   end
41 | end
42 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework/slice.rb:
--------------------------------------------------------------------------------
  1 | require "xccache/utils/template"
  2 | 
  3 | module XCCache
  4 |   module SPM
  5 |     class FrameworkSlice < Buildable
  6 |       def build(_options = {})
  7 |         live_log.puts("Building #{name}.framework (#{config}, #{sdk})".cyan, sticky: true)
  8 |         swift_build
  9 |         create_framework
 10 |       end
 11 | 
 12 |       private
 13 | 
 14 |       def override_resource_bundle_accessor
 15 |         # By default, Swift generates resource_bundle_accessor.swift for targets having resources
 16 |         # (Check .build/.build/DerivedSources/resource_bundle_accessor.swift)
 17 |         # This enables accessing the resource bundle via `Bundle.module`.
 18 |         # However, `Bundle.module` expects the resource bundle to be under the app bundle,
 19 |         # which is not the case for binary targets. Instead the bundle is under `Frameworks/.framework`
 20 |         # WORKAROUND:
 21 |         # - Overriding resource_bundle_accessor.swift to add `Frameworks/.framework` to the search list
 22 |         # - Compiling this file into an `.o` file before using `libtool` to create the framework binary
 23 |         live_log.info("Override resource_bundle_accessor")
 24 |         template_name = use_clang? ? "resource_bundle_accessor.m" : "resource_bundle_accessor.swift"
 25 |         source_path = tmpdir / File.basename(template_name)
 26 |         obj_path = products_dir / "#{module_name}.build" / "#{source_path.basename}.o"
 27 |         Template.new(template_name).render(
 28 |           { :pkg => pkg_target.pkg_name, :target => name, :module_name => module_name },
 29 |           save_to: source_path
 30 |         )
 31 | 
 32 |         if use_clang?
 33 |           cmd = ["xcrun", "clang"]
 34 |           cmd << "-x" << "objective-c"
 35 |           cmd << "-target" << sdk.triple(with_version: true) << "-isysroot" << sdk.sdk_path
 36 |           cmd << "-o" << obj_path.to_s
 37 |           cmd << "-c" << source_path
 38 |         else
 39 |           cmd = ["xcrun", "swiftc"]
 40 |           cmd << "-emit-library" << "-emit-object"
 41 |           cmd << "-module-name" << module_name
 42 |           cmd << "-target" << sdk.triple(with_version: true) << "-sdk" << sdk.sdk_path
 43 |           cmd << "-o" << obj_path.to_s
 44 |           cmd << source_path
 45 |         end
 46 |         sh(cmd)
 47 |       end
 48 | 
 49 |       def create_framework
 50 |         override_resource_bundle_accessor if resource_bundle_product_path.exist?
 51 |         create_info_plist
 52 |         create_framework_binary
 53 |         create_headers
 54 |         create_modules
 55 |         copy_resource_bundles if resource_bundle_product_path.exist?
 56 |       end
 57 | 
 58 |       def create_framework_binary
 59 |         # Write .o file list into a file
 60 |         obj_paths = products_dir.glob("#{module_name}.build/**/*.o")
 61 |         raise GeneralError, "Detected no object files for #{name}" if obj_paths.empty?
 62 | 
 63 |         objlist_path = tmpdir / "objects.txt"
 64 |         objlist_path.write(obj_paths.map(&:to_s).join("\n"))
 65 | 
 66 |         cmd = ["libtool", "-static"]
 67 |         cmd << "-o" << "#{path}/#{module_name}"
 68 |         cmd << "-filelist" << objlist_path.to_s
 69 |         sh(cmd)
 70 |         FileUtils.chmod("+x", path / module_name)
 71 |       end
 72 | 
 73 |       def create_info_plist
 74 |         Template.new("framework.info.plist").render(
 75 |           { :module_name => module_name },
 76 |           save_to: path / "Info.plist",
 77 |         )
 78 |       end
 79 | 
 80 |       def create_headers
 81 |         copy_headers
 82 |       end
 83 | 
 84 |       def create_modules
 85 |         copy_swiftmodules unless use_clang?
 86 | 
 87 |         live_log.info("Creating framework modulemap")
 88 |         Template.new("framework.modulemap").render(
 89 |           { :module_name => module_name, :target => name },
 90 |           save_to: modules_dir / "module.modulemap"
 91 |         )
 92 |       end
 93 | 
 94 |       def copy_headers
 95 |         live_log.info("Copying headers")
 96 |         swift_header_paths = products_dir.glob("#{module_name}.build/*-Swift.h")
 97 |         paths = swift_header_paths + pkg_target.header_paths
 98 |         paths.each { |p| process_header(p) }
 99 | 
100 |         umbrella_header_content = paths.map { |p| "#include <#{module_name}/#{p.basename}>" }.join("\n")
101 |         (headers_dir / "#{name}-umbrella.h").write(umbrella_header_content)
102 |       end
103 | 
104 |       def process_header(path)
105 |         handle_angle_bracket_import = proc do |statement, header|
106 |           next statement if header.include?("/")
107 | 
108 |           # NOTE: If importing a header with flat angle-bracket style (ex. `#import `)
109 |           # The header `foo.h` may belong to a dependency's headers.
110 |           # When packaging into xcframework, `#import ` no longer works because `foo.h`
111 |           # coz it's not visible within the framework's headers
112 |           # -> We need to explicitly specify the module it belongs to, ex. `#import `
113 |           targets = [pkg_target] + pkg_target.recursive_targets
114 |           target = targets.find { |t| t.header_paths.any? { |p| p.basename.to_s == header } }
115 |           next statement if target.nil?
116 | 
117 |           corrected_statement = statement.sub("<#{header}>", "<#{target.module_name}/#{header}>")
118 |           <<~CONTENT
119 |             // -------------------------------------------------------------------------------------------------
120 |             // NOTE: This import was corrected by xccache, from flat angle-bracket to nested angle-bracket style
121 |             // Original: `#{statement}`
122 |             #{corrected_statement}
123 |             // -------------------------------------------------------------------------------------------------
124 |           CONTENT
125 |         end
126 | 
127 |         content = path.read.gsub(/^ *#import <(.+)>/) { |m| handle_angle_bracket_import.call(m, $1) }
128 |         (headers_dir / path.basename).write(content)
129 |       end
130 | 
131 |       def copy_swiftmodules
132 |         live_log.info("Copying swiftmodules")
133 |         swiftmodule_dir = Dir.prepare("#{modules_dir}/#{module_name}.swiftmodule")
134 |         swiftinterfaces = products_dir.glob("#{module_name}.build/#{module_name}.swiftinterface")
135 |         to_copy = products_dir.glob("Modules/#{module_name}.*") + swiftinterfaces
136 |         to_copy.each do |p|
137 |           p.copy(to: swiftmodule_dir / p.basename.sub(module_name, sdk.triple))
138 |         end
139 |       end
140 | 
141 |       def copy_resource_bundles
142 |         resolve_resource_symlinks
143 |         live_log.info("Copying resource bundle to framework: #{resource_bundle_product_path.basename}")
144 |         resource_bundle_product_path.copy(to_dir: path)
145 |       end
146 | 
147 |       def resolve_resource_symlinks
148 |         # Well, Xcode seems to well handle symlinks in resources. In xcodebuild log, you would see something like:
149 |         #   CpResource: builtin-copy ... -resolve-src-symlinks
150 |         # But this is not the case if we build with `swift build`. Here, we have to manually handle it
151 |         resource_bundle_product_path.glob("**/*").select(&:symlink?).reject(&:exist?).each do |p|
152 |           UI.message("Resolve resource symlink: #{p}")
153 |           original = pkg_target.resource_paths.find { |rp| rp.symlink? && rp.readlink == p.readlink }
154 |           original&.realpath&.copy(to: p)
155 |         end
156 |       end
157 | 
158 |       def products_dir
159 |         @products_dir ||= pkg_dir / ".build" / sdk.triple / config
160 |       end
161 | 
162 |       def use_clang?
163 |         pkg_target.use_clang?
164 |       end
165 | 
166 |       def resource_bundle_product_path
167 |         @resource_bundle_product_path ||= products_dir / pkg_target.resource_bundle_name
168 |       end
169 | 
170 |       def headers_dir
171 |         @headers_dir ||= Dir.prepare(path / "Headers")
172 |       end
173 | 
174 |       def modules_dir
175 |         @modules_dir ||= Dir.prepare(path / "Modules")
176 |       end
177 |     end
178 |   end
179 | end
180 | 
--------------------------------------------------------------------------------
/lib/xccache/spm/xcframework/xcframework.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/spm/build"
 2 | 
 3 | module XCCache
 4 |   module SPM
 5 |     class XCFramework < Buildable
 6 |       attr_reader :slices
 7 | 
 8 |       def initialize(options = {})
 9 |         super
10 |         @slices ||= @sdks.map do |sdk|
11 |           FrameworkSlice.new(
12 |             **options,
13 |             sdks: [sdk],
14 |             path: Dir.prepare(tmpdir / sdk.triple / "#{module_name}.framework"),
15 |           )
16 |         end
17 |       end
18 | 
19 |       def build(merge_slices: false, **_options)
20 |         tmp_new_path = tmpdir / "new.xcframework"
21 |         tmp_existing_path = tmpdir / "existing.framework"
22 | 
23 |         slices.each(&:build)
24 |         create_xcframework(from: slices.map(&:path), to: tmp_new_path)
25 | 
26 |         path.copy(to: tmp_existing_path) if path.exist? && merge_slices
27 |         path.rmtree if path.exist?
28 | 
29 |         if merge_slices && tmp_existing_path.exist?
30 |           framework_paths =
31 |             [tmp_new_path, tmp_existing_path]
32 |             .flat_map { |p| p.glob("*/*.framework") }
33 |             .uniq { |p| p.parent.basename.to_s } # uniq by id (ex. ios-arm64), preferred new ones
34 |           create_xcframework(from: framework_paths, to: path)
35 |         else
36 |           path.parent.mkpath
37 |           tmp_new_path.copy(to: path)
38 |         end
39 |         live_log.info("-> XCFramework: #{path}")
40 |       end
41 | 
42 |       def create_xcframework(options = {})
43 |         live_log.info("Creating xcframework from slices")
44 |         cmd = ["xcodebuild", "-create-xcframework"]
45 |         cmd << "-allow-internal-distribution" unless library_evolution?
46 |         cmd << "-output" << options[:to]
47 |         options[:from].each { |p| cmd << "-framework" << p }
48 |         cmd << "> /dev/null" # Only care about errors
49 |         sh(cmd)
50 |       end
51 |     end
52 |   end
53 | end
54 | 
--------------------------------------------------------------------------------
/lib/xccache/storage.rb:
--------------------------------------------------------------------------------
1 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
2 | 
--------------------------------------------------------------------------------
/lib/xccache/storage/base.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   class Storage
 3 |     include Config::Mixin
 4 | 
 5 |     def initialize(options = {}); end
 6 | 
 7 |     def pull
 8 |       print_warnings
 9 |     end
10 | 
11 |     def push
12 |       print_warnings
13 |     end
14 | 
15 |     private
16 | 
17 |     def print_warnings
18 |       UI.warn <<~DESC
19 |         Do nothing as remote cache is not set up yet.
20 | 
21 |         To set it up, specify `remote` in `xccache.yml`.
22 |         See: https://github.com/trinhngocthuyen/xccache/blob/main/docs/configuration.md#remote
23 |       DESC
24 |     end
25 |   end
26 | end
27 | 
--------------------------------------------------------------------------------
/lib/xccache/storage/git.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | 
 3 | module XCCache
 4 |   class GitStorage < Storage
 5 |     attr_reader :branch
 6 | 
 7 |     def initialize(options = {})
 8 |       super
 9 |       if (@remote = options[:remote])
10 |         schemes = ["http://", "https://", "git@"]
11 |         @remote = File.expand_path(@remote) unless schemes.any? { |x| @remote.start_with?(x) }
12 |         ensure_remote
13 |       end
14 |       @branch = options[:branch]
15 |     end
16 | 
17 |     def pull
18 |       git.fetch("--depth 1 origin #{branch}")
19 |       git.switch("--detach FETCH_HEAD", capture: true)
20 |       git.clean("-dfx", capture: true)
21 |       # Re-create local branch so that it has the latest from remote
22 |       git.branch("-D #{branch} || true", capture: true)
23 |       git.checkout("-b #{branch}", capture: true)
24 |     end
25 | 
26 |     def push
27 |       return UI.info("No changes to push, cache repo is clean".magenta) if git.clean?
28 | 
29 |       git.add(".")
30 |       git.commit("-m \"Update cache at #{Time.new}\"")
31 |       git.push("-u origin #{branch}")
32 |     end
33 | 
34 |     private
35 | 
36 |     def git
37 |       @git ||= Git.new(config.spm_cache_dir)
38 |     end
39 | 
40 |     def ensure_remote
41 |       git.init unless git.init?
42 |       existing = git.remote("get-url origin || true", capture: true, log_cmd: false)[0].strip
43 |       return if @remote == existing
44 |       return git.remote("add origin #{@remote}") if existing.empty?
45 |       git.remote("set-url origin #{@remote}")
46 |     end
47 |   end
48 | end
49 | 
--------------------------------------------------------------------------------
/lib/xccache/storage/s3.rb:
--------------------------------------------------------------------------------
 1 | require_relative "base"
 2 | 
 3 | module XCCache
 4 |   class S3Storage < Storage
 5 |     def initialize(options = {})
 6 |       super
 7 |       @uri = options[:uri]
 8 |       @creds_path = Pathname(options[:creds_path] || "~/.xccache/s3.creds.json").expand_path
 9 |       creds = JSONRepresentable.new(@creds_path)
10 |       @access_key_id = creds["access_key_id"]
11 |       @secret_access_key = creds["secret_access_key"]
12 |     end
13 | 
14 |     def pull
15 |       s3_sync(src: @uri, dst: config.spm_cache_dir)
16 |     end
17 | 
18 |     def push
19 |       s3_sync(src: config.spm_cache_dir, dst: @uri)
20 |     end
21 | 
22 |     private
23 | 
24 |     def s3_sync(src: nil, dst: nil)
25 |       validate!
26 |       UI.info("Syncing cache from #{src.to_s.bold} to #{dst.to_s.bold}...")
27 |       env = {
28 |         "AWS_ACCESS_KEY_ID" => @access_key_id,
29 |         "AWS_SECRET_ACCESS_KEY" => @secret_access_key,
30 |       }
31 |       cmd = ["aws", "s3", "sync"]
32 |       cmd << "--exact-timestamps" << "--delete"
33 |       cmd << "--include" << "*.xcframework"
34 |       cmd << "--include" << "*.macro"
35 |       cmd << src << dst
36 |       Sh.run(cmd, env: env)
37 |     end
38 | 
39 |     def validate!
40 |       if File.which("awsss").nil?
41 |         raise GeneralError, "awscli is not installed. Please install it via brew: `brew install awscli`"
42 |       end
43 |       return unless @access_key_id.nil? || @secret_access_key.nil?
44 |       raise GeneralError, <<~DESC
45 |         Please ensure the credentials json at #{@creds_path}. Example:
46 |         {
47 |           "access_key_id": ,
48 |           "secret_access_key": 
49 |         }
50 |       DESC
51 |     end
52 |   end
53 | end
54 | 
--------------------------------------------------------------------------------
/lib/xccache/swift/sdk.rb:
--------------------------------------------------------------------------------
 1 | require "xccache/core/sh"
 2 | 
 3 | module XCCache
 4 |   module Swift
 5 |     class Sdk
 6 |       attr_reader :name, :arch, :vendor, :platform
 7 |       attr_accessor :version
 8 | 
 9 |       NAME_TO_PLATFORM = {
10 |         :iphonesimulator => :ios,
11 |         :iphoneos => :ios,
12 |         :macos => :macos,
13 |         :watchos => :watchos,
14 |         :watchsimulator => :watchos,
15 |         :appletvos => :tvos,
16 |         :appletvsimulator => :tvos,
17 |         :xros => :xros,
18 |         :xrsimulator => :xros,
19 |       }.freeze
20 | 
21 |       def initialize(name, version: nil)
22 |         @name = name.to_sym
23 |         @vendor = "apple"
24 |         @arch = "arm64"
25 |         @platform = NAME_TO_PLATFORM.fetch(@name, @name)
26 |         @version = version
27 |         return if NAME_TO_PLATFORM.key?(@name)
28 |         raise GeneralError, "Unknown sdk: #{@name}. Must be one of #{NAME_TO_PLATFORM.keys}"
29 |       end
30 | 
31 |       def to_s
32 |         name.to_s
33 |       end
34 | 
35 |       def triple(with_vendor: true, with_version: false)
36 |         cmps = [arch]
37 |         cmps << vendor if with_vendor
38 |         cmps << (with_version && version ? "#{platform}#{version}" : platform.to_s)
39 |         cmps << "simulator" if simulator?
40 |         cmps.join("-")
41 |       end
42 | 
43 |       def sdk_name
44 |         name == :macos ? :macosx : name
45 |       end
46 | 
47 |       def sdk_path
48 |         # rubocop:disable Layout/LineLength
49 |         # /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
50 |         # rubocop:enable Layout/LineLength
51 |         @sdk_path ||= Pathname(Sh.capture_output("xcrun --sdk #{sdk_name} --show-sdk-path")).realpath
52 |       end
53 | 
54 |       def sdk_platform_developer_path
55 |         @sdk_platform_developer_path ||= sdk_path.parent.parent # iPhoneSimulator.platform/Developer
56 |       end
57 | 
58 |       def swiftc_args
59 |         developer_library_frameworks_path = sdk_platform_developer_path / "Library" / "Frameworks"
60 |         developer_usr_lib_path = sdk_platform_developer_path / "usr" / "lib"
61 |         [
62 |           "-F#{developer_library_frameworks_path}",
63 |           "-I#{developer_usr_lib_path}",
64 |         ]
65 |       end
66 | 
67 |       def simulator?
68 |         name.to_s.end_with?("simulator")
69 |       end
70 |     end
71 |   end
72 | end
73 | 
--------------------------------------------------------------------------------
/lib/xccache/swift/swiftc.rb:
--------------------------------------------------------------------------------
 1 | module XCCache
 2 |   module Swift
 3 |     module Swiftc
 4 |       def self.version
 5 |         @version ||= begin
 6 |           m = /Apple Swift version ([\d\.]+)/.match(Sh.capture_output("xcrun swift -version"))
 7 |           m.nil? ? "6.0" : m[1]
 8 |         end
 9 |       end
10 | 
11 |       def self.version_without_patch
12 |         version.split(".")[...2].join(".")
13 |       end
14 |     end
15 |   end
16 | end
17 | 
--------------------------------------------------------------------------------
/lib/xccache/utils/template.rb:
--------------------------------------------------------------------------------
 1 | require "erb"
 2 | require "xccache/core/error"
 3 | 
 4 | module XCCache
 5 |   class Template
 6 |     attr_reader :name, :path
 7 | 
 8 |     def initialize(name)
 9 |       @name = name
10 |       @path = ROOT / "lib/xccache/assets/templates/#{name}.template"
11 |     end
12 | 
13 |     def render(hash = {}, save_to: nil)
14 |       raise GeneralError, "Template not found: #{name}" if path.nil?
15 | 
16 |       rendered = ERB.new(File.read(@path)).result_with_hash(hash)
17 |       Pathname(save_to).write(rendered) unless save_to.nil?
18 |       rendered
19 |     end
20 |   end
21 | end
22 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj.rb:
--------------------------------------------------------------------------------
1 | require "xccache/core"
2 | Dir["#{__dir__}/#{File.basename(__FILE__, '.rb')}/*.rb"].sort.each { |f| require f }
3 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/build_configuration.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class XCBuildConfiguration
 7 |         def base_configuration_xcconfig
 8 |           path = base_configuration_xcconfig_path
 9 |           Config.new(path) if path
10 |         end
11 | 
12 |         def base_configuration_xcconfig_path
13 |           return base_configuration_reference.real_path if base_configuration_reference
14 |           return unless base_configuration_reference_anchor && base_configuration_reference_relative_path
15 |           project.dir / base_configuration_reference_anchor.path / base_configuration_reference_relative_path
16 |         end
17 |       end
18 |     end
19 |   end
20 | end
21 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/config.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Config
 5 |     def path
 6 |       @filepath
 7 |     end
 8 |   end
 9 | end
10 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/file_system_synchronized_root_group.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class PBXFileSystemSynchronizedRootGroup
 7 |         attribute :name, String
 8 | 
 9 |         def display_name
10 |           return name if name
11 |           return File.basename(path) if path
12 |           super
13 |         end
14 |       end
15 |     end
16 |   end
17 | end
18 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/group.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class PBXGroup
 7 |         def synced_groups
 8 |           children.grep(PBXFileSystemSynchronizedRootGroup)
 9 |         end
10 | 
11 |         def ensure_synced_group(name: nil, path: nil)
12 |           synced_groups.find { |g| g.name == name } || new_synced_group(name: name, path: path)
13 |         end
14 | 
15 |         def new_synced_group(name: nil, path: nil)
16 |           path = path.relative_path_from(project.dir) unless path.relative?
17 |           synced_group = project.new(PBXFileSystemSynchronizedRootGroup)
18 |           synced_group.path = path.to_s
19 |           synced_group.name = name || path.basename.to_s
20 |           self << synced_group
21 |           synced_group
22 |         end
23 |       end
24 |     end
25 |   end
26 | end
27 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/pkg.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       module PkgRefMixin
 7 |         def id
 8 |           local? ? (relative_path || path) : repositoryURL
 9 |         end
10 | 
11 |         def slug
12 |           File.basename(id, File.extname(id))
13 |         end
14 | 
15 |         def local?
16 |           is_a?(XCLocalSwiftPackageReference)
17 |         end
18 | 
19 |         def xccache_pkg?
20 |           local? && ["xccache/packages/umbrella", "xccache/packages/proxy"].include?(id)
21 |         end
22 | 
23 |         def non_xccache_pkg?
24 |           !xccache_pkg?
25 |         end
26 | 
27 |         def create_pkg_product_dependency_ref(product)
28 |           ref = project.new(XCSwiftPackageProductDependency)
29 |           ref.package = self
30 |           ref.product_name = product
31 |           ref
32 |         end
33 | 
34 |         def create_target_dependency_ref(product)
35 |           ref = project.new(PBXTargetDependency)
36 |           ref.name = product
37 |           ref.product_ref = create_pkg_product_dependency_ref(product)
38 |           ref
39 |         end
40 |       end
41 | 
42 |       class XCLocalSwiftPackageReference
43 |         include PkgRefMixin
44 | 
45 |         def ascii_plist_annotation
46 |           # Workaround: Xcode is using display_name while Xcodeproj is using File.basename(display_name)
47 |           # Here, the plugin forces to use display_name so that updates either by Xcode or Xcodeproj are consistent
48 |           " #{isa} \"#{display_name}\" "
49 |         end
50 | 
51 |         def to_h
52 |           {
53 |             "path_from_root" => absolute_path.relative_path_from(Pathname.pwd).to_s,
54 |           }
55 |         end
56 | 
57 |         def absolute_path
58 |           path.nil? ? project.dir / relative_path : path
59 |         end
60 |       end
61 | 
62 |       class XCRemoteSwiftPackageReference
63 |         include PkgRefMixin
64 | 
65 |         def to_h
66 |           { "repositoryURL" => repositoryURL, "requirement" => requirement }
67 |         end
68 |       end
69 |     end
70 |   end
71 | end
72 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/pkg_product_dependency.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class XCSwiftPackageProductDependency
 7 |         def full_name
 8 |           @full_name ||= "#{pkg&.slug || '__unknown__'}/#{product_name}"
 9 |         end
10 | 
11 |         def pkg
12 |           return package if package
13 |           return if @warned_missing_pkg
14 |           @warned_missing_pkg = true
15 |           Log.warn("Missing pkg of product dependency #{uuid}: #{to_hash}")
16 |         end
17 | 
18 |         def remove_alongside_related
19 |           target = referrers.find { |x| x.is_a?(PBXNativeTarget) }
20 |           Log.info(
21 |             "(-) Remove #{product_name.red} from product dependencies of target #{target.display_name.bold}"
22 |           )
23 |           target.dependencies.each { |x| x.remove_from_project if x.product_ref == self }
24 |           target.build_phases.each do |phase|
25 |             phase.files.select { |f| f.remove_from_project if f.product_ref == self }
26 |           end
27 |           remove_from_project
28 |         end
29 |       end
30 |     end
31 |   end
32 | end
33 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/project.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     Log = XCCache::UI
 6 | 
 7 |     def display_name
 8 |       relative_path.to_s
 9 |     end
10 | 
11 |     def relative_path
12 |       @relative_path ||= path.relative_path_from(Pathname(".").expand_path)
13 |     end
14 | 
15 |     def dir
16 |       path.parent
17 |     end
18 | 
19 |     def pkgs
20 |       root_object.package_references
21 |     end
22 | 
23 |     def xccache_pkg
24 |       pkgs.find(&:xccache_pkg?)
25 |     end
26 | 
27 |     def non_xccache_pkgs
28 |       pkgs.reject(&:xccache_pkg?)
29 |     end
30 | 
31 |     def has_pkg?(hash)
32 |       pkg_hash = XCCache::Lockfile::Pkg.from_h(hash)
33 |       pkgs.any? { |p| p.id == pkg_hash.id }
34 |     end
35 | 
36 |     def has_xccache_pkg?
37 |       pkgs.any?(&:xccache_pkg?)
38 |     end
39 | 
40 |     def add_pkg(hash)
41 |       pkg_hash = XCCache::Lockfile::Pkg.from_h(hash)
42 |       pkg_hash["relative_path"] = pkg_hash.relative_path_from_dir(dir).to_s if pkg_hash.key == "path_from_root"
43 | 
44 |       Log.info("Add package #{pkg_hash.id.bold} to project #{display_name.bold}")
45 |       cls = pkg_hash.local? ? XCLocalSwiftPackageReference : XCRemoteSwiftPackageReference
46 |       ref = new(cls)
47 |       custom_keys = ["path_from_root"]
48 |       pkg_hash.each { |k, v| ref.send("#{k}=", v) unless custom_keys.include?(k) }
49 |       root_object.package_references << ref
50 |       ref
51 |     end
52 | 
53 |     def add_xccache_pkg
54 |       sandbox_path = XCCache::Config.instance.spm_proxy_sandbox
55 |       add_pkg("relative_path" => sandbox_path.relative_path_from(path.parent).to_s)
56 |     end
57 | 
58 |     def remove_pkgs(&block)
59 |       pkgs.select(&block).each do |pkg|
60 |         Log.info("(-) Remove #{pkg.display_name.red} from package refs of project #{display_name.bold}")
61 |         pkg.remove_from_project
62 |       end
63 |     end
64 | 
65 |     def get_target(name)
66 |       targets.find { |t| t.name == name }
67 |     end
68 | 
69 |     def get_pkg(name)
70 |       pkgs.find { |p| p.slug == name }
71 |     end
72 | 
73 |     def xccache_config_group
74 |       self["xccache.config"] || new_group("xccache.config")
75 |     end
76 |   end
77 | end
78 | 
--------------------------------------------------------------------------------
/lib/xccache/xcodeproj/target.rb:
--------------------------------------------------------------------------------
 1 | require "xcodeproj"
 2 | 
 3 | module Xcodeproj
 4 |   class Project
 5 |     module Object
 6 |       class PBXNativeTarget
 7 |         alias pkg_product_dependencies package_product_dependencies
 8 | 
 9 |         def non_xccache_pkg_product_dependencies
10 |           pkg_product_dependencies.reject { |d| d.pkg&.xccache_pkg? }
11 |         end
12 | 
13 |         def has_xccache_product_dependency?
14 |           pkg_product_dependencies.any? { |d| d.pkg&.xccache_pkg? }
15 |         end
16 | 
17 |         def has_pkg_product_dependency?(name)
18 |           pkg_product_dependencies.any? { |d| d.full_name == name }
19 |         end
20 | 
21 |         def add_pkg_product_dependency(name)
22 |           Log.info("(+) Add dependency #{name.blue} to target #{display_name.bold}")
23 |           pkg_name, product_name = name.split("/")
24 |           pkg = project.get_pkg(pkg_name)
25 |           pkg_product_dependencies << pkg.create_target_dependency_ref(product_name).product_ref
26 |         end
27 | 
28 |         def add_xccache_product_dependency
29 |           add_pkg_product_dependency("proxy/#{name}.xccache")
30 |         end
31 | 
32 |         def remove_xccache_product_dependencies
33 |           remove_pkg_product_dependencies { |d| d.pkg&.xccache_pkg? }
34 |         end
35 | 
36 |         def remove_pkg_product_dependencies(&block)
37 |           package_product_dependencies.select(&block).each(&:remove_alongside_related)
38 |         end
39 |       end
40 |     end
41 |   end
42 | end
43 | 
--------------------------------------------------------------------------------
/xccache.gemspec:
--------------------------------------------------------------------------------
 1 | lib = File.expand_path("lib", __dir__)
 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
 3 | 
 4 | Gem::Specification.new do |spec|
 5 |   spec.name          = "xccache"
 6 |   spec.version       = File.read("VERSION").strip
 7 |   spec.authors       = ["Thuyen Trinh"]
 8 |   spec.email         = ["trinhngocthuyen@gmail.com"]
 9 |   spec.description   = "A Ruby gem"
10 |   spec.summary       = spec.description
11 |   spec.homepage      = "https://github.com/trinhngocthuyen/xccache"
12 |   spec.license       = "MIT"
13 | 
14 |   spec.files         = Dir["{lib,bin}/**/*"]
15 |   spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16 |   spec.require_paths = ["lib"]
17 | 
18 |   spec.add_dependency "claide"
19 |   spec.add_dependency "parallel"
20 |   spec.add_dependency "tty-cursor"
21 |   spec.add_dependency "tty-screen"
22 |   spec.add_dependency "xcodeproj", ">= 1.26.0"
23 | end
24 | 
--------------------------------------------------------------------------------