Compare commits

..

409 Commits

Author SHA1 Message Date
Bartosz Polaczyk 352e72f44c Merge pull request #199 from polac24/add-graceful-missing-common-sha
Add support for graceful handling missing common sha
2023-04-20 10:04:08 -04:00
Bartosz Polaczyk 1c67b79a7a Apply suggestions from code review
Co-authored-by: Aleksander Grzyb <aleksander.grzyb@gmail.com>
2023-04-19 21:44:17 -04:00
Bartosz Polaczyk c7de203741 Fix linting 2023-04-18 19:51:10 -07:00
Bartosz Polaczyk b7e18916e6 Add support for graceful handling missing common sha 2023-04-18 19:41:53 -07:00
Bartosz Polaczyk dfb4039404 Merge pull request #198 from polac24/bartosz/20230413-override-exclusions
Set libtool Build Setting for excluded sdks
2023-04-14 20:21:37 -07:00
Bartosz Polaczyk b28613a2ef Fix formatting 2023-04-13 18:47:54 -07:00
Bartosz Polaczyk 1535b762bc Cleanup unnecessary unless 2023-04-13 18:33:10 -07:00
Bartosz Polaczyk e6816846c3 Add original LIBTOOL to excluded SDKs 2023-04-13 18:27:27 -07:00
Bartosz Polaczyk b89d98f411 Reset LIBTOOL to libtool for excluded sdks 2023-04-12 17:43:22 -07:00
Bartosz Polaczyk a0d3d1b0b9 Reset SWIFT_EXEC to swiftc for excluded sdks 2023-04-11 22:33:20 -07:00
Bartosz Polaczyk f432917505 Merge pull request #197 from polac24/fix-exclude-sdk
Reset cflags and swiftflags on each cocoapods integration
2023-04-11 18:49:32 -07:00
Bartosz Polaczyk 5d297a4fb2 Revert incorrect OTHER_SWIFT_FLAGS format 2023-04-10 22:04:45 -07:00
Bartosz Polaczyk b0d5f1660e Reset cflags and swiftflags on each cocoapods integration 2023-04-10 19:30:32 -07:00
Bartosz Polaczyk baea2de79a Merge pull request #196 from polac24/exclude-sdks
Add support for excluded sdks
2023-04-10 19:20:09 -07:00
Bartosz Polaczyk d2803f4ad5 Fix linter 2023-04-09 11:40:46 -07:00
Bartosz Polaczyk ab017367b2 Add disabled support in Marking 2023-04-09 11:35:50 -07:00
Bartosz Polaczyk 30cb648641 Add disabled support in XCPostbuild 2023-04-09 11:06:50 -07:00
Bartosz Polaczyk 3330ca45f8 Cleanup README.md 2023-04-09 09:33:42 -07:00
Bartosz Polaczyk d3193b15a8 Fix hook 2023-04-09 09:30:42 -07:00
Bartosz Polaczyk 7b14f2c9ff Revert e2e cleanups 2023-04-09 09:24:24 -07:00
Bartosz Polaczyk 4933b454a5 Reset build settings in cocoapods 2023-04-09 09:23:49 -07:00
Bartosz Polaczyk 79ffdce295 Fix standalone validation 2023-04-08 20:03:30 -07:00
Bartosz Polaczyk 376e6a17c5 Fix linting 2023-04-08 19:47:42 -07:00
Bartosz Polaczyk a4d1849821 Fix linting 2023-04-08 19:36:35 -07:00
Bartosz Polaczyk 325fb07080 Add explicit switch 2023-04-08 19:27:58 -07:00
Bartosz Polaczyk a2afe62751 Fix configs 2023-04-08 17:51:09 -07:00
Bartosz Polaczyk bbaa374e12 Fix configs jsons 2023-04-08 17:46:41 -07:00
Bartosz Polaczyk 39a259ff49 Fix hook 2023-04-08 17:39:12 -07:00
Bartosz Polaczyk a2b9cbf332 Add cocoapods E2E tests 2023-04-08 17:29:35 -07:00
Bartosz Polaczyk 5710594bc4 Bump version to 0.0.16 2023-04-08 04:37:35 -07:00
Bartosz Polaczyk 7d123792b8 Add cocoapods support 2023-04-08 04:37:14 -07:00
Bartosz Polaczyk 5856dbec77 Add skippedSDKs 2023-04-08 04:19:28 -07:00
Bartosz Polaczyk 600310f44b Merge pull request #194 from polac24/space-aws
Encode path in AWS V4 signing requests
2023-04-03 07:24:02 -07:00
Bartosz Polaczyk aae2b3289c Use addingPercentEncoding for path escaping 2023-03-31 18:33:39 -07:00
Bartosz Polaczyk 11eabdab3d Merge pull request #188 from polac24/bartosz/20230310-lipo
Introduce xclipo to mock lipo
2023-03-21 09:55:30 -04:00
Bartosz Polaczyk de24e609ef Delete leftover comment 2023-03-11 10:34:24 -05:00
Bartosz Polaczyk 850983cbde Fix formatting 2023-03-11 10:21:42 -05:00
Bartosz Polaczyk 0064335cc7 Introduce WatchApp to the Standalone E2E project 2023-03-11 10:16:47 -05:00
Bartosz Polaczyk 1ddadcb361 Add xclipo 2023-03-11 10:15:29 -05:00
Bartosz Polaczyk 2f10c6a3a0 Merge pull request #185 from polac24/bartosz/20230306-watch-library
Support universal binary framework
2023-03-08 15:21:10 -05:00
Bartosz Polaczyk c7cd649aab Fix formatting 2023-03-08 06:02:24 -05:00
Bartosz Polaczyk 8201f7778b Fix for static frameworks 2023-03-08 05:33:39 -05:00
Bartosz Polaczyk 504393c3e3 Bundle optional private.swiftinterface 2023-03-08 05:29:02 -05:00
Aleksander Grzyb 82334dda04 Merge pull request #181 from polac24/lddplus-automatic
Add LDPLUSPLUS in automatic integration mode
2023-01-09 09:24:34 +01:00
Bartosz Polaczyk 1072979479 Fix formatting 2023-01-08 10:14:38 -08:00
Bartosz Polaczyk d741b3f6df Add unit tests 2023-01-08 09:59:44 -08:00
Bartosz Polaczyk a0f20b4da3 Add LDPLUSPLUS in automatic mode 2023-01-08 09:53:03 -08:00
Aleksander Grzyb f325b74796 Merge pull request #177 from polac24/pch-c-header
Compile locally PCH for c-header
2023-01-06 06:40:12 +01:00
Aleksander Grzyb a50eae615c Merge pull request #178 from polac24/xcode-1420
Switch to Xcode 14.2
2023-01-02 10:46:23 +01:00
Bartosz Polaczyk 3c8f062e95 Remove whitespaces 2022-12-30 10:59:16 -08:00
Bartosz Polaczyk 1cf685e197 Switch to Xcode 14.2 2022-12-30 10:57:49 -08:00
Bartosz Polaczyk d2ba874079 Compile locally PCH for c-header 2022-12-30 10:52:11 -08:00
Bartosz Polaczyk b439674378 Merge pull request #164 from polac24/20220831-optional-public-headers-folder
Make PUBLIC_HEADERS_FOLDER_PATH ENV optional
2022-11-24 05:27:57 +01:00
Bartosz Polaczyk de066f2b1c Merge pull request #175 from polac24/polac24-private-swiftinterface
Add SPI files: private.swiftinterface and abi.json
2022-11-18 15:47:03 +01:00
Bartosz Polaczyk 73d7a13246 Fix linter issue 2022-11-14 20:27:45 -08:00
Bartosz Polaczyk 75fdd27a5f Add unit tests for SPI changes in Xcode14 2022-11-14 20:00:18 -08:00
Bartosz Polaczyk 2fa1f4e927 Update SwiftmoduleFileExtension.swift 2022-11-14 06:34:45 +01:00
Bartosz Polaczyk 0a64893489 Bundle optional private.swiftinterface 2022-11-14 06:28:10 +01:00
Vadim Smal 56850cf2b0 Merge pull request #167 from CognitiveDisson/catalog-info
Add spotify OSS maintainer metadata
2022-09-01 15:56:55 +01:00
Vadim Smal cef92d2f0b Add spotify OSS maintainer metadata 2022-09-01 12:23:49 +01:00
Bartosz Polaczyk b1507b6e60 Fix linting 2022-08-31 13:42:30 +02:00
Bartosz Polaczyk c76c8a7672 Make PUBLIC_HEADERS_FOLDER_PATH optional 2022-08-31 13:22:13 +02:00
Bartosz Polaczyk 816a9c07c3 Merge pull request #149 from polac24/20220606-publish-swifth-md5
Support exposing enums from ObjC via Bridging headers
2022-08-25 09:53:50 +02:00
Vadim Smal 1e741bc859 Merge pull request #159 from polac24/update-docs-readme
Update Docs link in Readme
2022-08-24 11:13:35 +01:00
Vadim Smal 398b9b11e4 Merge pull request #161 from CognitiveDisson/features/add-retry-logic-for-download
Add retry logic for download
2022-08-24 09:26:18 +01:00
Vadim Smal cb76934ca2 Add retry logic for download 2022-08-11 18:06:20 +01:00
Bartosz Polaczyk aa92805e14 Update Docs link in Readme 2022-08-02 20:22:15 +02:00
Vadim Smal d6355074b2 Merge pull request #158 from CognitiveDisson/maxConcurrentRequests
Add upload_batch_size
2022-08-02 13:59:00 +01:00
Vadim Smal 070e671ddb Merge pull request #156 from polac24/20220711-fix-incremental
[CocoaPodsPlugin] Regenerate cached projects when XCRC is finally on
2022-07-31 21:25:52 +01:00
Vadim Smal 4efdbabf3e Check retryDelay and uploadBatchSize reading from rcinfo 2022-07-20 12:54:51 +01:00
Vadim Smal f33819da60 Rename max_concurrent_requests to upload_batch_size 2022-07-19 17:58:47 +01:00
Vadim Smal f16e6b06f3 Add maxConcurrentRequests 2022-07-19 10:22:09 +01:00
Vadim Smal 25ff5a790b Merge pull request #157 from CognitiveDisson/addUploadRetryDelay
Add a delay between network request retries
2022-07-15 18:38:20 +01:00
Vadim Smal f446f6061d Fix config read 2022-07-15 18:01:22 +01:00
Bartosz Polaczyk 4262620c57 Update Sources/XCRemoteCache/Artifacts/ArtifactProcessor.swift
Co-authored-by: PatrikBillgren <PatrikBillgren@users.noreply.github.com>
2022-07-15 16:03:54 +02:00
Vadim Smal b46d1e2ca1 Fixed retryDelay decoding 2022-07-14 18:06:35 +01:00
Vadim Smal 88d0666da9 Fix swiftlint violations 2022-07-14 17:44:31 +01:00
Vadim Smal 08bf5c21bf Update documentation 2022-07-14 17:38:48 +01:00
Vadim Smal be97a6a247 Fix swiftlint violations 2022-07-14 17:37:46 +01:00
Vadim Smal dff71f8070 Add a delay between upload retries 2022-07-14 16:59:40 +01:00
Bartosz Polaczyk 36fc5ae1e4 Bump plugin version 2022-07-11 22:49:13 +02:00
Bartosz Polaczyk 2d7c881b3b Do not skip install cache invalidation in consumer 2022-07-11 22:48:45 +02:00
Bartosz Polaczyk aed4a124cd [CocoaPodsPlugin] Regenerate cached projects when XCRC is finally enabled 2022-07-11 21:10:57 +02:00
Vadim Smal cda5fc1188 Add a delay between upload retries 2022-07-11 17:21:51 +01:00
Bartosz Polaczyk 2a5c6edfce More cleanup 2022-06-20 21:28:17 +02:00
Bartosz Polaczyk 7a1703e70f Pre-review cleanup 2022-06-20 21:24:47 +02:00
Bartosz Polaczyk 15586755d2 Merge pull request #152 from polac24/xcode14-fallback
Xcode 14.0 support
2022-06-20 09:29:41 +02:00
Bartosz Polaczyk e978eb182c Add limitation 2022-06-18 17:51:54 +02:00
Bartosz Polaczyk 7e98cebdd9 Merge remote-tracking branch 'upstream/master' into xcode14-fallback 2022-06-18 17:48:18 +02:00
Bartosz Polaczyk fbc2982aa3 [Xcode 14.0] Disable Swift integration driver 2022-06-18 17:43:33 +02:00
Bartosz Polaczyk cbcc028cad Merge pull request #151 from ainopara/fix/denpendency-writer-white-space
Escape white space in denpendency writer
2022-06-14 15:25:57 +02:00
ainopara 2acf97eca1 Fix lint issue 2022-06-14 13:37:33 +08:00
ainopara 40803cf747 Add unit test 2022-06-13 14:45:15 +08:00
ainopara 97496ed7b8 Escape white space in denpendency writer 2022-06-13 01:35:15 +08:00
Bartosz Polaczyk 45222f8e33 Merge pull request #150 from jarbogast-salesforce/cplusplus-support
Support C++ Linker
2022-06-11 22:26:58 +02:00
Jonathan Arbogast 91b5b5e590 Support C++ Linker 2022-06-11 15:07:30 -04:00
Bartosz Polaczyk 8222478817 Delete previous -Swift.h.md5 2022-06-07 08:53:53 -04:00
Bartosz Polaczyk 7a1b5267bf Merge remote-tracking branch 'upstream/master' into 20220606-publish-swifth-md5 2022-06-07 08:52:44 -04:00
Bartosz Polaczyk 2bd50e1c19 Delete old override for .h 2022-06-07 08:49:17 -04:00
Bartosz Polaczyk f2a7880c24 Merge pull request #148 from polac24/20220606-integrate-postbuild
Place postbuild build phase right after compilation
2022-06-07 07:29:14 -04:00
Bartosz Polaczyk 00cb8cc23d Merge pull request #147 from polac24/20220606-bridging-headers-enum-test
Add E2E test for exposing public enums from ObjC to Swift
2022-06-07 07:28:56 -04:00
Bartosz Polaczyk 6d0fd51c8e Add docs 2022-06-06 18:15:58 -04:00
Bartosz Polaczyk 65a16d0964 Use -Swift.h.md5 placeholder if available 2022-06-06 18:02:47 -04:00
Bartosz Polaczyk a152d9b159 Add support for generating md5 for -Swift.h 2022-06-06 18:01:27 -04:00
Bartosz Polaczyk a3cd6bea07 Add postbuild right after compilation for CocoaPods 2022-06-06 17:53:11 -04:00
Bartosz Polaczyk 86c762b070 Add postbuild right after compilation for integrate 2022-06-06 17:49:44 -04:00
Bartosz Polaczyk bd21156695 Add E2E test for exposing public enums from ObjC to Swift 2022-06-06 17:34:14 -04:00
Bartosz Polaczyk 3a82ad91b2 Merge pull request #144 from devMEremenko/fix-caching-in-env-fingerprint-generator
Fixed caching in `generatedFingerprint` in `EnvironmentFingerprintGenerator`
2022-06-02 23:03:38 +02:00
Maxim Eremenko 5a5bf35c4a Clean up 2022-06-02 22:46:10 +03:00
Maxim Eremenko 1f766ad4a4 Fixed SwiftLint in EnvironmentFingerprintGeneratorTests 2022-06-02 22:34:23 +03:00
Maxim Eremenko 3c3cd84d81 Fixed swiftlint issues 2022-06-02 22:23:42 +03:00
Maxim Eremenko 7728733aef Cached generatedFingerprint in EnvironmentFingerprintGenerator 2022-06-02 21:06:41 +03:00
Bartosz Polaczyk 50580bf9fd Merge pull request #139 from samuelsainz/feature/postbuild-dependencies-reader-perf-improvement
Improved DependenciesReader performance (~10x faster)
2022-05-24 17:40:21 +02:00
Samuel Sainz ceaff318d6 Fixed SwiftLint issues and CR improvements 2022-05-24 11:09:18 -03:00
Samuel Sainz 4cc932a592 Improved Dependencies Reader performance to be 10x faster 2022-05-23 15:31:41 -03:00
Bartosz Polaczyk 75bef0baf6 Merge pull request #136 from polac24/20220517-fix-vendored-frameworks
Support Vendored frameworks in the CocoaPods development pod
2022-05-19 09:03:04 +02:00
Bartosz Polaczyk 03671e71b7 Support Vendored frameworks in the CocoaPods plugin 2022-05-17 19:46:15 +02:00
Bartosz Polaczyk 91505a59d2 Merge pull request #135 from polac24/fix-hybrid-incremental
Fix hybrid incremental performance
2022-05-16 09:43:46 +02:00
Bartosz Polaczyk 59c1d999b1 Always clean history when prebuilding 2022-05-15 20:04:27 +02:00
Bartosz Polaczyk ba58c1c21e Do not add marker dependency 2022-05-15 19:59:42 +02:00
Bartosz Polaczyk 96ce18bc31 Retrigger clang compilations when a target is switching between cache miss<->hit 2022-05-13 12:07:12 +02:00
Bartosz Polaczyk 30e49ef7bc Merge pull request #132 from zzzworm/master
Update gem_version.rb
2022-05-12 12:25:57 +02:00
zzzworm d46f09c6dd Update gem_version.rb
just update the gem version to fix pod install error when disable XCRemoteCache
2022-05-12 15:30:44 +08:00
Bartosz Polaczyk 2fdf6c39e2 Merge pull request #131 from zzzworm/master
fix pod install  error when disable cache
2022-05-12 06:48:03 +02:00
zhouchanghong 064b22a2d1 fix pod install error when disable cache 2022-05-11 17:45:26 +08:00
Bartosz Polaczyk ddeffe75d6 Merge pull request #130 from polac24/irrelevant-dependency
Add irrelevant dependencies property
2022-05-11 08:59:32 +02:00
Bartosz Polaczyk 181997e3d2 Test fix 2022-05-11 08:37:01 +02:00
Bartosz Polaczyk 1d6ac2c171 Apply reviewer suggestions 2022-05-11 08:30:10 +02:00
Bartosz Polaczyk 3dfd6e296f Typo cleanup 2022-05-10 21:31:34 +02:00
Bartosz Polaczyk c39b286222 Fix swiftlint 2022-05-10 13:15:22 +02:00
Bartosz Polaczyk a55a4547ac Add skipped dependencies regex 2022-05-10 12:58:19 +02:00
Bartosz Polaczyk 51c2007c5b Merge pull request #129 from polac24/docs
Generate gh-pages docs using docc
2022-05-09 09:22:08 +02:00
Bartosz Polaczyk fc092fc4e3 Merge pull request #128 from zzzworm/master
cocoapods plugin support custom FAKE_SRCROOT
2022-05-08 10:55:55 +02:00
Bartosz Polaczyk 3f9596f6e6 Bump to Xcode 13.3.1 2022-05-07 18:00:43 +02:00
Bartosz Polaczyk 15f4ee7eab Generate gh-pages docs using docc 2022-05-07 17:55:26 +02:00
zhouchanghong a6325db0a5 [Docs] add fake_src_root doc for cocoapods plugin 2022-05-07 19:47:11 +08:00
zhouchanghong cfa4fbd070 cocoapods plugin support custom FAKE_SRCROOT 2022-05-07 19:40:10 +08:00
Bartosz Polaczyk c573ced4f4 Add irrelevant_dependencies_paths property 2022-05-07 09:29:20 +02:00
Bartosz Polaczyk 0c2afc15fc Merge pull request #125 from ainopara/master
Fix xcode preview compile issue
2022-04-27 17:48:06 +02:00
ainopara 586e8df56e Fix xcode preview compile issue
When SWIFT_EXEC and LD is setted in a terget, files in the taget can not be previewed due to compile failure which is caused by missing argument.
2022-04-26 19:20:09 +08:00
Bartosz Polaczyk ef3a88c6ec Merge pull request #122 from polac24/20220418-derived
Fix building target dependencies list with custom DERIVED_FILE_DIR
2022-04-19 09:32:43 +02:00
Bartosz Polaczyk 95f95be29e Identify custom DerivedData under sources dir 2022-04-18 09:07:36 +02:00
Bartosz Polaczyk 4d12800096 Skip all DerivedData files in dependencies list 2022-04-18 09:04:17 +02:00
Bartosz Polaczyk fbb456b0e0 Merge pull request #121 from cezarsignori/csignori/rc_derived_files_dir
Set auto-generated Swift header under DERIVED_FILES_DIR as irrelevant dependency
2022-04-18 08:57:44 +02:00
Cezar Signori a321ea3362 Set auto-generated Swift header under DERIVED_FILES_DIR as irrelevant dependency 2022-04-14 14:20:30 +02:00
Bartosz Polaczyk 963e6858ee Merge pull request #117 from polac24/20220411-loop-configs
Load extra configuration in a chain
2022-04-12 22:31:46 +02:00
Bartosz Polaczyk f9999a402f Merge pull request #118 from polac24/20220412-bump-cocoapods
Bump CocoaPods plugin to 0.0.9
2022-04-12 22:25:42 +02:00
Bartosz Polaczyk dbfe1dd8d4 Bump CocoaPods plugin to 0.0.9 2022-04-12 15:44:52 +02:00
Bartosz Polaczyk def12fab6f Merge pull request #116 from samuelsainz/bugfix/prebuild-phase-order
Fix error that moves the Prebuild phase script after the source phase
2022-04-12 00:01:12 +02:00
Bartosz Polaczyk 6fee69f081 Fix linter 2022-04-11 20:10:43 +02:00
Bartosz Polaczyk b74bafa5d7 Cleanup tests 2022-04-11 19:53:33 +02:00
Bartosz Polaczyk 33de361317 Add Readme docs 2022-04-11 19:40:00 +02:00
Bartosz Polaczyk 0aeb5aee36 Allow importing extra config multiple times 2022-04-11 19:37:36 +02:00
Samuel Sainz 8682731f30 Fix error that moves the Prebuild phase script after the source build phase 2022-04-11 11:03:15 -03:00
Bartosz Polaczyk 62b637bdaa Merge pull request #113 from polac24/cocoapods-bump-008
Bump cocoapods plugin version 0.0.8
2022-04-04 09:27:19 +02:00
Bartosz Polaczyk 0bca5e5bc4 Bump cocoapods plugin version 0.0.8 2022-04-04 08:14:06 +02:00
Bartosz Polaczyk eb0d4b17b7 Merge pull request #95 from polac24/20220226-pch-suport
Switch to local PCH headers compilation
2022-04-01 08:41:39 +02:00
Bartosz Polaczyk d9ea5bfd49 Apply suggestions from code review
Co-authored-by: Aleksander Grzyb <aleksander.grzyb@gmail.com>
2022-04-01 08:24:07 +02:00
Bartosz Polaczyk ee8bf69814 Add comments 2022-03-31 22:09:55 +02:00
Bartosz Polaczyk 2c0f78056f Merge branch 'master' into 20220226-pch-suport 2022-03-31 22:03:10 +02:00
Bartosz Polaczyk b6b17bdc8a Merge pull request #111 from devMEremenko/integrate-aws-security-token
Integrate AWS Temporary Access Keys
2022-03-31 20:15:30 +02:00
Maxim Eremenko a387359930 Removed trailing_whitespace 2022-03-31 20:44:13 +03:00
Maxim Eremenko 456233b90e Update tests 2022-03-30 23:46:15 +03:00
Maxim Eremenko d6f6e2e9bc Integrate AWS Temporary Security Token 2022-03-30 23:21:22 +03:00
Bartosz Polaczyk a35bd0b394 Merge pull request #110 from samuelsainz/bugfix/mark-script_build-config-param
Adding quotes to the config param in the Mark script
2022-03-25 23:20:02 +01:00
Samuel Sainz def131f2a0 Fix config param in Mark script considering build configs with a space in the name 2022-03-25 17:40:17 -03:00
Bartosz Polaczyk c052ed8ed6 Merge pull request #105 from cezarsignori/csignori/xcrc_fake_src_root
Set provided fakeSrcRoot on Xcode proj build settings as XCRC_FAKE_SRCROOT
2022-03-22 17:00:59 +01:00
Cezar Signori d4f9486b92 Set provided fakeSrcRoot on Xcode proj build settings as XCRC_FAKE_SRCROOT 2022-03-22 16:26:16 +01:00
Bartosz Polaczyk 8241914543 Merge pull request #106 from alecgarza96/master
Fixes issue #49
2022-03-21 20:08:19 +01:00
alecgarza96 b5ff16484f removed trailing whitespace 2022-03-21 11:31:09 -05:00
alecgarza96 2b9dde9aec added support for both ".S" and ".s" extensions 2022-03-21 11:16:40 -05:00
alecgarza96 136e7a99ff added .s file extension for assembly support 2022-03-10 14:40:58 -06:00
Bartosz Polaczyk c626d51f97 Merge pull request #99 from polac24/20220304-document-debugging
[Docs] Local development steps
2022-03-07 17:29:49 +01:00
Bartosz Polaczyk e8ddc9297d [Docs] Local development steps 2022-03-04 18:47:29 +01:00
Bartosz Polaczyk 3b614c6172 Merge pull request #97 from polac24/20220228-fix-swiftlint
Fix swiftlint errors
2022-03-02 08:47:44 +01:00
Bartosz Polaczyk 87a214104e Add headers linting 2022-03-01 22:58:00 +01:00
Bartosz Polaczyk 599e1e229d Fix linter errors 2022-03-01 22:44:21 +01:00
Bartosz Polaczyk 423da7cc4a Enable trailing_dot_in_comments 2022-03-01 22:44:03 +01:00
Bartosz Polaczyk 4b082e9dd2 Run CI linting in strict mode 2022-03-01 22:22:45 +01:00
Vadim Smal 36803d6b5d Merge pull request #96 from CognitiveDisson/update-dependencies
Update dependencies
2022-03-01 08:54:36 +00:00
Bartosz Polaczyk 5809bc963c Fix serious errors 2022-02-28 23:14:52 +01:00
Bartosz Polaczyk cb6626cfbc Add exclude to E2E Pods 2022-02-28 22:57:45 +01:00
Bartosz Polaczyk 3e18711e09 Apply autocorrect 2022-02-28 22:56:34 +01:00
Bartosz Polaczyk ccda424791 Formatting 2022-02-28 22:55:04 +01:00
Vadim Smal 90d784cc8d Update dependencies 2022-02-28 16:05:59 +00:00
Bartosz Polaczyk 6715824195 Update Sources/XCRemoteCache/Commands/Prepare/CCWrapperBuilder.swift 2022-02-28 08:29:35 +01:00
Bartosz Polaczyk c4e08d4288 Remove limitation 2022-02-26 19:11:54 +01:00
Bartosz Polaczyk a2067f8dfe Add unit test 2022-02-26 18:37:56 +01:00
Bartosz Polaczyk 253e9597bd Force compilation success in xccc generation tests 2022-02-26 18:37:56 +01:00
Bartosz Polaczyk 4a93f944fd Fallback when compiling pch header 2022-02-26 16:03:54 +01:00
Bartosz Polaczyk f839b4064b Merge pull request #93 from polac24/20220223-add-troubleshootings
[Docs] Add troubleshooting tips
2022-02-24 20:48:29 +01:00
Bartosz Polaczyk e71837b8b2 Merge pull request #94 from polac24/20220223-missing-overlay-log
Print overlay error messages to os_log only
2022-02-24 20:48:12 +01:00
Bartosz Polaczyk b45792646b Apply suggestions from code review
Co-authored-by: Aleksander Grzyb <aleksander.grzyb@gmail.com>
2022-02-24 08:46:57 +01:00
Bartosz Polaczyk 1966562eef Print overlay error messages to logs only 2022-02-23 21:58:03 +01:00
Bartosz Polaczyk 883b207c5b Fix formatting 2022-02-23 21:43:40 +01:00
Bartosz Polaczyk ca137d0ce4 Add troubleshooting steps 2022-02-23 21:24:07 +01:00
Bartosz Polaczyk b9a633c86f Merge pull request #91 from polac24/20220222-disable-urlcache
Disable local URLSession cache
2022-02-22 17:33:59 +01:00
Bartosz Polaczyk 55a87eb4e9 Disable local URLSession cache 2022-02-22 14:37:06 +01:00
Bartosz Polaczyk e6b56024b9 Merge pull request #90 from polac24/bump-cocoapods-007
Bump CocoaPods plugin version
2022-02-21 10:25:45 +01:00
Bartosz Polaczyk 8c34a31110 Bump CocoaPods plugin version 2022-02-19 08:54:22 +01:00
Bartosz Polaczyk f7c32d6e80 Merge pull request #87 from polac24/20220217-custom-mappings-envs
Customize rewritting dependency paths
2022-02-18 11:30:51 +01:00
Bartosz Polaczyk b3a16ae5d0 Merge pull request #88 from polac24/20220217-decorate-logs
Decorate logs with a target name
2022-02-18 11:30:35 +01:00
Bartosz Polaczyk d013fe4c81 Decorate logs with a target name 2022-02-17 20:35:13 +01:00
Bartosz Polaczyk 4aefee078e Customize rewritting paths 2022-02-17 20:13:25 +01:00
Bartosz Polaczyk 9363e68d51 Merge pull request #85 from polac24/20220214-remapper-stable
Do not change dependencies order when remapping overlay
2022-02-15 09:06:50 +01:00
Bartosz Polaczyk 4af8156da5 Do not change dependencies order when remapping overlay 2022-02-14 18:46:51 +01:00
Bartosz Polaczyk f086feb005 Merge pull request #83 from polac24/issue-template
Add issue templates
2022-02-14 09:01:51 +01:00
Bartosz Polaczyk d98d4cd0a3 Apply suggestions from code review 2022-02-14 08:46:25 +01:00
Bartosz Polaczyk a38d445ae9 Merge pull request #82 from polac24/20220210-best-effort-overlay
Do not fail prebuild/postbuild for invalid vfs overlay
2022-02-14 08:45:22 +01:00
Bartosz Polaczyk 0436f6ae27 Separate templates 2022-02-13 21:55:56 +01:00
Bartosz Polaczyk be78959437 Add Issue template 2022-02-11 18:10:00 +01:00
Bartosz Polaczyk cccae0d9f6 Add disable_vfs_overlay feature flag 2022-02-10 21:03:53 +01:00
Bartosz Polaczyk e7d1e905cf Do not throw overlay for the best-effort mode 2022-02-10 20:57:05 +01:00
Bartosz Polaczyk 20d53a1c71 Merge pull request #75 from polac24/20220209-prebuild-index
[Integrate] Place xcprebuild script right before compilation
2022-02-10 20:17:16 +01:00
Bartosz Polaczyk f4ba03d581 Merge pull request #72 from polac24/20220207-non-throwing-init
Make DependenciesRemapper throwable
2022-02-09 22:36:36 +01:00
Bartosz Polaczyk 0e48d39818 [Integrate] Move prebuild script right before compilation 2022-02-09 17:52:31 +01:00
Bartosz Polaczyk 3b30939f99 Merge pull request #74 from vasvf/master
Rename prebuild and postbuild phases, adjust prebuild position
2022-02-09 17:40:41 +01:00
Vasily Fedorov e263cb6e25 fix comment 2022-02-09 17:01:35 +03:00
Vasily Fedorov b805bf4a99 revert some changes 2022-02-09 16:55:13 +03:00
Vasily Fyodorov fd7b68a344 Merge branch 'spotify:master' into master 2022-02-08 19:18:39 +03:00
Vasily Fedorov a63488a043 Move build configs to xcconfigs 2022-02-08 19:02:38 +03:00
Bartosz Polaczyk 48bcdd8ce9 Apply suggestions from code review
Co-authored-by: PatrikBillgren <PatrikBillgren@users.noreply.github.com>
2022-02-08 16:49:57 +01:00
Bartosz Polaczyk 41dd1cae03 Change to throwing remapping 2022-02-07 20:40:44 +01:00
Bartosz Polaczyk 1e86cac3ec Merge pull request #71 from polac24/20220204-overlay-mapper
Enable virtual file system overlay replacements
2022-02-07 20:39:53 +01:00
Bartosz Polaczyk 22faa5dbdb Fix comments 2022-02-07 19:39:22 +01:00
Bartosz Polaczyk 522900748d Change DerivedData's path for consumer 2022-02-07 19:35:55 +01:00
Bartosz Polaczyk 4998cc4f87 Merge remote-tracking branch 'upstream/master' into 20220204-overlay-mapper 2022-02-07 19:31:10 +01:00
Bartosz Polaczyk 9221f9d2b5 Merge pull request #70 from polac24/20220204-overlay-reader
Parse vfs overlay file
2022-02-07 19:28:04 +01:00
Bartosz Polaczyk 1127257ad5 Merge pull request #67 from vasvf/resolve_symlinks
Resolve symlinks and dirpaths in dependency processor
2022-02-07 19:26:56 +01:00
Bartosz Polaczyk 599e5fe561 Merge pull request #64 from polac24/20220127-automate-e2e
Add E2E automation
2022-02-07 17:52:39 +01:00
Bartosz Polaczyk 478649a1d7 Add docs 2022-02-05 21:58:00 +01:00
Bartosz Polaczyk 1c5aa569dd Cleanup xcodeproj 2022-02-05 21:37:36 +01:00
Bartosz Polaczyk c6b31d3086 Disable exclusive DD paths 2022-02-05 21:37:06 +01:00
Bartosz Polaczyk f0a4d361b1 Use separate DerivedData path for consumser 2022-02-05 21:25:34 +01:00
Bartosz Polaczyk 0778639ae2 Extract to a separate file 2022-02-05 21:06:54 +01:00
Bartosz Polaczyk c44b793a19 Fix comments 2022-02-04 22:07:30 +01:00
Bartosz Polaczyk 2b383f046e Add scenario for empty overlay 2022-02-04 21:55:04 +01:00
Bartosz Polaczyk cf0b27d03c Add integration
This reverts commit b2d47760cf.
2022-02-04 21:48:43 +01:00
Bartosz Polaczyk b2d47760cf Revert integration 2022-02-04 19:20:21 +01:00
Bartosz Polaczyk 714c9ef35b Add overlay and related integration 2022-02-04 19:16:08 +01:00
Bartosz Polaczyk b872a8d7f9 Post test cleanup 2022-02-04 19:01:05 +01:00
Bartosz Polaczyk 56b6722dbe Reuse DerivedData for producer and consumer 2022-02-03 21:44:55 +01:00
Bartosz Polaczyk 92875bcdee Change DerivedData path 2022-02-03 20:35:07 +01:00
Bartosz Polaczyk 46a99bbe32 Cleanup 2022-02-03 20:32:45 +01:00
Bartosz Polaczyk 408698f1e8 Switch to nginx 2022-02-03 20:10:51 +01:00
Bartosz Polaczyk e54ce770e7 Start docker 2022-02-03 19:39:06 +01:00
Bartosz Polaczyk 49be11184e Fix dash in docker installer 2022-02-03 19:29:41 +01:00
Bartosz Polaczyk d8850b555a Install docker 2022-02-03 19:11:05 +01:00
Bartosz Polaczyk 93a60b2b40 Fix branch typo 2022-02-03 18:52:40 +01:00
Bartosz Polaczyk cde81d852f add a branch 2022-02-03 18:24:05 +01:00
Bartosz Polaczyk 97328144fd Log remotes 2022-02-03 17:40:42 +01:00
Bartosz Polaczyk 3f8333c07b print log times 2022-02-03 17:02:52 +01:00
Bartosz Polaczyk 2160645f2c do not build again for e2e 2022-02-02 23:45:44 +01:00
Bartosz Polaczyk d9c2213f50 Log xcodebuild to stdout 2022-02-02 23:42:47 +01:00
Bartosz Polaczyk 327d282e23 Merge pull request #66 from vasvf/master
Deintegrate XCRemoteCache from all projects on errors
2022-02-02 20:43:04 +01:00
Vasily Fedorov 06781763aa Resolve symlinks in dependency processor 2022-02-01 20:40:41 +03:00
Vasily Fedorov 60c7a586c7 Also remove unneeded space in CFlags 2022-01-31 15:36:35 +03:00
Vasily Fedorov 2ac3da9035 Deintegrate XCRemoteCache from all projects on errors 2022-01-30 15:43:44 +03:00
Bartosz Polaczyk 4dbc5c9b19 Merge pull request #63 from vasvf/master
support producer-fast mode for plugin
2022-01-30 10:06:19 +01:00
Vasily Fedorov 6d10ac8c7d Fix after code review 2022-01-29 13:22:32 +03:00
Vasily Fedorov 8b8c2d627c Remove CC in plugin also 2022-01-28 12:08:55 +03:00
Vasily Fyodorov b1026b16da Merge branch 'spotify:master' into master 2022-01-28 12:06:23 +03:00
Bartosz Polaczyk e1cc629c55 Merge remote-tracking branch 'upstream/master' into 20220127-automate-e2e 2022-01-28 08:05:10 +01:00
Bartosz Polaczyk f75c77efa2 Merge pull request #61 from polac24/20220126-cocoapods-skip-aggregation
Limit integrating CocoaPods to native targets
2022-01-28 08:04:19 +01:00
Bartosz Polaczyk 3f8ec5b453 WIP 2022-01-27 22:43:49 +01:00
Vasily Fedorov 94b57475e8 support producer-fast mode for plugin, and also remove "cc" config when in producer mode. 2022-01-27 20:23:26 +03:00
Bartosz Polaczyk a30d56ac16 Cleanup native targets limitation 2022-01-27 17:08:30 +01:00
Bartosz Polaczyk 81077edfa4 Align with external Pods 2022-01-26 22:33:57 +01:00
Bartosz Polaczyk da3a5d59fa Merge remote-tracking branch 'upstream/master' into 20220126-cocoapods-skip-aggregation 2022-01-26 22:32:56 +01:00
Bartosz Polaczyk 94490532f7 Merge pull request #57 from vasvf/master
Add support for generate_multiple_pod_projects
2022-01-26 22:32:23 +01:00
Bartosz Polaczyk 326cac7668 Limit integrating CocoaPods to native targets 2022-01-26 21:40:25 +01:00
Vasily Fedorov 44a09befa0 Add support for generate_multiple_pod_projects 2022-01-23 19:43:05 +03:00
Bartosz Polaczyk cdddc5bf19 Merge pull request #56 from vasvf/master
Add support for swift modules with @objc interfaces
2022-01-23 17:31:49 +01:00
Vasily Fedorov 93b8dcd0c3 Also added swiftinterface file check to swiftc tests 2022-01-23 18:21:52 +03:00
Vasily Fedorov da0d1c20d2 Separated testing for including swiftinterface file in artifacts builders 2022-01-23 18:13:42 +03:00
Vasily Fedorov 8155e042b5 Add support for swift modules with @objc interfaces 2022-01-23 03:59:49 +03:00
Bartosz Polaczyk f3832c31bd Merge pull request #55 from polac24/20220118-release-cocoapods-automatically
Release CocoaPods plugin automatically
2022-01-19 10:25:09 +01:00
Bartosz Polaczyk 6a7e6d1135 Add a section link 2022-01-18 21:55:17 +01:00
Bartosz Polaczyk da59c2a211 Document releasing to RubyGems 2022-01-18 21:51:16 +01:00
Bartosz Polaczyk eaba7f3c67 Publish to RubyGems 2022-01-18 21:46:27 +01:00
Bartosz Polaczyk bab3326175 Merge pull request #52 from polac24/20220116-cocoapods-plugin-release
Prepare Cocoapods plugin for a release
2022-01-17 12:21:07 +01:00
Bartosz Polaczyk dc0a82058b Bump plugin version 2022-01-16 12:56:46 +01:00
Bartosz Polaczyk c5c2732cc9 Expose prettify_meta_files and disable_certificate_verification to .rcinfo 2022-01-16 12:56:35 +01:00
Bartosz Polaczyk 46debcfa8a Merge pull request #51 from alekzernov/feature/ssl-cert-not-valid
Added a flag to disable checking the ssl certificate
2022-01-16 12:52:53 +01:00
Зернов Александр 3f6d3af5a5 code review. 2022-01-16 14:23:28 +03:00
Зернов Александр c42cb7ac55 fix hooks 2022-01-13 17:59:41 +03:00
Зернов Александр c65eccc5ee Add certificateVerification setting. 2022-01-13 15:22:06 +03:00
Aleksander Grzyb a7316d35cc Merge pull request #46 from spotify/adjust-xccc-for-apple-silicon
adjust `xccc` to support `arm64`
2022-01-06 13:49:37 +01:00
Aleksander Grzyb 62a7fea0be adjust xccc to support arm64 2022-01-06 09:52:25 +01:00
Bartosz Polaczyk 88e4dceb99 Merge pull request #45 from polac24/20220102-pods-rcinfo
[Pods] Generate unique .rcinfo for Pods directory
2022-01-05 22:00:35 +01:00
Bartosz Polaczyk e8db767d4a Merge pull request #44 from polac24/20211229-fingerprint-include-archs
Always include ARCHS in a fingerprint
2022-01-04 19:26:51 +01:00
Bartosz Polaczyk 12c635e5ca Merge pull request #42 from polac24/20211228-m1-consumer
Patch PLATFORM_PREFERRED_ARCH to support native Apple Silicon builds
2022-01-04 19:26:26 +01:00
Bartosz Polaczyk 7f95da7b7c Generate unique .rcinfo for Pods directory 2022-01-02 11:16:18 +01:00
Bartosz Polaczyk 5892a92546 Update Readme 2021-12-29 19:43:12 +01:00
Bartosz Polaczyk 7aa44f20c1 Include ARCHS in a figerprint 2021-12-29 19:03:26 +01:00
Bartosz Polaczyk 9df2bd5a8e Bump cocoapods plugin version 2021-12-28 19:09:34 +01:00
Bartosz Polaczyk 508b11d6ac Patch PLATFORM_PREFERRED_ARCH to support native Apple Silicon builds 2021-12-28 18:39:18 +01:00
Bartosz Polaczyk 5f2a8409f2 Merge pull request #41 from polac24/20211215-out-of-band
Add out_of_band mappings
2021-12-20 23:13:02 +01:00
Bartosz Polaczyk df627ca374 Prereview cleanup 2021-12-16 20:13:41 +01:00
Bartosz Polaczyk a905cdbddc Extract remapping factory 2021-12-16 20:06:34 +01:00
Bartosz Polaczyk 6995c7c1b7 Replace generic paths in the reverse order 2021-12-16 19:42:31 +01:00
Bartosz Polaczyk 7f43cb87bd Update readme 2021-12-16 19:42:06 +01:00
Bartosz Polaczyk 5ff9888c11 Align out of band with ENV replacements 2021-12-16 10:52:47 +01:00
Bartosz Polaczyk ad545c7802 Simplify path remapper initialization 2021-12-15 17:11:31 +01:00
Bartosz Polaczyk 057c5c3e28 Renames mapping property and adds docs 2021-12-15 16:59:47 +01:00
Bartosz Polaczyk 4116dba33d Add Composiete remapper tests 2021-12-15 16:50:26 +01:00
Bartosz Polaczyk 46cc3b75aa OutOfBandRemapping 2021-12-15 16:27:40 +01:00
Bartosz Polaczyk 2285822ae6 Merge pull request #39 from polac24/20211211-native-arch
Support producer mode on Apple Silicon
2021-12-13 08:35:10 +01:00
Bartosz Polaczyk e518d28723 Merge pull request #40 from polac24/20211212-dark
Add dark mode logo version
2021-12-13 08:34:55 +01:00
Bartosz Polaczyk 3b453b15bc Update README.md
Co-authored-by: PatrikBillgren <PatrikBillgren@users.noreply.github.com>
2021-12-13 08:22:34 +01:00
Bartosz Polaczyk 73edc2c7aa Add dark mode logo version 2021-12-12 16:29:41 +01:00
Bartosz Polaczyk a0c21471b9 Fox development readme 2021-12-12 14:57:12 +01:00
Bartosz Polaczyk 610946f0c4 Fix Readme 2021-12-12 14:51:35 +01:00
Bartosz Polaczyk 53d4f07286 Add readme documentation 2021-12-12 14:46:34 +01:00
Bartosz Polaczyk bc9a77a58f Revert "Bump CocoaPods plugin version"
This reverts commit 8f86917597.
2021-12-12 14:23:24 +01:00
Bartosz Polaczyk e0205f749a Document explicit dependencies rule 2021-12-12 14:23:05 +01:00
Bartosz Polaczyk 0d259b56d3 Recognize building architecturefrom ARCHS 2021-12-12 14:22:46 +01:00
Bartosz Polaczyk 366c485453 Revert "Replace PLATFORM_PREFERRED_ARCH with NATIVE_ARCH"
This reverts commit f9524a6854.
2021-12-12 13:56:20 +01:00
Bartosz Polaczyk 8f86917597 Bump CocoaPods plugin version 2021-12-11 18:36:04 +01:00
Bartosz Polaczyk f9524a6854 Replace PLATFORM_PREFERRED_ARCH with NATIVE_ARCH 2021-12-11 18:34:40 +01:00
Bartosz Polaczyk 9a27fa81a4 Merge pull request #35 from polac24/xcode-1310
Switch CI to Xcode 13.1
2021-12-10 12:11:32 +01:00
Bartosz Polaczyk c55f1a5803 Merge pull request #30 from mihai8804858/pretty-meta-files
Encode json files using pretty format
2021-12-09 22:05:03 +01:00
Mihai Seremet e4d277c8db Remove default value 2021-12-09 11:52:44 +02:00
Bartosz Polaczyk 6cd662bf7d Switch to Xcode 13.1 for releases 2021-12-08 18:39:18 +01:00
Bartosz Polaczyk 56081f75b3 Switch to 13.1 on PR 2021-12-08 18:38:42 +01:00
Mihai Seremet c1cd1ac565 Add pretty meta files parameter 2021-12-08 12:02:42 +02:00
Bartosz Polaczyk 768a296175 Merge pull request #32 from polac24/20211207-fix-coverage-builds
Inspect code coverage with CLANG_COVERAGE_MAPPING ENV
2021-12-08 09:08:41 +01:00
Bartosz Polaczyk 6a1a8c6919 Do not bump schema version 2021-12-08 08:05:51 +01:00
Bartosz Polaczyk 3d02af8ade Inspect code coverage with CLANG_COVERAGE_MAPPING 2021-12-07 20:41:37 +01:00
Bartosz Polaczyk bf90d518f4 Merge pull request #27 from polac24/20211201-debug-map-align
[Pods] Align Debub-prefix-map for Pods project
2021-12-05 20:35:03 +01:00
Bartosz Polaczyk 634afb3f3f Merge pull request #25 from polac24/20211122-producer-fast
Add producer-fast mode
2021-12-05 20:34:48 +01:00
Bartosz Polaczyk c2b80c0112 Do not return optional array in findTargetPackageZip 2021-12-02 22:28:07 +01:00
Bartosz Polaczyk d0604e9042 Fix producer full typo 2021-12-02 22:24:49 +01:00
Bartosz Polaczyk 297c1a90cb Merge remote-tracking branch 'upstream/master' into 20211201-debug-map-align 2021-12-02 22:22:14 +01:00
Bartosz Polaczyk 34cb54b675 Fix typo 2021-12-02 22:19:44 +01:00
Bartosz Polaczyk 14b2b3aceb [CocoapodsPlugin] Support first-party caching for incremental installation 2021-12-02 22:19:43 +01:00
Bartosz Polaczyk cbed913c63 Merge pull request #19 from polac24/20211121-incremental-installation
[CocoaPodsPlugin] Support first-party caching for incremental install
2021-12-02 10:55:47 +01:00
Bartosz Polaczyk 63448ff0a0 Fix typo 2021-12-02 08:58:19 +01:00
Bartosz Polaczyk 64bccaed16 Merge branch 'master' into 20211121-incremental-installation 2021-12-02 08:56:26 +01:00
Bartosz Polaczyk 80a7abb4d5 Update RDoc array definition 2021-12-02 07:26:36 +01:00
Bartosz Polaczyk c50ee6f798 Align Debub-prefix-map for Pods project 2021-12-01 19:47:44 +01:00
Bartosz Polaczyk 6e4bf25d1c Merge pull request #20 from polac24/20211121-update-spm-limitation
Provide a reason why SPM dependencies cannot be supported
2021-11-30 17:43:27 +01:00
Bartosz Polaczyk 71af03f227 Fix user message for producer-fast 2021-11-25 22:14:10 +01:00
Bartosz Polaczyk 0ebe6f5ceb Reuse existing meta sha 2021-11-25 22:11:04 +01:00
Bartosz Polaczyk 29cba26c5d Add tests 2021-11-25 21:38:55 +01:00
Bartosz Polaczyk 08b6115187 Merge remote-tracking branch 'upstream/master' into 20211122-producer-fast 2021-11-25 21:11:27 +01:00
Bartosz Polaczyk bbbb0a5b0f Add unit tests for Postbuild 2021-11-25 21:10:18 +01:00
Bartosz Polaczyk 758764ad95 Refactor to MetaWriter 2021-11-25 20:58:14 +01:00
Bartosz Polaczyk f332593076 Upload meta on the reused artifact scenario 2021-11-24 22:58:09 +01:00
Bartosz Polaczyk d0b2bc0f71 Prereview cleanup 2021-11-24 21:49:30 +01:00
Bartosz Polaczyk e2f68c8f4e Add unit tests for thinning creator plugin 2021-11-24 21:41:34 +01:00
Bartosz Polaczyk dbff760716 Merge pull request #24 from woodencoder/woodencooder/fix_swiftlint_warnings
Fix SwiftLint warnings
2021-11-24 19:47:32 +01:00
Bartosz Polaczyk bb05b02bd8 Merge pull request #22 from mihai8804858/reuse-existing-build-phases
Reuse existing build phases in CocoaPods plugin
2021-11-24 19:40:02 +01:00
Vladislav Klimenko 5c568a1338 Fix SwiftLint warnings 2021-11-24 21:07:40 +03:00
Mihai Seremet 86273017b4 Fix debugging for Pods project 2021-11-24 12:23:46 +02:00
Mihai Seremet 4f1f73132e Support switching between models in plugin 2021-11-23 00:28:08 +02:00
Bartosz Polaczyk ec1ef567cb Add producer-fast mode 2021-11-22 22:18:59 +01:00
Bartosz Polaczyk 1c94e51059 Add producerFast mode 2021-11-22 21:59:19 +01:00
Mihai Seremet ba41e40bb0 Reuse existing build phases in CocoaPods plugin 2021-11-22 19:04:51 +02:00
Bartosz Polaczyk a0c88d9059 Provide a reason why SPM dependencies cannot be supported 2021-11-21 19:41:21 +01:00
Bartosz Polaczyk 15173b9575 [CocoapodsPlugin] Support first-party caching for incremental installation 2021-11-21 14:17:34 +01:00
Bartosz Polaczyk fa82f920ad Merge pull request #17 from tejassharma96/patch-1
Update how and why link in readme
2021-11-17 22:19:28 +01:00
Tejas Sharma 78031a3135 Update how and why link in readme 2021-11-17 12:57:51 -08:00
Bartosz Polaczyk 2156de1706 Merge pull request #14 from polac24/support-readme
Add support session to Readme
2021-11-17 10:45:21 +01:00
Bartosz Polaczyk ce2ef3ea69 Merge pull request #13 from polac24/cocoapods-002
Release CocoaPods plugin 0.0.2
2021-11-17 10:45:02 +01:00
Bartosz Polaczyk 3219ac24aa Merge pull request #15 from eliperkins/patch-1
Fix typo in README
2021-11-17 08:21:23 +01:00
Eli Perkins d9ef32f24f Fix typo in README 2021-11-16 12:23:19 -05:00
Bartosz Polaczyk 3aaf483263 Add a section about slack support 2021-11-16 17:32:47 +01:00
Bartosz Polaczyk cc691b8c67 Add an option to install via RubyGems 2021-11-16 17:10:45 +01:00
Bartosz Polaczyk 4c95ff915f Release CocoaPods plugin 0.0.2 2021-11-16 17:04:18 +01:00
Bartosz Polaczyk 41829e769f Merge pull request #12 from spotify/20211116-cocoapods-artifact
Download XCRemoteCache binary from GitHub for CocoaPods
2021-11-16 08:49:27 +01:00
Bartosz Polaczyk e3261d9bb1 Download XCRemoteCache binary from GitHub 2021-11-16 08:12:03 +01:00
Bartosz Polaczyk b6318e9785 Merge pull request #11 from polac24/add-logo
Add logo and TOC
2021-11-14 19:24:49 +01:00
Bartosz Polaczyk 5a77ab1766 Add TOC 2021-11-14 15:50:28 +01:00
Bartosz Polaczyk faec67a2ff Add logo image 2021-11-14 15:37:54 +01:00
Bartosz Polaczyk 3e309f2b64 Merge pull request #6 from spotify/readme-spm-limitation
Document releasing process and add SPM limitation
2021-10-22 09:47:47 +02:00
Bartosz Polaczyk 88f4d30735 Be explicit about tag formatting
Co-authored-by: Vadim Smal <kenshin312@gmail.com>
2021-10-20 14:31:21 +02:00
Bartosz Polaczyk e3d45b6ad5 Merge pull request #8 from spotify/20211017-place-swiftsourceinfo
Place sourceinfo in Project subdirectory
2021-10-20 11:19:03 +02:00
Bartosz Polaczyk 1c09a7a242 Merge pull request #10 from spotify/20211018-add-indexing-stats
Introduce separate stats for indexbuild
2021-10-20 11:18:37 +02:00
Bartosz Polaczyk 1468d315ab Merge pull request #9 from spotify/20211018-fix-indexbuild-caching
Support not defined object for Swift output map
2021-10-20 11:17:17 +02:00
Bartosz Polaczyk 2077897871 Add unit tests for action 2021-10-18 22:27:45 +02:00
Bartosz Polaczyk b2b1a93911 Add unit test 2021-10-18 21:50:59 +02:00
Bartosz Polaczyk 1205fec047 Add indexing stats 2021-10-18 21:50:32 +02:00
Bartosz Polaczyk a9ee8dceb3 Revert "Merge pull request #5 from CognitiveDisson/fix-cache-miss-for-xcode13"
This reverts commit 0f44da7bea, reversing
changes made to caed3b253c.
2021-10-18 21:50:32 +02:00
Bartosz Polaczyk 80515205cc Revert "Merge pull request #7 from spotify/20211017-fix-indexbuild"
This reverts commit 764219fa7d, reversing
changes made to 0f44da7bea.
2021-10-18 21:50:32 +02:00
Bartosz Polaczyk 999dabed84 Support no object for Swift output map 2021-10-18 11:47:18 +02:00
Bartosz Polaczyk 4fbb063094 Update release package creation snippet 2021-10-18 07:52:30 +02:00
Bartosz Polaczyk 600648d2d5 Place swiftsourceinfo in a Project directory for the thinning plugin 2021-10-18 07:47:03 +02:00
Bartosz Polaczyk 764219fa7d Merge pull request #7 from spotify/20211017-fix-indexbuild
Produce step .d file for indexbuild
2021-10-17 22:42:33 +02:00
Bartosz Polaczyk 86c74e61e2 Place sourceinfo in a subdirectory 2021-10-17 22:34:13 +02:00
Bartosz Polaczyk 0139392ec9 Produce step .d file for indexbuild 2021-10-17 20:03:21 +02:00
Bartosz Polaczyk d25b4e0e06 Document releasing process and add SPM limitations 2021-10-15 14:23:54 +02:00
Vadim Smal 0f44da7bea Merge pull request #5 from CognitiveDisson/fix-cache-miss-for-xcode13
Add ACTION env check for prebuild and postbuild
2021-10-15 08:59:09 +01:00
Vadim Smal 9052558d60 Add action type check for prebuild and postbuild 2021-10-14 16:27:15 +01:00
Bartosz Polaczyk caed3b253c Merge pull request #1 from spotify/add-workflows
Add Github workflows CI
2021-09-06 10:58:46 +02:00
Bartosz Polaczyk 5a313594e5 Add CI badge 2021-09-06 08:21:17 +02:00
Bartosz Polaczyk 4ea6c2b33d Disable SPM sandbox 2021-09-06 08:15:56 +02:00
Bartosz Polaczyk 571c1a72f4 Include add binaries in zip 2021-09-06 07:51:46 +02:00
Bartosz Polaczyk 141b919ba7 Add releasing actions 2021-09-05 21:55:49 +02:00
Bartosz Polaczyk 381294ea63 Disable strict swiftlint check 2021-09-04 19:17:03 +02:00
Bartosz Polaczyk 45cae2b411 Prepare arch support 2021-09-04 19:12:13 +02:00
Bartosz Polaczyk 890777b761 Fix Rake testing invocation 2021-09-04 19:06:42 +02:00
Bartosz Polaczyk f04591ef8b Add Github workflows CI 2021-09-04 18:59:41 +02:00
Bartosz Polaczyk 816cb7ceac Introduce probe repeats 2021-08-26 07:50:02 +02:00
Bartosz Polaczyk 210d04dc27 Update license for cocoapods plugin 2021-08-23 08:29:22 +02:00
Bartosz Polaczyk 5a34cde2de Add Lincenses to Swift files 2021-08-23 08:20:54 +02:00
Bartosz Polaczyk 52fc7ac662 Readme extras 2021-08-23 08:11:01 +02:00
Bartosz Polaczyk be66491f4f Add cocoapods-plugin 2021-08-23 07:44:34 +02:00
Bartosz Polaczyk a3ed25d45a Initial commit 2021-08-22 21:37:18 +02:00
893 changed files with 31825 additions and 882 deletions
+65
View File
@@ -0,0 +1,65 @@
---
name: ⚠️ Bug Report
about: Something isn't working as expected
---
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
-->
**My integration setup**
[ ] CocoaPods cocoapods-xcremotecache plugin
[ ] Automatic integration using `xcprepare integrate ...`
[ ] Manual integration
[ ] Carthage
**Expected/desired behavior**
<!-- Describe what the desired behavior would be. -->
**Minimal reproduction of the problem with instructions**
<!-- Please provide the *STEPS TO REPRODUCE*. -->
**Producer Logs**
<!-- Capture logs from 10 minutes: `log show --predicate 'sender BEGINSWITH "xc"' --style compact --info --debug -last 10m` -->
<details>
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
</details>
**Consumer Logs**
<!-- Capture logs from 10 minutes: `log show --predicate 'sender BEGINSWITH "xc"' --style compact --info --debug -last 10m` -->
<details>
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
</details>
**Pods/Carthage file**
<!-- Delete if you don't use CocoaPods or Carthage -->
<details>
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
</details>
**Environment**
* **XCRemoteCache:** X.Y.Z
* **cocoapods-xcremotecache:** X.Y.Z <!-- check with `gem list cocoapods-xcremotecache` >
* **HTTP cache server:** ... <!-- e.g. demo docker, nginx, AWS etc. >
* **Xcode:** X.Y.Z
**Post build stats**
<!--
To capture build statistics:
* call `xcprepare stats --reset` (or `XCRC/xcprepare stats --reset` for CocoaPods)
* Build a project in Xcode
* `xcprepare stats` (or `XCRC/xcprepare stats` for CocoaPods)
-->
<details>
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
</details>
**Others**
<!-- Anything else relevant? Operating system version, , ... -->
@@ -0,0 +1,16 @@
---
name: 📕 Documentation Issue
about: Suggestion for a change in a documentation
---
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
-->
**A suggestion**
<!-- Describe how could the documentation be improved. -->
**Which file, section, line**
<!-- Provide a section it relates (if exist). -->
+20
View File
@@ -0,0 +1,20 @@
---
name: 🙏 Future Request
about: Suggestion for an improvement, either behaviour or implementation
---
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
-->
**Expected/desired behavior**
<!-- Describe what the desired behavior would be. -->
**Relevant integration setup**
[ ] CocoaPods cocoapods-xcremotecache plugin
[ ] Automatic integration using `xcprepare integrate ...`
[ ] Manual integration
[ ] Carthage
+29
View File
@@ -0,0 +1,29 @@
name: CI
on: [pull_request]
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.1.0
with:
args: --strict
macOS:
runs-on: macos-12
env:
XCODE_VERSION: ${{ '14.2' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
- name: Checkout
uses: actions/checkout@v1
- name: Build and Run
run: rake build[release]
- name: Test
run: rake test
- name: E2ETests
run: rake e2e_only
+24
View File
@@ -0,0 +1,24 @@
name: Docs
on:
push:
branches:
- master
jobs:
docs:
runs-on: macos-12
env:
XCODE_VERSION: ${{ '14.2' }}
steps:
- uses: actions/checkout@v2
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
- name: "Generate documentation"
run: "swift package --allow-writing-to-directory ./docs generate-documentation --target XCRemoteCache --disable-indexing --transform-for-static-hosting --output-path ./docs --hosting-base-path XCRemoteCache/"
- name: Deploy GH-pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
keep_files: false
+72
View File
@@ -0,0 +1,72 @@
name: release_binaries
on:
release:
types: created
jobs:
macOS:
name: Add macOS binaries to release
runs-on: macos-12
env:
XCODE_VERSION: ${{ '14.2' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
- name: Checkout
uses: actions/checkout@v1
- name: Set tag name
run: echo "TAG_NAME=$(echo $GITHUB_REF | cut -c 11-)" >> $GITHUB_ENV
- name: Build x86_64-apple-macosx
run: "rake 'build[release, x86_64-apple-macosx]'"
- name: Save x86_64 executable to be lipo'd later
run: "mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-x86_64"
- name: Clean releases dir to not conflict with other archs
run: "rm -rf releases"
- name: Build arm64-apple-macosx
run: "rake 'build[release, arm64-apple-macosx]'"
- name: Save arm64 executable to be lipo'd later
run: "mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-arm64"
- name: Clean releases dir to not conflict with other files to attach
run: "rm -rf releases"
- name: Zip x86_64-apple-macosx release
run: "mkdir -p releases && zip -jr releases/XCRemoteCache-macOS-x86_64-$TAG_NAME.zip LICENSE README.md tmp/xcremotecache-x86_64"
- name: Zip arm64-apple-macosx release
run: "zip -jr releases/XCRemoteCache-macOS-arm64-$TAG_NAME.zip LICENSE README.md tmp/xcremotecache-arm64"
- name: Lipo macOS executables
run: "mkdir -p tmp/xcremotecache && ls tmp/xcremotecache-x86_64 | xargs -I {} lipo -create -output tmp/xcremotecache/{} tmp/xcremotecache-x86_64/{} tmp/xcremotecache-arm64/{}"
- name: Zip x86_64-arm64-apple-macosx release
run: "zip -jr releases/XCRemoteCache-macOS-arm64-x86_64-$TAG_NAME.zip LICENSE README.md tmp/xcremotecache"
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v1-release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: releases/*
file_glob: true
tag: ${{ github.ref }}
overwrite: true
cocoapods:
name: Publish CocoaPods plugin
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: cocoapods-plugin
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- run: bundle install
- name: Publish to RubyGems
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec
CURRENT_VERSION=$(gem list cocoapods-xcremotecache --remote -q | sed 's/[^0-9\.]//g')
[ -f cocoapods-xcremotecache-$CURRENT_VERSION.gem ] && echo "Version $CURRENT_VERSION already exists" || gem push *.gem
env:
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
+13
View File
@@ -0,0 +1,13 @@
.DS_Store
/.build
/Packages
*.xcodeproj/
*.xcworkspace/
DerivedData
/.swiftpm/
releases
tmp/
.idea/
xcuserdata
*.gem
Pods/
View File
+1
View File
@@ -0,0 +1 @@
5.1
+45
View File
@@ -0,0 +1,45 @@
# Rule configurations
--allman false
--binarygrouping 4,8
--commas always
--comments indent
--decimalgrouping 3,5
--elseposition same-line
--empty void
--exponentcase lowercase
--exponentgrouping disabled
--fractiongrouping enabled
--header strip
--hexgrouping none
--hexliteralcase lowercase
--ifdef noindent
--indent 4
--indentcase false
--xcodeindentation enabled
--linebreaks lf
--octalgrouping 4,8
--patternlet inline
--self remove
--semicolons inline
--stripunusedargs closure-only
--trimwhitespace always
--wraparguments beforefirst
--wrapcollections beforefirst
--closingparen balanced
--xcodeindentation enabled
# Disabled rules
--disable numberFormatting
--disable consecutiveBlankLines
--disable andOperator
--disable spaceAroundOperators
--disable redundantReturn
--disable blankLinesAtStartOfScope
# Enabled rules
# Tool options
--symlinks ignore
# Excluded directories
--exclude Carthage,.build,DerivedData
+149
View File
@@ -0,0 +1,149 @@
disabled_rules:
- identifier_name # Does not make sense to lint for the length of identifiers.
- type_name # Same as above.
- empty_enum_arguments # It warns about an explicit pattern we use.
- superfluous_disable_command # Disabled since we disable some rules pre-emptively to avoid issues in the future
- todo # Temporarily disabled. We have too many right now hiding real issues :(
- nesting # Does not make sense anymore since Swift 4 uses nested `CodingKeys` enums for example
opt_in_rules:
- anyobject_protocol
- attributes
- closure_end_indentation
- closure_spacing
- collection_alignment
- colon
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- discouraged_object_literal
- empty_collection_literal
- empty_count
- empty_string
- explicit_init
- extension_access_modifier
- fatal_error_message
- file_header
- first_where
- identical_operands
- implicit_return
- inert_defer
- joined_default_parameter
- literal_expression_end_indentation
- legacy_hashing
- legacy_random
- multiline_arguments
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- notification_center_detachment
- number_separator
- operator_usage_whitespace
- overridden_super_call
- private_action
- prohibited_interface_builder
- prohibited_super_call
- redundant_nil_coalescing
- redundant_objc_attribute
- single_test_class
- sorted_imports
- static_operator
- toggle_bool
- trailing_comma
- trailing_whitespace
- unneeded_parentheses_in_closure_argument
- vertical_parameter_alignment_on_call
- yoda_condition
excluded:
- .github/
- .build/
- build/
- Carthage/
- docs/
- fastlane/
- DerivedData/
- e2eTests/XCRemoteCacheSample/Pods
- e2eTests/StandaloneSampleApp
attributes:
always_on_same_line:
- "@IBAction"
- "@NSManaged"
- "@objc"
closure_spacing: warning
empty_count:
severity: warning
implicit_return:
included:
- closure
explicit_init: warning
fatal_error_message: warning
file_header:
severity: warning
forbidden_pattern: |
\/\/
\/\/ .*?\..*
\/\/ .*
\/\/
\/\/ Created by .*? on .*\.
\/\/ Copyright © \d{4} .*\. All rights reserved\.
\/\/
required_pattern: |
\/\/ Copyright \(c\) \d{4} Spotify AB\.
\/\/
\/\/ Licensed to the Apache Software Foundation \(ASF\) under one
\/\/ or more contributor license agreements\. See the NOTICE file
\/\/ distributed with this work for additional information
\/\/ regarding copyright ownership\. The ASF licenses this file
\/\/ to you under the Apache License, Version 2.0 \(the
\/\/ "License"\); you may not use this file except in compliance
\/\/ with the License\. You may obtain a copy of the License at
\/\/
\/\/ http:\/\/www.apache.org\/licenses\/LICENSE-2\.0
\/\/
\/\/ Unless required by applicable law or agreed to in writing,
\/\/ software distributed under the License is distributed on an
\/\/ \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
\/\/ KIND, either express or implied\. See the License for the
\/\/ specific language governing permissions and limitations
\/\/ under the License\.
force_cast: warning
force_try: warning
implicit_getter: warning
indentation: 4 # 4 spaces
line_length:
warning: 120
error: 200
ignores_function_declarations: true
multiline_arguments:
first_argument_location: next_line
number_separator:
minimum_length: 5 # number of digits, i.e. >= 10_000
redundant_nil_coalescing: warning
shorthand_operator: warning
trailing_comma:
mandatory_comma: true
vertical_whitespace:
max_empty_lines: 2
weak_delegate: warning
cyclomatic_complexity:
warning: 12
function_parameter_count:
warning: 7
reporter:
- "xcode"
- "junit"
custom_rules:
associated_values_unwrapping:
name: "Associated Value Unwrapping"
regex: "case let [a-zA-Z0-9]*.[a-zA-Z0-9]+\\([a-zA-Z0-9 ,]+"
message: "Each associated value should be defined as a separate constant (i.e: .enumCase(let val1, let val2))"
severity: warning
trailing_dot_in_comments:
name: "Trailing dot in comments"
regex: '^(?!\/\/\ Copyright\ \(c\)\ \d{4}\ Spotify AB\.|\/\/\ under\ the\ License\.)[ ]*///?[^\n]*\.\n'
message: "There shouldn't be trailing dot in comments"
severity: warning
+3
View File
@@ -0,0 +1,3 @@
This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
[code-of-conduct]: https://github.com/spotify/code-of-conduct/blob/main/code-of-conduct.md
+13
View File
@@ -0,0 +1,13 @@
# Copyright 2021 Spotify AB
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+2
View File
@@ -0,0 +1,2 @@
XCRemoteCache
Copyright 2021 Spotify AB
+79
View File
@@ -0,0 +1,79 @@
{
"object": {
"pins": [
{
"package": "AEXML",
"repositoryURL": "https://github.com/tadija/AEXML.git",
"state": {
"branch": null,
"revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
"version": "4.6.1"
}
},
{
"package": "PathKit",
"repositoryURL": "https://github.com/kylef/PathKit.git",
"state": {
"branch": null,
"revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511",
"version": "1.0.0"
}
},
{
"package": "Spectre",
"repositoryURL": "https://github.com/kylef/Spectre.git",
"state": {
"branch": null,
"revision": "f79d4ecbf8bc4e1579fbd86c3e1d652fb6876c53",
"version": "0.9.2"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "9f04d1ff1afbccd02279338a2c91e5f27c45e93a",
"version": "0.0.5"
}
},
{
"package": "SwiftDocCPlugin",
"repositoryURL": "https://github.com/apple/swift-docc-plugin",
"state": {
"branch": null,
"revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
"version": "1.0.0"
}
},
{
"package": "XcodeProj",
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
"state": {
"branch": null,
"revision": "fae27b48bc14ff3fd9b02902e48c4665ce5a0793",
"version": "8.9.0"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "00c403debcd0a007b854bb35e598466207a2d58c",
"version": "5.0.0"
}
},
{
"package": "Zip",
"repositoryURL": "https://github.com/marmelroy/Zip.git",
"state": {
"branch": null,
"revision": "67fa55813b9e7b3b9acee9c0ae501def28746d76",
"version": "2.1.2"
}
}
]
},
"version": 1
}
+90
View File
@@ -0,0 +1,90 @@
// swift-tools-version:5.3
// swiftlint:disable:previous file_header
// The swift-tools-version declares the minimum version of Swift required to build this package
import PackageDescription
let package = Package(
name: "XCRemoteCache",
platforms: [
.macOS(.v10_14),
],
products: [
.executable(name: "xcprebuild", targets: ["xcprebuild"]),
],
dependencies: [
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"),
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.9.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(
name: "XCRemoteCache",
dependencies: ["Zip", "Yams", "XcodeProj"]
),
.target(
name: "xcprebuild",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcswiftc",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtoolSupport",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtool",
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
),
.target(
name: "xclipo",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcpostbuild",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcprepare",
dependencies: [
"XCRemoteCache",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.target(
name: "xcld",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcldplusplus",
dependencies: ["XCRemoteCache"]
),
.target(
// Wrapper target that builds all binaries but does nothing in runtime
name: "Aggregator",
dependencies: [
"xcprebuild",
"xcswiftc",
"xclibtool",
"xcpostbuild",
"xcprepare",
"xcld",
"xcldplusplus",
"xclipo",
]
),
.testTarget(
name: "XCRemoteCacheTests",
dependencies: ["XCRemoteCache"],
resources: [.copy("TestData")]
),
.testTarget(
name: "xclibtoolSupportTests",
dependencies: ["xclibtoolSupport"]
),
]
)
Executable
+528
View File
@@ -0,0 +1,528 @@
<p align="center">
<img src="docs/img/logo.png#gh-light-mode-only" width="75%">
<img src="docs/img/logo-dark.png#gh-dark-mode-only" width="75%">
</p>
_XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server._
[![Build Status](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![Slack](https://slackin.spotify.com/badge.svg)](https://slackin.spotify.com)
[![Docs](https://github.com/spotify/XCRemoteCache/workflows/Docs/badge.svg)](https://spotify.github.io/XCRemoteCache/documentation/xcremotecache/)
- [How and Why?](#how-and-why)
* [Accurate target input files](#accurate-target-input-files)
+ [New file added to the target](#new-file-added-to-the-target)
* [Debug symbols](#debug-symbols)
* [Performance optimizations](#performance-optimizations)
* [Focused targets](#focused-targets)
- [How to integrate XCRemoteCache with your Xcode project?](#how-to-integrate-xcremotecache-with-your-xcode-project)
* [1. Download XCRemoteCache](#1-download-xcremotecache)
* [A. Automatic integration](#a-automatic-integration)
+ [2. Create a minimal XCRemoteCache configuration](#2-create-a-minimal-xcremotecache-configuration)
+ [3. Run automatic integration script](#3-run-automatic-integration-script)
- [3a. Producer side](#3a-producer-side)
- [3b. Consumer side](#3b-consumer-side)
* [A full list of `xcprepare integrate` supported options](#a-full-list-of-xcprepare-integrate-supported-options)
* [B. Manual integration](#b-manual-integration)
+ [2. Configure XCRemoteCache](#2-configure-xcremotecache)
+ [3. Call xcprepare](#3-call-xcprepare)
+ [4. Integrate with the Xcode project](#4-integrate-with-the-xcode-project)
+ [5. Configure LLDB source-map (Optional)](#5-configure-lldb-source-map-optional)
+ [6. Producer mode - Artifacts generation](#6-producer-mode---artifacts-generation)
- [6a. Configure producer mode](#6a-configure-producer-mode)
- [6b. Fill the cache](#6b-fill-the-cache)
- [6c. Mark commit sha](#6c-mark-commit-sha)
- [A full list of configuration parameters:](#a-full-list-of-configuration-parameters)
- [Backend cache server](#backend-cache-server)
* [Sample REST cache server from a docker image](#sample-rest-cache-server-from-a-docker-image)
* [Amazon S3 and Google Cloud Storage](#amazon-s3-and-google-cloud-storage)
- [CocoaPods plugin](#cocoapods-plugin)
- [Requirements](#requirements)
- [Apple silicon support](#apple-silicon-support)
* [Artifacts per architecture (Recommended)](#artifacts-per-architecture-recommended)
* [Fat artifacts](#fat-artifacts)
- [Limitations](#limitations)
- [FAQ](#faq)
- [Development](#development)
- [Release](#release)
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
* [Building release package](#building-release-package)
- [Contributing](#contributing)
- [Code of conduct](#code-of-conduct)
- [License](#license)
- [Security Issues?](#security-issues)
## How and Why?
The caching mechanism is based on remote artifacts that should be generated and uploaded to the cache server for each commit on a `master` branch, preferably as a part of CI/CD step. Xcode products are not portable between different Xcode versions, each XCRemoteCache artifact is linked with a specific Xcode build number that generated it. To support multiple Xcode versions, artifacts generation should happen for each Xcode version.
The artifact reuse flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
> Multiple commits that have the same target sources reuse artifact package on a remote server.
### Accurate target input files
Finding a precise list of input and dependency files is a non-trivial task as Xcode heavily relies on implicit target dependencies. It means that Xcode is trying to use required dependencies from provided search paths and looks up `DerivedData`'s product dir. To find a narrow list of files to compute fingerprint hash, XCRemoteCache fetches a `meta.json` file from a server which contains remotelly generated fingerprint hash and a list of input files that should constitute a fingerprint. That list of input files was produced during the artifact generation process, based on `.d` output from `clang` and `swift` compilers.
Before building a project in Xcode, XCRemoteCache needs to find the best git commit sha for which artifacts will be used. This happens as a part of the `xcprepare` execution, which should be called after each merge or switching a branch. `xcprepare` finds a list of 10 most recent common sha with the remote repo branch using git's first-parent strategy and selects the newest one for which all artifacts have been uploaded.
_The generation side is responsible to call `xcprepare mark` subcommand after each successful build. Marking process creates an empty marker file on a remote cache server with a given format: `#{commmmitSha}-#{TargetName}-#{Configuration}-#{Platform}-#{XcodeBuildNumber}-#{ContextBuildSettings}-#{SchemaID}.json`._
`xcprepare` makes `HEAD` requests for all identified shas, picks the newest one for which a marker file exists remotely, and saves it in the text form to the `arc.rc` file. That file informs the prebuild phase which meta file should be fetched to get a list of target dependency files.
#### New file added to the target
Considering in the hash fingerprint only a list of previously observed files can give invalid results if a build contains a new source file as it isn't considered in the hash.
For a new `.swift` file in a swift-only target, `xcswiftc` automatically recognizes that case and forces local compilation of the entire target. For Objective-C or mixed targets, fallbacking to the local compilation is more difficult as some previous invocations (either `xccc` or `xcswiftc`) could already be finished with no-operation. To mitigate that, each wrapper appends invocation call to a side file (`history.compile`) just in case some other process would need to compile the entire target locally. If that happens, compilation of the newly added file acquires a target-wide lock that stops other wrapper invocations, executes already mocked steps one by one to backfill already skipped compilation steps.
### Debug symbols
Binaries built with "debug symbols: enabled" embed source file absolute paths so compilation products cannot be directly ported between two machines with different source roots. Otherwise, LLDB debugger is not able to correlate a set of currently executing machine instructions with a local file that produced it. To mitigate that, XCRemoteCache recommends adding a custom C and Swift debug flags `prefix-map` for all XCRemoteCache builds. These flags ensure that all binaries, generated locally and downloaded from a remote server, have the same debug symbols absolute paths which are translated to an actual local path at the beginning of the LLDB session.
### Performance optimizations
XCRemoteCache involves several optimization techniques:
* Local HTTP cache stores all responses from the remote server at `~/Library/Caches/XCRemoteCache/`
* Prebuild and postbuild steps leverage Xcode's discovered dependency file to avoid recomputing fingerprint hashes if none of the input files has changed
* A wrapper for the `clang` compilation is a C program, generated and compiled during the `xcprepare` step. It is called many times to compile each `*.(m|c)` file and accessing a disk to read a configuration would introduce a significant slowdown, especially if a project contains a lot of Objective-C files. As a remedy, `xcprepare` reads the XCRemoteCache configuration only once and embeds all configurable fields directly into the `xccc` binary
* `arc.rc`, generated by `xcprepare`, gets file modification equal to the commit date if refers. `arc.rc` is included in the discovered dependency file, touching it in the `xcprepare` would automatically invalidate previous XCRemoteCache prebuild step and force redundant fingerprint checks. By syncing the `mdate` with a git commit, Xcode avoids prebuild steps unless the remote cache commit has changed
* If a target cache miss happens, XCRemoteCache disables cache for that target until a commit sha in `arc.rc` changes. That bypasses a fingerprint computation for incremental builds
### Focused targets
If a list of targets that can have dirty sources is limited, XCRemoteCache can be configured with focused targets, specified in `.rcinfo`.
By default, all targets are focused and these compare local fingerprint with one available remotely and fallbacks to the local compilation if it doesn't match. Non-focused targets, called 'thin' targets, always use cached artifacts what eliminates a fingerprint computation. Thin targets should contain only a single compilation file with `thin_target_mock_filename`, e.g. `standin.swift` or `standin.m`.
## How to integrate XCRemoteCache with your Xcode project?
To enable XCRemoteCache in the existing `.xcodeproj` you need to add extra build settings and build phases to targets that you want to cache.
You can do that in an automatic way, using the XCRemoteCache-provided integration command, or manually modify your Xcode project.
### 1. Download XCRemoteCache
From the Github [Releases page](https://github.com/spotify/XCRemoteCache/releases), download the XCRemoteCache bundle zip. Unzip the bundle to a directory next to your `.xcodeproj`.
_The following steps will assume the bundle has been unzipped to `xcremotecache` dir, placed next to the `.xcodeproj`._
### A. Automatic integration
#### 2. Create a minimal XCRemoteCache configuration
Create `.rcinfo` yaml file next to the `.xcodeproj` with a minimum set of configuration entries, like:
```yaml
primary_repo: https://yourRepo.git
cache_addresses:
- https://xcremotecacheserver.com
```
#### 3. Run automatic integration script
##### 3a. Producer side
Execute a command that modifies `<yourProject.xcodeproj>`:
```bash
xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode producer --final-producer-target <YourMainTarget>
```
##### 3b. Consumer side
Execute a command that modifies `<yourProject.xcodeproj>`:
```bash
xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consumer
```
###### A full list of `xcprepare integrate` supported options
| Argument | Description | Default | Required |
| ------------- | ------------- | ------------- | ------------- |
| `--input` | .xcodeproj location | N/A | ✅ |
| `--mode` | mode. Supported values: `consumer`, `producer`, `producer-fast`(experimental) | N/A | ✅ |
| `--targets-include` | comma-separated list of targets to integrate XCRemoteCache. | `""` | ⬜️ |
| `--targets-exclude` | comma-separated list of targets to not integrate XCRemoteCache. Takes priority over --targets-include. | `""` | ⬜️ |
| `--configurations-include` | comma-separated list of configurations to integrate XCRemoteCache. | `""` | ⬜️ |
| `--configurations-exclude` | comma-separated list of configurations to not integrate XCRemoteCache. Takes priority over --configurations-include. | `Release` | ⬜️ |
| `--final-producer-target` | [Producer only] The final target that generates cache artifacts. Once this targets is finished, no other targets are allowed to upload artifacts to the remote server for a given sha, configuration and platform context. | `nil` | ⬜️ |
| `--consumer-eligible-configurations` | [Consumer only] comma-separated list of configurations that need to have all artifacts uploaded to the remote site before using given sha. | `Debug` | ⬜️ |
| `--consumer-eligible-platforms` | [Consumer only] comma-separated list of platforms that need to have all artifacts uploaded to the remote site before using given sha | `iphonesimulator` | ⬜️ |
| `--lldb-init` | LLDBInit mode. Appends to .lldbinit a command required for debugging. Supported values: 'none' (do not append to .lldbinit), 'user' (append to ~/.lldbinit) | `user` | ⬜️ |
| `--fake-src-root` | An arbitrary source location shared between producers and consumers. Should be unique for a project. | `/xxxxxxxxxx` | ⬜️ |
| `--output` | Save the project with integrated XCRemoteCache to a separate location. | N/A | ⬜️ |
| `--sdks-exclude` | comma separated list of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*"). (Experimental) | `""` | ⬜️ |
</details>
### B. Manual integration
#### 2. Configure XCRemoteCache
Create yaml configuration file `.rcinfo`, next to the `.xcodeproj`, with your full XCRemoteCache configuration, according to [the parameters list](#a-full-list-of-configuration-parameters) e.g.:
```yaml
primary_repo: https://yourRepo.git
cache_addresses:
- https://xcremotecacheserver.com
repo_root: "."
remote_commit_file: arc.rc
xccc_file: xcremotecache/xccc
```
#### 3. Call xcprepare
Execute `xcprepare --configuration #Configuration# --platform #platform#` command after each merge or rebase with the primary branch. Otherwise, the remote cache artifacts may be outdated and final hit rate may be poor.
The `xcprepare` application saves `arc.rc` file on a disk and prints a summary to the standard output. The printed `recommended_remote_address` is just a recommendation which cache remote server use. It is up to the integration tooling to decide if it makes sense. If so, the project's `.rcinfo` should define that value as `recommended_remote_address` parameter.
Example:
```shell
$ xcremotecache/xcprepare --configuration Debug --platform iphonesimulator
result: true
commit: aabbccc00
age: 0
recommended_remote_address: https://xcremotecacheserver.com
```
#### 4. Integrate with the Xcode project
Configure Xcode targets that **should use** XCRemoteCache:
1. Override Build Settings:
* `CC` - `xccc_file` from your `.rcinfo` configuration (e.g. `xcremotecache/xccc`)
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
* `LIPO` - location of `xclipo` (e.g. `xcremotecache/xclipo`)
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
* `LDPLUSPLUS` - location of `xcldplusplus` (e.g. `xcremotecache/xcldplusplus`)
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
* `SWIFT_USE_INTEGRATED_DRIVER` - `NO` (required in Xcode 14.0+)
<details>
<summary>Screenshot</summary>
![build-settings](docs/img/build-settings.png)
</details>
2. Add a `Prebuild` build phase (before compilation):
* command: `"$SCRIPT_INPUT_FILE_0"`
* input files: location of `xcprebuild` (e.g. `xcremotecache/xcprebuild`)
* output files:
* `$(TARGET_TEMP_DIR)/rc.enabled`
* `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)`
* discovery dependency file: `$(TARGET_TEMP_DIR)/prebuild.d`
3. Add `Postbuild` build phase (after compilation):
* command: `"$SCRIPT_INPUT_FILE_0"`
* input files: location of `xcpostbuild` command (e.g. `xcremotecache/xcpostbuild`)
* output files:
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5`
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5`
* discovery dependency file: `$(TARGET_TEMP_DIR)/postbuild.d`
<details>
<summary>Screenshot</summary>
![build-phases](docs/img/build-phases.png)
</details>
#### 5. Configure LLDB source-map (Optional)
Rewriting source-map is required to support debugging and hit breakpoints, see [Debug symbols](#debug-symbols).
1. Ooverride the following Build Settings for **all targets**:
* `XCRC_SRCROOT` - `/xxxxxxxxxx` (or any other arbitrary string for your project)
* add `-debug-prefix-map $(SRCROOT)=$(XCRC_SRCROOT)` to `OTHER_SWIFT_FLAGS`. _If it doesn't exist, define it as `$(inherited) -debug-prefix-map $(SRCROOT)=$(XCRC_SRCROOT)`_
* add `-fdebug-prefix-map=$(SRCROOT)=$(XCRC_SRCROOT)` to `OTHER_CFLAGS`. _If it doesn't exists, define it as `$(inherited) -fdebug-prefix-map=$(SRCROOT)=$(XCRC_SRCROOT)`_
2. Add `settings set target.source-map /xxxxxxxxxx /Users/account/src/PathToTheProject` to `~/.lldbinit` on end machine that builds a project with XCRemoteCache
> `XCRC_SRCROOT` arbitrary path should be project-exclusive to avoid clashing.
_Tip: In some rare cases, Xcode caches `~/.lldbinit` content so make sure to restart Xcode after the modification._
#### 6. Producer mode - Artifacts generation
XCRemoteCache can operate in two main modes: `consumer` (default) tries to reuse artifacts available on the remote server and `producer` is used to generate all artifacts - it builds all targets locally and uploads meta and artifact files to the remote cache server.
##### 6a. Configure producer mode
To enable the `producer` mode, configure it directly in the `.rcinfo` file.
> Optionally, you can define `extra_configuration_file` in a `.rcinfo` with a path to the other yaml file that will override the default configuration in `.rcinfo`. That approach can be useful if you want to track main `.rcinfo` and keep your local configuration out of git.
##### 6b. Fill the cache
Build the project from Xcode or using `xcodebuild`
##### 6c. Mark commit sha
Once all artifacts have been uploaded, "mark a build" using `xcprepare mark` command:
```shell
$ xcremotecache/xcprepare mark --configuration Debug --platform iphonesimulator
```
That command creates an empty file on a remote server which informs that for given sha, configuration, platform, Xcode versions etc. all artifacts are available.
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool`, `xclipo` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
##### 7. Generalize `-Swift.h` (Optional only if using static library with a bridging header with public `NS_ENUM` exposed from ObjC)
If a static library target contains a mixed target with a bridging header exposing an enum from ObjC in a public Swift API, your custom script that moves `*-Swift.h` to the shared location, it should also move `*-Swift.h.md5` next to it.
Example:
##### Existing script (Before):
```shell
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
```
where
* `SCRIPT_INPUT_FILE_0="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
* `SCRIPT_OUTPUT_FILE_0="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
##### Correct script (After):
```shell
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
[ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm "${SCRIPT_OUTPUT_FILE_1}"
```
where
* `SCRIPT_INPUT_FILE_0="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
* `SCRIPT_INPUT_FILE_1="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME).md5"`
* `SCRIPT_OUTPUT_FILE_0="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
* `SCRIPT_OUTPUT_FILE_1="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME).md5"`
Note: This step is not required if at least one of these is true:
* you build a framework (not a static library)
* you don't expose `NS_ENUM` type from ObjC to Swift via a bridging header
## A full list of configuration parameters:
| Property | Description | Default | Required |
| ------------- | ------------- | ------------- | ------------- |
| `mode` | build mode. Possible values: `consumer`, `producer` | `consumer` | ⬜️ |
| `cache_addresses` | Addresses of all remote cache replicas. _Required to be a non-empty array of strings_ | N/A | ✅ |
| `recommended_cache_address` | Address of the best remote cache to use in the consumer mode. If not specified, the first item in `cache_addresses` will be used | N/A | ⬜️ |
| `cache_health_path` | Probe request path to the `cache_addresses` (relative to a path in `cache_addresses`) that determines the best cache to use | `nginx-health` | ⬜️ |
| `cache_health_path_probe_count` | Number of `cacheAddresses` probe requests | `3` | ⬜️ |
| `remote_commit_file` | Filepath to the file with the remote commit sha | `build/remote-cache/arc.rc` | ⬜️ |
| `xccc_file` | Path to the xccc wrapper | `build/bin/xccc` | ⬜️ |
| `prebuild_discovery_path` | Path, relative to `$TARGET_TEMP_DIR`, that specifies prebuild discovery .d file | `prebuild.d` | ⬜️ |
| `postbuild_discovery_path` | Path, relative to `$TARGET_TEMP_DIR`, that specifies postbuild discovery .d file | `postbuild.d` | ⬜️ |
| `mode_marker_path` | Path, relative to `$TARGET_TEMP_DIR`, of a maker file to enable or disable the remote cache for a given target. Includes a list of all allowed input files to use remote cache | `rc.enabled` | ⬜️ |
| `clang_command` | Command for a standard C compilation fallback | `clang` | ⬜️ |
| `swiftc_command` | Command for a standard Swift compilation fallback | `swiftc` | ⬜️ |
| `primary_repo` | Address of the primary git repository that produces cache artifacts (case-sensitive) | N/A | ✅ |
| `primary_branch` | The main (primary) branch on the `primary_repo` that produces cache artifacts | `master` | ⬜️ |
| `repo_root` | The path to the git repo root | `"."` | ⬜️ |
| `cache_commit_history` | Number of historical git commits to look for cache artifacts | `10` | ⬜️ |
| `source_root` | Source root of the Xcode project | `""` | ⬜️ |
| `fingerprint_override_extension` | Fingerprint override extension (sample override `Module.swiftmodule/x86_64.swiftmodule.md5`) | `md5` | ⬜️ |
| `extra_configuration_file` | Configuration file that overrides project configuration (this property can be overriden multiple times in different files to chain extra configuration files) | `user.rcinfo` | ⬜️ |
| `publishing_sha` | Custom commit sha to publish artifact (producer only) | `nil` | ⬜️ |
| `artifact_maximum_age` | Maximum age in days HTTP response should be locally cached before being evicted | `30` | ⬜️ |
| `custom_fingerprint_envs` | Extra ENV keys that should be convoluted into the environment fingerprint | `[]` | ⬜️ |
| `stats_dir` | Directory where all XCRemoteCache statistics (e.g. counters) are stored | `~/.xccache` | ⬜️ |
| `download_retries` | Number of retries for download requests | `0` | ⬜️ |
| `upload_retries` | Number of retries for upload requests | `3` | ⬜️ |
| `retry_delay` | Delay between retries in seconds | `10` | ⬜️ |
| `upload_batch_size` | Maximum number of simultaneous requests. 0 means no limits | `0` | ⬜️ |
| `request_custom_headers` | Dictionary of extra HTTP headers for all remote server requests | `[]` | ⬜️ |
| `thin_target_mock_filename` | Filename (without an extension) of the compilation input file that is used as a fake compilation for the forced-cached target (aka thin target) | `standin` | ⬜️ |
| `focused_targets` | A list of all targets that are not thinned. If empty, all targets are meant to be non-thin | `[]` | ⬜️ |
| `disable_http_cache ` | Disable cache for http requests to fetch metadata and download artifacts | `false` | ⬜️ |
| `compilation_history_file ` | Path, relative to $TARGET_TEMP_DIR which gathers all compilation commands that should be executed if a target switches to local compilation. Example: A new `.swift` file invalidates remote artifact and triggers local compilation. When that happens, all previously skipped clang build steps need to be eventually called locally - this file lists all these commands. | `history.compile` | ⬜️ |
| `timeout_response_data_chunks_interval ` | Timeout for remote response data interval (in seconds). If an interval between data chunks is longer than a timeout, a request fails. | `20` | ⬜️ |
| `turn_off_remote_cache_on_first_timeout ` | If true, any observed request timeout switches off remote cache for all targets | `false` | ⬜️ |
| `product_files_extensions_with_content_override ` | List of all extensions that should carry over source fingerprints. Extensions of all product files that contain non-deterministic content (absolute paths, timestamp, etc) should be included. | `["swiftmodule"]` | ⬜️ |
| `thinning_enabled ` | If true, support for thin projects is enabled | `false` | ⬜️ |
| `thinning_target_module_name ` | Module name of a target that works as a helper for thinned targets | `"ThinningRemoteCacheModule"` | ⬜️ |
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
| `aws_secret_key` | Secret key for AWS V4 Signature Authorization. If this is set to a non-empty String - an Authentication Header will be added based on this and the other `aws_*` parameters.| `""` | ⬜️ |
| `aws_access_key` | Access key for AWS V4 Signature Authorization. | `""` | ⬜️ |
| `aws_security_token` | Temporary security token provided by the AWS Security Token Service. | `nil` | ⬜️ |
| `aws_region` | Region for AWS V4 Signature Authorization. E.g. `eu`. | `""` | ⬜️ |
| `aws_service` | Service for AWS V4 Signature Authorization. E.g. `storage`. | `""` | ⬜️ |
| `out_of_band_mappings` | A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled dependencies. Keys represent generic replacement and values are substrings that should be replaced. Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]` `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`). Warning: remapping order is not-deterministic so avoid remappings with multiple matchings. | `[:]` | ⬜️ |
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
| `disable_vfs_overlay` | A feature flag to disable virtual file system overlay support (temporary) | `false` | ⬜️ |
| `custom_rewrite_envs` | A list of extra ENVs that should be used as placeholders in the dependency list. ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process. | `[]` | ⬜️ |
| `irrelevant_dependencies_paths` | Regexes of files that should not be included in a list of dependencies. Warning! Add entries here with caution - excluding dependencies that are relevant might lead to a target overcaching. The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude all `.modulemap` files. | `[]` | ⬜️ |
| `gracefully_handle_missing_common_sha` | If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch. That might be useful on CI, where a shallow clone is used and cloning depth is not big enough to fetch a commit from a primary branch | `false` | ⬜️ |
## Backend cache server
As a cache server, XCRemoteCache may use any REST server that supports PUT, GET and HEAD methods.
For the development phase, you can try the simplest cache server available as a docker image in [backend-example](backend-example). For the production environment, it is recommended to configure a reliable, fast server, preferrably located in a close proximity to developer's machines.
Out-of-the-box, XCRemoteCache supports V4 Signature Authorization used by Amazon's S3 and Google's GCS. Altenatively, if your server has a customized authentication procedure, you can add extra HTTP request headers with `request_custom_headers` configuration property.
### Sample REST cache server from a docker image
To run a local instance of a server, use a snippet which exposes a cache endpoint under `http://localhost:8080/cache`:
```bash
docker build -t xcremotecache-demo-server backend-example
docker run -it --rm -d -p 8080:8080 --name xcremotecache xcremotecache-demo-server
```
As the docker image saves all files in a container non-persistent storage, to reset cache's content, just restart it:
```bash
# stop the container
docker kill xcremotecache
# run a new instance of the image
docker run -it --rm -d -p 8080:8080 --name xcremotecache xcremotecache-demo-server
```
To review all files stored in the cache server, navigate to the container's cache root directory:
```bash
docker exec -w /tmp/cache -it xcremotecache /bin/bash
```
### Amazon S3 and Google Cloud Storage
XCRemoteCache supports Amazon S3 and Google Cloud Storage buckets to be used as cache servers using the Amazon v4 Signature Authorization.
To set it up use the configuration parameters `aws_secret_key`, `aws_access_key`, `aws_region`, and `aws_service` in the `.rcinfo` file. Specify the URL to the bucket in cache-addresses field in the same file.
XCRemoteCache also supports [AWS Temporary Access Keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#temporary-access-keys). Use additional `aws_security_token` parameter combined with `aws_secret_key`, `aws_access_key` to set it up. [This page](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) describes how to receive a security token.
Example
```yaml
...
cache_addresses:
- https://bucketname.s3.eu-central-1.amazonaws.com/
aws_secret_key: <SECRET_KEY>
aws_access_key: <ACCESS_KEY>
aws_region: eu-central-1
aws_service: s3
...
```
Retention Policy: Buckets usually have a retention policy option which ensures objects are retained for a certain amount of time and won't be modified or deleted. Keep this option short or disable it to avoid errors in case multiple builds are done consecutively on the producer side for the same configuration.
## CocoaPods plugin
Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how to integrate XCRemoteCache in your CocoaPods project.
## Apple silicon support
### Artifacts per architecture (Recommended)
_If all of your machines (both producer and all consumers have the same architecture, either Intel or Apple Silicon), you don't have to do anything._
XCRemoteCache supports building artifacts for Apple silicon consumers. Is it recommended to build separately for `x86_64` and `arm64` architectures to have single-architecture artifacts that do not require downloading irrelevant binaries. Here are required steps if you want to support both Intel and Apple silicon consumers.
* Building for a simulator on a producer: run a first build for `x86_64`, clean a build and build again for `arm64`, e.g.:
```
xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO build ...
xcodebuild clean
xcodebuild ARCHS=arm64 ONLY_ACTIVE_ARCH=NO build ...
```
### Fat artifacts
If you prefer to generate far artifacts (with both Intel and Apple silicon binaries), you can disable "Build Archive Architecture Only" on a producer side, e.g.
```
xcodebuild ONLY_ACTIVE_ARCH=NO build ...
```
Note: This setup is not recommended and may not be supported in future XCRemoteCache releases.
## Requirements
* The repo under `git` version control
* Xcode 11.4+
* Xcode New Build System
* Current Xcode location set by `xcode-select`
* Using the default Xcode Toolchain
* Recommended: multi-targets Xcode project
* Recommended: do not use fast-forward PR strategy (use merge or squash instead)
* Recommended: avoid `DWARF with dSYM File` "Debug Information Format" build setting. Use `DWARF` instead
* Recommended: avoid having a symbolic link in the source root (e.g. placing a project in `/tmp`)
## Limitations
* Swift Package Manager (SPM) dependencies are not supported. _Because SPM does not allow customizing Build Settings, XCRemoteCache cannot specify `clang` and `swiftc` wrappers that control if the local compilation should be skipped (cache hit) or not (cache miss)_
* Filenames with `_vers.c` suffix are reserved and cannot be used as a source file
* All compilation files should be referenced via the git repo root. Referencing `/AbsolutePath/someOther.swift` or `../../someOther.swift` that resolve to the location outside of the git repo root is prohibited.
* The new Swift driver (introduced by default in Xcode 14.0) is not supported and has to be disabled when using XCRemoteCache
## FAQ
Follow the [FAQ](docs/FAQ.md) page.
## Development
Follow the [Development](docs/Development.md) guide. It has all the information on how to get started.
## Release
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
Packages with binaries will be automatically uploaded to the GitHub [Releases](https://github.com/spotify/XCRemoteCache/releases) page.
### Releasing CocoaPods plugin
Bump a gem version defined in [gem_version.rb](cocoapods-plugin/lib/cocoapods-xcremotecache/gem_version.rb) and create a new release described above.
A plugin is automatically uploaded to [RubyGems](https://rubygems.org/gems/cocoapods-xcremotecache) if a given version doesn't exist yet.
### Building release package
To build a release zip package for a single platform (e.g. `x86_64-apple-macosx`, `arm64-apple-macosx`), call:
```shell
rake 'build[release, x86_64-apple-macosx]'
```
The zip package will be generated at `releases/XCRemoteCache.zip`.
## Support
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
## Contributing
We feel that a welcoming community is important and we ask that you follow Spotify's
[Open Source Code of Conduct](https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md)
in all interactions with the community.
## Code of conduct
This project adheres to the [Open Code of Conduct](https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md). By participating, you are expected to honor this code.
## License
```
Copyright 2021 Spotify AB
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
## Security Issues?
Please report sensitive security issues via Spotify's bug-bounty program (https://hackerone.com/spotify) rather than GitHub.
+146
View File
@@ -0,0 +1,146 @@
# encoding: utf-8
require_relative 'tasks/e2e'
################################
# Rake configuration
################################
# Paths
DERIVED_DATA_DIR = File.join('.build').freeze
RELEASES_ROOT_DIR = File.join('releases').freeze
EXECUTABLE_NAME = 'XCRemoteCache'
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld', 'xcldplusplus', 'xclipo']
PROJECT_NAME = 'XCRemoteCache'
SWIFTLINT_ENABLED = true
SWIFTFORMAT_ENABLED = true
################################
# Tasks
################################
task :prepare do
Dir.mkdir(DERIVED_DATA_DIR) unless File.exists?(DERIVED_DATA_DIR)
end
desc 'lint'
task :lint => [:prepare] do
puts 'Run linting'
system("swiftformat --lint --config .swiftformat --cache ignore .") or abort "swiftformat failure" if SWIFTFORMAT_ENABLED
system("swiftlint lint --config .swiftlint.yml --strict") or abort "swiftlint failure" if SWIFTLINT_ENABLED
end
task :autocorrect => [:prepare] do
puts 'Run autocorrect'
system("swiftformat --config .swiftformat --cache ignore .") or abort "swiftformat failure" if SWIFTFORMAT_ENABLED
system("swiftlint autocorrect --config .swiftlint.yml") or abort "swiftlint failure" if SWIFTLINT_ENABLED
end
desc 'build package artifacts'
task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args|
# Set task defaults
args.with_defaults(:configuration => 'debug', :sdks => ['macos'])
unless args.configuration == 'Debug'.downcase || args.configuration == 'Release'.downcase
fail("Unsupported configuration. Valid values: ['Debug', 'Release']. Found '#{args.configuration}''")
end
# Clean data generated by SPM
# FIXME: dangerous recursive rm
system("rm -rf #{DERIVED_DATA_DIR} > /dev/null 2>&1")
# Build
build_paths = []
args.sdks.each do |sdk|
spm_build(args.configuration, args.arch)
# Path of the executable looks like: `.build/(debug|release)/XCRemoteCache`
build_path_base = File.join(DERIVED_DATA_DIR, args.configuration)
sdk_build_paths = EXECUTABLE_NAMES.map {|e| File.join(build_path_base, e)}
build_paths.push(sdk_build_paths)
end
puts "Build products: #{build_paths}"
if args.configuration == 'Release'.downcase
puts "Creating release zip"
create_release_zip(build_paths[0])
end
end
desc 'Build release artifacts'
task :prepare_release do
system("rm -rf releases && rm -rf tmp")
Rake::Task['build'].invoke("release", "x86_64-apple-macosx")
system("mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-x86_64")
system("rm -rf releases")
Rake::Task['build'].invoke("release", "arm64-apple-macosx")
system("rake 'build[release, arm64-apple-macosx]'")
system("mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-arm64")
system("rm -rf releases")
system("mkdir -p releases && zip -jr releases/XCRemoteCache-macOS-x86_64.zip LICENSE README.md tmp/xcremotecache-x86_64")
system("zip -jr releases/XCRemoteCache-macOS-arm64.zip LICENSE README.md tmp/xcremotecache-arm64")
system("mkdir -p tmp/xcremotecache && ls tmp/xcremotecache-x86_64 | xargs -I {} lipo -create -output tmp/xcremotecache/{} tmp/xcremotecache-x86_64/{} tmp/xcremotecache-arm64/{}")
system("zip -jr releases/XCRemoteCache-macOS-arm64-x86_64.zip LICENSE README.md tmp/xcremotecache")
end
desc 'run tests with SPM'
task :test do
# Running tests
spm_test()
end
desc 'build and run E2E tests'
task :e2e => [:build, :e2e_only]
desc 'run E2E tests without building the XCRemoteCache binary'
task :e2e_only => ['e2e:run']
################################
# Helper functions
################################
def spm_build(configuration, arch)
spm_cmd = "swift build "\
"-c #{configuration} "\
"#{arch.nil? ? "" : "--triple #{arch}"} "
system(spm_cmd) or abort "Build failure"
end
def bash(command)
system "bash -c \"#{command}\""
end
def spm_test()
tests_output_file = File.join(DERIVED_DATA_DIR, 'tests.log')
# Redirect error stream with to a file and pass to the second stream output
spm_cmd = "swift test --enable-code-coverage 2> >(tee #{tests_output_file})"
test_succeeded = bash(spm_cmd)
abort "Test failure" unless test_succeeded
end
def create_release_zip(build_paths)
release_dir = RELEASES_ROOT_DIR
# Create and move files into the release directory
mkdir_p release_dir
build_paths.each {|p|
cp_r p, release_dir
}
output_artifact_basename = "#{PROJECT_NAME}.zip"
Dir.chdir(release_dir) do
# -X: no extras (uid, gid, file times, ...)
# -x: exclude .DS_Store
# -r: recursive
system("zip -X -x '*.DS_Store' -r #{output_artifact_basename} .") or abort "zip failure"
# List contents of zip file
system("unzip -l #{output_artifact_basename}") or abort "unzip failure"
end
end
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// Aggregator is an empty target to rebuild all executables
@@ -0,0 +1,117 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Locally generated artifact
struct Artifact {
/// Unique identifier of an artifact
let id: String
/// Location of the generated artifact package
let package: URL
/// Location of the generated meta file
let meta: URL
}
/// Creates a local artifact that contains all products generated in the building process
protocol ArtifactCreator {
func createArtifact(artifactKey: String, meta: MainArtifactMeta) throws -> Artifact
}
class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
private let buildDir: URL
private let tempDir: URL
private let executablePath: String
private let moduleName: String?
private let modulesFolderPath: String
private let dSYMPath: URL
private let metaWriter: MetaWriter
private let artifactProcessor: ArtifactProcessor
private let fileManager: FileManager
init(
buildDir: URL,
tempDir: URL,
executablePath: String,
moduleName: String?,
modulesFolderPath: String,
dSYMPath: URL,
metaWriter: MetaWriter,
artifactProcessor: ArtifactProcessor,
fileManager: FileManager
) {
self.buildDir = buildDir
self.modulesFolderPath = modulesFolderPath
self.tempDir = tempDir
self.executablePath = executablePath
self.moduleName = moduleName
self.fileManager = fileManager
self.dSYMPath = dSYMPath
self.metaWriter = metaWriter
self.artifactProcessor = artifactProcessor
super.init(workingDir: tempDir, moduleName: moduleName, fileManager: fileManager)
}
func createArtifact(artifactKey: String, meta: MainArtifactMeta) throws -> Artifact {
let zipWorkingDir = buildingArtifactLocation()
let binary = buildDir.appendingPathComponent(executablePath)
var zipPaths = [binary]
let swiftArtifacts = try prepareSwiftArtifacts(tempDir: zipWorkingDir)
zipPaths.append(contentsOf: swiftArtifacts)
let dynamicLibraryArtifacts = try prepareDynamicLibraryArtifacts()
zipPaths.append(contentsOf: dynamicLibraryArtifacts)
let creator = ZipArtifactCreator(
workingDir: zipWorkingDir,
metaWriter: metaWriter,
fileManager: fileManager
)
return try creator.createArtifact(zipContent: zipPaths, artifactKey: artifactKey, meta: meta)
}
/// Prepare optional swift products: .swiftmodule, .swiftdoc, -Swift.h
/// - Parameter tempDir: Temp location to organize file hierarchy in the artifact
/// - returns: URLs to include into the artifact package
fileprivate func prepareSwiftArtifacts(tempDir: URL) throws -> [URL] {
try artifactProcessor.process(localArtifact: tempDir)
var artifacts: [URL] = []
// Add optional directory with generated ObjC headers
let generatedObjCURL = buildingArtifactObjCHeadersLocation()
if fileManager.fileExists(atPath: generatedObjCURL.path) {
artifacts.append(generatedObjCURL)
}
// Add optional directory with generated .swiftmodule files
let generatedSwiftModuleURL = buildingArtifactSwiftModulesLocation()
if fileManager.fileExists(atPath: generatedSwiftModuleURL.path) {
artifacts.append(generatedSwiftModuleURL)
}
return artifacts
}
/// Returns a list of extra files to bundle, related to the dynamic library (if present)
fileprivate func prepareDynamicLibraryArtifacts() throws -> [URL] {
if fileManager.fileExists(atPath: dSYMPath.path) {
return [dSYMPath]
}
return []
}
}
@@ -0,0 +1,124 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
import Zip
enum ArtifactOrganizerError: Error {
case invalidLocation(URL)
}
enum ArtifactOrganizerLocationPreparationResult: Equatable {
/// Artifact already exists at the artifactDir
case artifactExists(artifactDir: URL)
/// Ready to download the artifact into artifact location
case preparedForArtifact(artifact: URL)
}
/// Prepares existing .zip artifact for the local operations
protocol ArtifactOrganizer {
/// Prepares the location for the artifact unzipping
/// - Parameter fileKey: artifact fileKey that corresponds to the zip filename on the remote cache server
func prepareArtifactLocationFor(fileKey: String) throws -> ArtifactOrganizerLocationPreparationResult
/// Unzips the zip artifact at the URL
func prepare(artifact: URL) throws -> URL
/// Activates the artifact - to all other xc* applications use it (links the directory to the "active" location)
func activate(extractedArtifact: URL) throws
/// Returns local location of the artifact to use in cached scenario (aka active artifact)
func getActiveArtifactLocation() -> URL
/// Returns a fileKey of the current active artifact
func getActiveArtifactFilekey() throws -> String
}
class ZipArtifactOrganizer: ArtifactOrganizer {
private let cacheDir: URL
// all processors that should "prepare" the unzipped raw artifact
private let artifactProcessors: [ArtifactProcessor]
private let fileManager: FileManager
init(targetTempDir: URL, artifactProcessors: [ArtifactProcessor], fileManager: FileManager) {
cacheDir = targetTempDir.appendingPathComponent("xccache")
self.artifactProcessors = artifactProcessors
self.fileManager = fileManager
}
private func getArtifactLocation(for fileKey: String) -> URL {
return cacheDir.appendingPathComponent(fileKey)
}
func getActiveArtifactLocation() -> URL {
return cacheDir.appendingPathComponent("active")
}
func getActiveArtifactFilekey() throws -> String {
let activeLocation = getActiveArtifactLocation()
// Context specific fingerprint is used as a name of an active directory symlink. That ensures that
// aritfacts do not mix up with each other but also gives a chance here to quickly get a fingerprint string
let localArtifactLocation = try fileManager.spt_followSymbolicLink(activeLocation)
return localArtifactLocation.lastPathComponent
}
func prepareArtifactLocationFor(fileKey: String) throws -> ArtifactOrganizerLocationPreparationResult {
let artifactDirURL = getArtifactLocation(for: fileKey)
let artifactPackageURL = artifactDirURL.appendingPathExtension("zip")
if fileManager.fileExists(atPath: artifactDirURL.path) {
return .artifactExists(artifactDir: artifactDirURL)
}
try createParentLocation(for: artifactPackageURL)
return .preparedForArtifact(artifact: artifactPackageURL)
}
func prepare(artifact: URL) throws -> URL {
let destinationURL = artifact.deletingPathExtension()
guard fileManager.fileExists(atPath: destinationURL.path) == false else {
infoLog("Skipping artifact, already existing at \(destinationURL)")
return destinationURL
}
// Uzipping to a temp file first to never leave the uncompleted zip in the final location
// when the command was interrupted (internal crash or `kill -9` signal)
let tempDestination = destinationURL.appendingPathExtension("tmp")
try Zip.unzipFile(artifact, destination: tempDestination, overwrite: true, password: nil)
try artifactProcessors.forEach { processor in
try processor.process(rawArtifact: tempDestination)
}
try fileManager.moveItem(at: tempDestination, to: destinationURL)
return destinationURL
}
func activate(extractedArtifact: URL) throws {
let activeLocationURL = getActiveArtifactLocation()
try fileManager.spt_forceSymbolicLink(at: activeLocationURL, withDestinationURL: extractedArtifact)
}
private func createParentLocation(for file: URL) throws {
let directoryURL = file.deletingLastPathComponent()
var isDir: ObjCBool = false
if fileManager.fileExists(atPath: directoryURL.path, isDirectory: &isDir) {
guard isDir.boolValue else {
errorLog("Invalid Artifact parent location at: \(directoryURL.description)")
throw ArtifactOrganizerError.invalidLocation(directoryURL)
}
} else {
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
}
}
}
@@ -0,0 +1,55 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Plugin that can extend the artifact creation phase
protocol ArtifactCreatorPlugin {
/// Optional remapper that replaces dependencies paths
/// Useful when a plugin modifies underlying file locations in the compilation step
var customPathsRemapper: DependenciesRemapper? { get }
/// Gives a chance to append extra keys to the meta type that will be uploaded to the cache server
/// - Parameter meta: existing meta
/// - Returns: extra dictionary that should be appended to the meta's extraKeys field
func extraMetaKeys(_ meta: MainArtifactMeta) throws -> [String: String]
/// Optional artifacts that should be uploaded to the remote server
/// - Parameter main: main artifact that has been uploaded to the remote cache server
/// - Returns: list of artifacts that should be uploaded
func artifactToUpload(main: MainArtifactMeta) throws -> [Artifact]
}
/// Plugin that manages addons to the artifact consumption phase (in the prebuild phase)
protocol ArtifactConsumerPrebuildPlugin {
/// Called when the artifact preparation phase happens. Intended to download all companion artifacts uploaded
/// from the `artifactToUpload` returned items
/// - Parameter meta: main artifact meta
func run(meta: MainArtifactMeta) throws
}
/// Plugin that manages addons to the artifact consumption phase (in the postbuild phase)
protocol ArtifactConsumerPostbuildPlugin {
/// Called after the target has been reused from cache
/// - Parameter meta: main artifact meta
func run(meta: MainArtifactMeta) throws
}
@@ -0,0 +1,80 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Performs a pre/postprocessing on an artifact package
/// Could be a place for file reorganization (to support legacy package formats) and/or
/// remapp absolute paths in some package files
protocol ArtifactProcessor {
/// Processes a raw artifact in a directory. Raw artifact is a format of an artifact
/// that is stored in a remote cache server (generic)
/// - Parameter rawArtifact: directory that contains raw artifact content
func process(rawArtifact: URL) throws
/// Processes a local artifact in a directory
/// - Parameter localArtifact: directory that contains local (machine-specific) artifact content
func process(localArtifact: URL) throws
}
/// Processes downloaded artifact by replacing generic paths in generated ObjC headers placed in ./include
class UnzippedArtifactProcessor: ArtifactProcessor {
/// All directories in an artifact that should be processed by path remapping
private static let remappingDirs = ["include"]
private let fileRemapper: FileDependenciesRemapper
private let dirScanner: DirScanner
init(fileRemapper: FileDependenciesRemapper, dirScanner: DirScanner) {
self.fileRemapper = fileRemapper
self.dirScanner = dirScanner
}
private func findProcessingEligableFiles(path: String) throws -> [URL] {
let remappingURL = URL(fileURLWithPath: path)
let allFiles = try dirScanner.recursiveItems(at: remappingURL)
return allFiles.filter({ !$0.isHidden })
}
/// Replaces all generic paths in a raw artifact's `include` dir with
/// absolute paths, specific for a given machine and configuration
/// - Parameter rawArtifact: raw artifact location
func process(rawArtifact url: URL) throws {
for remappingDir in Self.remappingDirs {
let remappingPath = url.appendingPathComponent(remappingDir).path
let allFiles = try findProcessingEligableFiles(path: remappingPath)
try allFiles.forEach(fileRemapper.remap(fromGeneric:))
}
}
func process(localArtifact url: URL) throws {
for remappingDir in Self.remappingDirs {
let remappingPath = url.appendingPathComponent(remappingDir).path
let allFiles = try findProcessingEligableFiles(path: remappingPath)
try allFiles.forEach(fileRemapper.remap(fromLocal:))
}
}
}
fileprivate extension URL {
// Recognize hidden files starting with a dot
var isHidden: Bool {
lastPathComponent.hasPrefix(".")
}
}
@@ -0,0 +1,121 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ArtifactSwiftProductsBuilderError: Error {
/// Thrown when trying to include generated ObjC header to a non-module target
case populatingObjCHeaderForNonModule
/// Throws when trying to include non-existing ObjC header
case populatingNonExistingObjCHeader
/// Missing generated swiftmodule-related file (e.f. .swiftmodule or .swiftdoc)
case missingGeneratedModuleFile(path: String)
}
/// A builder to prepare artifact Swift-generated products in a single location, ready to zip into an artifact archive
protocol ArtifactSwiftProductsBuilder {
/// Location where all files expected to be bundled to the product should be placed
func buildingArtifactLocation() -> URL
/// Location where all generated ObjC headers should be placed in order to be bundled into the artifact product
/// - Returns: location URL to put ObjC headers
func buildingArtifactObjCHeadersLocation() -> URL
/// Moves generated ObjC header to the artifact "working" location
/// - Parameter arch: architecture of the build
/// - Parameter headerURL: file to include as an ObjC header
func includeObjCHeaderToTheArtifact(arch: String, headerURL: URL) throws
/// Moves generated .swift{module|doc} products to the artifact "working" location
/// - Parameter arch: architecture of the build
/// - Parameter moduleURL: generated .swift{module|doc|..} file
func includeModuleDefinitionsToTheArtifact(arch: String, moduleURL: URL) throws
}
/// Default Builder implementation for a Swift module compilation step
/// * all files are stored in #{workingDir}/xccache/produced
/// * all module ObjC headers are stored in
/// # {workingDir}/xccache/produced/include/#{moduleName} (if `moduleName` is defined)
class ArtifactSwiftProductsBuilderImpl: ArtifactSwiftProductsBuilder {
private let workingDir: URL
private let moduleName: String?
private let fileManager: FileManager
init(workingDir: URL, moduleName: String?, fileManager: FileManager) {
self.workingDir = workingDir
self.moduleName = moduleName
self.fileManager = fileManager
}
func buildingArtifactLocation() -> URL {
return workingDir.appendingPathComponent("xccache").appendingPathComponent("produced")
}
func buildingArtifactObjCHeadersLocation() -> URL {
return buildingArtifactLocation().appendingPathComponent("include")
}
func buildingArtifactSwiftModulesLocation() -> URL {
return buildingArtifactLocation().appendingPathComponent("swiftmodule")
}
func includeObjCHeaderToTheArtifact(arch: String, headerURL: URL) throws {
guard let module = moduleName else {
throw ArtifactSwiftProductsBuilderError.populatingObjCHeaderForNonModule
}
let zipObjCDir = buildingArtifactObjCHeadersLocation()
// Embed the ObjC header to the include/arch/module_name directory (XCRemoteCache arbitrary format)
let moduleObjCURL = zipObjCDir.appendingPathComponent(arch).appendingPathComponent(module)
let objCHeaderFilename = headerURL.lastPathComponent
let headerArtifactURL = moduleObjCURL.appendingPathComponent(objCHeaderFilename)
// Product module dir may not exist, even if the `moduleName` is present
guard fileManager.fileExists(atPath: headerURL.path) else {
throw ArtifactSwiftProductsBuilderError.populatingNonExistingObjCHeader
}
try fileManager.createDirectory(at: moduleObjCURL, withIntermediateDirectories: true, attributes: nil)
try fileManager.spt_forceCopyItem(at: headerURL, to: headerArtifactURL)
}
func includeModuleDefinitionsToTheArtifact(arch: String, moduleURL: URL) throws {
let zipModuleDir = buildingArtifactSwiftModulesLocation()
// Embed the swiftmodule|doc to the swiftmodule/arch/ directory (XCRemoteCache arbitrary format)
let artifactModuleURL = zipModuleDir.appendingPathComponent(arch)
let moduleURLDir = moduleURL.deletingLastPathComponent()
let swiftModuleFilename = moduleURL.deletingPathExtension().lastPathComponent
let swiftArtifactModuleBase = moduleURLDir.appendingPathComponent(swiftModuleFilename)
let filesToInclude: [URL] = try SwiftmoduleFileExtension.SwiftmoduleExtensions.compactMap { ext, type in
let file = swiftArtifactModuleBase.appendingPathExtension(ext.rawValue)
guard fileManager.fileExists(atPath: file.path) else {
if case .required = type {
throw ArtifactSwiftProductsBuilderError.missingGeneratedModuleFile(path: file.path)
} else {
return nil
}
}
return file
}
// Product module dir may not exist, even if the `moduleName` is present
try fileManager.createDirectory(at: artifactModuleURL, withIntermediateDirectories: true, attributes: nil)
for fileToInclude in filesToInclude {
let filename = fileToInclude.lastPathComponent
let artifactLocation = artifactModuleURL.appendingPathComponent(filename)
try fileManager.spt_forceLinkItem(at: fileToInclude, to: artifactLocation)
}
}
}
@@ -0,0 +1,81 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum FileDependenciesRemapperError: Error {
/// Thrown when the file to remap is invalid (e.g. doesn't exist or has unexpected format)
case invalidRemappingFile(URL)
}
/// Replaces paths in a file content between generic (placeholder-based)
/// and local formats
protocol FileDependenciesRemapper {
/// Replaces all generic paths (with placeholders) to a local, machine
/// specific absolute paths
/// - Parameter url: location of a file that should be remapped in-place
func remap(fromGeneric url: URL) throws
/// Replaces all local, machine specific absolute paths to
/// generic ones
/// - Parameter url: location of a file that should be remapped in-place
func remap(fromLocal url: URL) throws
}
/// Remaps absolute paths in a text files stored on a disk
/// Note: That class can be used only for text-based files, not binaries
class TextFileDependenciesRemapper: FileDependenciesRemapper {
private static let linesSeparator = "\n"
private let remapper: DependenciesRemapper
private let fileAccessor: FileAccessor
init(remapper: DependenciesRemapper, fileAccessor: FileAccessor) {
self.remapper = remapper
self.fileAccessor = fileAccessor
}
private func readFileLines(_ url: URL) throws -> [String] {
guard let content = try fileAccessor.contents(atPath: url.path) else {
// the file is empty
return []
}
guard let contentString = String(data: content, encoding: .utf8) else {
throw FileDependenciesRemapperError.invalidRemappingFile(url)
}
return contentString.components(separatedBy: .newlines)
}
private func storeFileLines(lines: [String], url: URL) throws {
let contentString = lines.joined(separator: "\n")
let contentData = contentString.data(using: String.Encoding.utf8)
try fileAccessor.write(toPath: url.path, contents: contentData)
}
func remap(fromGeneric url: URL) throws {
let contentLines = try readFileLines(url)
let remappedContent = try remapper.replace(genericPaths: contentLines)
try storeFileLines(lines: remappedContent, url: url)
}
func remap(fromLocal url: URL) throws {
let contentLines = try readFileLines(url)
let remappedContent = try remapper.replace(localPaths: contentLines)
try storeFileLines(lines: remappedContent, url: url)
}
}
@@ -0,0 +1,48 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum SwiftmoduleFileExtensionType {
case required
case optional
}
// Type of the file that constitutes a full modulemap package
// RawValue corresponds to the file extension
enum SwiftmoduleFileExtension: String {
case swiftmodule
case swiftdoc
case swiftsourceinfo
case swiftinterface
case privateSwiftinterface = "private.swiftinterface"
case abiJson = "abi.json"
}
extension SwiftmoduleFileExtension {
/// List of all swiftmodule extensions that should be copied to the artifact
static let SwiftmoduleExtensions: [SwiftmoduleFileExtension: SwiftmoduleFileExtensionType] = [
.swiftmodule: .required,
.swiftdoc: .required,
.swiftsourceinfo: .optional,
.swiftinterface: .optional,
.privateSwiftinterface: .optional,
.abiJson: .optional,
]
}
@@ -0,0 +1,45 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
import Zip
class ZipArtifactCreator {
/// Location where zip file should be generated
private let workingDir: URL
private let metaWriter: MetaWriter
private let fileManager: FileManager
init(workingDir: URL, metaWriter: MetaWriter, fileManager: FileManager) {
self.workingDir = workingDir
self.metaWriter = metaWriter
self.fileManager = fileManager
}
func createArtifact<T: Meta>(zipContent: [URL], artifactKey: String, meta: T) throws -> Artifact {
let zipURL = workingDir.appendingPathComponent("\(artifactKey).zip")
try fileManager.createDirectory(at: workingDir, withIntermediateDirectories: true, attributes: nil)
// Include meta json to the artifact
let metaURL = try metaWriter.write(meta, locationDir: workingDir)
let zipPaths = zipContent + [metaURL]
try Zip.zipFiles(paths: zipPaths, zipFilePath: zipURL, password: nil, progress: nil)
return Artifact(id: artifactKey, package: zipURL, meta: metaURL)
}
}
@@ -0,0 +1,107 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum XCCreateUniversalBinaryError: Error {
/// Missing ar libraries that should constitute an universal build
case missingInputLibrary
}
/// Wrapper for `libtool`/`lipo` call for creating an universal binary
class XCCreateUniversalBinary: XCLibtoolLogic {
private let output: URL
private let tempDir: URL
private let firstInputURL: URL
private let toolName: String
private let fallbackCommand: String
init(
output: String,
inputs: [String],
toolName: String,
fallbackCommand: String
) throws {
self.output = URL(fileURLWithPath: output)
guard let firstInput = inputs.first else {
throw XCCreateUniversalBinaryError.missingInputLibrary
}
let firstInputURL = URL(fileURLWithPath: firstInput)
// inputs are place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/Binary/$TARGET_NAME.a
// TODO: find better (stable) technique to determine `$TARGET_TEMP_DIR`
errorLog("\(firstInputURL.absoluteString)")
tempDir = firstInputURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
self.firstInputURL = firstInputURL
self.toolName = toolName
self.fallbackCommand = fallbackCommand
}
func run() {
// check if RC is enabled. if so, take any input .a and copy to the output location
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
} catch {
errorLog("\(toolName) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
fallbackToDefault()
}
let markerURL = tempDir.appendingPathComponent(config.modeMarkerPath)
do {
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
guard markerReader.canRead() else {
fallbackToDefault()
}
// Remote cache artifact stores a final library from DerivedData/Products location
// (an universal binary here)
// Fot a target where universal binary is used as a product, an output from single-architecture `xclibtool`
// already is a universal library (the one from artifact package)
// Link any of input libraries (here first) to the final output location because xclibtool flow ensures
// that these are already an universal binary
try fileManager.spt_forceLinkItem(at: firstInputURL, to: output)
} catch {
errorLog("\(toolName) failed with error: \(error). Fallbacking to \(fallbackCommand)")
do {
try fileManager.removeItem(at: markerURL)
fallbackToDefault()
} catch {
exit(1, "FATAL: \(fallbackCommand) failed with error: \(error)")
}
}
}
private func fallbackToDefault() -> Never {
let args = ProcessInfo().arguments
let paramList = [fallbackCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(fallbackCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
}
@@ -0,0 +1,60 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Represents a mode that libtool was called
public enum XCLibtoolMode: Equatable {
/// Creating a static library (ar format) from a set of .o input files
case createLibrary(output: String, filelist: String, dependencyInfo: String)
/// Creating a universal library (multiple-architectures) from a set of input .a static libraries
case createUniversalBinary(output: String, inputs: [String])
}
public class XCLibtool {
private let logic: XCLibtoolLogic
/// Intializer that depending on the argument mode, creates different libtool logic (kind of abstract factory)
/// - Parameter mode: libtool mode to setup
/// - Throws: XCLibtoolLogic specific errors if the mode arguments are invalid or inconsistent
public init(_ mode: XCLibtoolMode) throws {
switch mode {
case .createLibrary(let output, let filelist, let dependencyInfo):
logic = XCCreateBinary(
output: output,
filelist: filelist,
dependencyInfo: dependencyInfo,
fallbackCommand: "libtool",
stepDescription: "Libtool"
)
case .createUniversalBinary(let output, let inputs):
logic = try XCCreateUniversalBinary(
output: output,
inputs: inputs,
toolName: "Libtool",
fallbackCommand: "libtool"
)
}
}
/// Executes the libtool logic
public func run() {
logic.run()
}
}
@@ -0,0 +1,26 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/// XCLibtool wrapper logic that executes the libtool logic
protocol XCLibtoolLogic {
/// Executes xclibtool mocked logic or fallbacks to the libtool execution
func run()
}
extension XCCreateBinary: XCLibtoolLogic {}
@@ -0,0 +1,50 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Wrapper for a `lipo` tool that creates a fat archive
public class XCLipo {
private let logic: XCLibtoolLogic
public init(
output: String,
inputs: [String],
fallbackCommand: String,
stepDescription: String
) throws {
errorLog("\(output)")
errorLog("\(inputs.joined(separator: ","))")
logic = try XCCreateUniversalBinary(
output: output,
inputs: inputs,
toolName: stepDescription,
fallbackCommand: fallbackCommand
)
}
/// Handles a `-create` action which is responsible to create a fat archive
/// If remote cache can reuse artifacts from a remote cache, it just links any of input
/// files to the destination (output) location because the binary in XCRC artifact already
/// contains a fat library
/// If a remote artifact cannot be reused, a fallback to the `lipo` command is performed
public func run() {
logic.run()
}
}
@@ -0,0 +1,26 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Errors thrown from Plugins
enum PluginError: Error {
/// The error is severe and the command should fail immediately
case unrecoverableError(Error)
}
@@ -0,0 +1,74 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ArtifactInspectorError: Error {
/// The unzipped artifact is malformed. Is misses *.swiftmodule file in "swiftmodule/{{arch}}" directory
case missingSwiftmoduleFileInArtifact(artifact: URL)
}
// Inspects the unzipped artifact
protocol ArtifactInspector {
/// Enumerates all files in an artifact and finds out which should be moved to the builtProductsDir
/// - Parameter artifact: location of the unzipped artifact
/// - Returns: all files/dirs to move to builtProductsDir
func findBinaryProducts(fromArtifact artifact: URL) throws -> [URL]
/// Inspects unzipped artifact file structure to recognize the name of a module name
func recognizeModuleName(fromArtifact artifact: URL, arch: String) throws -> String?
}
class DefaultArtifactInspector: ArtifactInspector {
private let dirAccessor: DirAccessor
/// Name of a directory in an artifact that stores swiftmodules files
private static let ArtifactSwiftmoduleDir = "swiftmodule"
/// Swiftmodule file extension in an artifact
private static let SwiftmoduleFileExtension = "swiftmodule"
/// Extensions of files that should be considered as binaries
// TODO: Supporting only libraries for now. Consider other formats like frameworks or dsyms
private static let BinaryProductsExtensions = ["a"]
init(dirAccessor: DirAccessor) {
self.dirAccessor = dirAccessor
}
func findBinaryProducts(fromArtifact artifact: URL) throws -> [URL] {
let artifactItems = try dirAccessor.items(at: artifact)
return artifactItems.filter { Self.BinaryProductsExtensions.contains($0.pathExtension) }
}
func recognizeModuleName(fromArtifact artifact: URL, arch: String) throws -> String? {
let swiftmodulesDir = artifact
.appendingPathComponent(Self.ArtifactSwiftmoduleDir)
.appendingPathComponent(arch)
guard case .dir = try dirAccessor.itemType(atPath: swiftmodulesDir.path) else {
// This target doesn't contain any swiftmodule (e.g. ObjC target)
return nil
}
// All files have basename of a modulename
let moduleFiles = try dirAccessor.items(at: swiftmodulesDir)
// Find a first *.swiftmodule file's basename - the "swiftmodule/{{arch}}" directory contains
// {{moduleName}}.swiftc{module|doc} files
let swiftmoduleFile = moduleFiles.first(where: { $0.pathExtension == Self.SwiftmoduleFileExtension })
guard swiftmoduleFile != nil else {
throw ArtifactInspectorError.missingSwiftmoduleFileInArtifact(artifact: artifact)
}
return swiftmoduleFile.map { $0.deletingPathExtension().lastPathComponent }
}
}
@@ -0,0 +1,45 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Factory to create `ArtifactOrganizer`
protocol ThinningConsumerArtifactsOrganizerFactory {
/// Builds artifacts aggregator that oranizes artifacts in a dedicated target temp dir
/// - Parameter targetTempDir: location where should the organizer organize the artifact ($TARGET_TEMP_DIR)
func build(targetTempDir: URL) -> ArtifactOrganizer
}
class ThinningConsumerZipArtifactsOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory {
private let processors: [ArtifactProcessor]
private let fileManager: FileManager
init(processors: [ArtifactProcessor], fileManager: FileManager) {
self.processors = processors
self.fileManager = fileManager
}
func build(targetTempDir: URL) -> ArtifactOrganizer {
ZipArtifactOrganizer(
targetTempDir: targetTempDir,
artifactProcessors: processors,
fileManager: fileManager
)
}
}
@@ -0,0 +1,109 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Factory that builds Swift products organizer (that can place all files in the final Derived Data location)
/// for a specific module
protocol ThinningConsumerSwiftProductsOrganizerFactory {
/// Builds products organizer that produces swift products (swiftmodule, swiftdoc) for a given module
/// - Parameters:
/// - architecture: .swiftmodule architecture to generate
/// (it can be an extended arch, like "x86_64-apple-ios-simulator")
/// - targetName: name of the target to generate
/// - moduleName: name of the module to generate
/// - artifactLocation: location of the unzipped artifact
func build(
architecture: String,
targetName: String,
moduleName: String,
artifactLocation: URL
) -> SwiftProductsOrganizer
}
/// Factory that syncs swiftc products from the the unzipped artifacts and uses product generator that
/// employs hard linking to place files in the desired location
class ThinningConsumerUnzippedArtifactSwiftProductsOrganizerFactory: ThinningConsumerSwiftProductsOrganizerFactory {
/// The base architecture - that current build is compiling. Equals $(ARCHS)
private let arch: String
private let productsLocationProvider: SwiftProductsLocationProvider
private let fingerprintSyncer: FingerprintSyncer
private let diskCopier: DiskCopier
/// Default initializer
/// - Parameters:
/// - arch: current architecture that the target is building for
/// - productsLocationProvider: a provider that provides swift products final location
/// - fingerprintSyncer: a syncer to decorate swift products with a figerprint override
/// - fileManager: FileManager
init(
arch: String,
productsLocationProvider: SwiftProductsLocationProvider,
fingerprintSyncer: FingerprintSyncer,
diskCopier: DiskCopier
) {
self.arch = arch
self.productsLocationProvider = productsLocationProvider
self.fingerprintSyncer = fingerprintSyncer
self.diskCopier = diskCopier
}
/// Generates a swift products generator for a specific architecture and moduleName
/// - Parameters:
/// - architecture: .swiftmodule architecture to generate
/// (it can be an extended arch, like "x86_64-apple-ios-simulator")
/// - targetName: target name to generate
/// - moduleName: swiftmodule name
private func buildGenerator(architecture: String, targetName: String, moduleName: String) -> SwiftcProductsGenerator {
let modulePathOutput = productsLocationProvider.swiftmoduleFileLocation(
moduleName: moduleName,
architecture: architecture
)
let objcHeaderOutput = productsLocationProvider.objcHeaderLocation(
targetName: targetName,
moduleName: moduleName
)
return ThinningDiskSwiftcProductsGenerator(
modulePathOutput: modulePathOutput,
objcHeaderOutput: objcHeaderOutput,
diskCopier: diskCopier
)
}
func build(
architecture: String,
targetName: String,
moduleName: String,
artifactLocation: URL
) -> SwiftProductsOrganizer {
let productGenerator = buildGenerator(
architecture: architecture,
targetName: targetName,
moduleName: moduleName
)
return UnzippedArtifactSwiftProductsOrganizer(
arch: arch,
moduleName: moduleName,
artifactLocation: artifactLocation,
productsGenerator: productGenerator,
fingerprintSyncer: fingerprintSyncer
)
}
}
@@ -0,0 +1,104 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum WorkerResult {
case successes
case errors([Error])
}
/// Worker that manages executing blocks
protocol Worker {
/// Adding an action to run in parallel
/// - Parameter action: action to perform
func appendAction(_ action: @escaping () throws -> Void)
/// Wait for actions to finish
/// - Returns: execution result of all appended actions
func waitForResult() -> WorkerResult
}
/// Worker that executes actions in pararell using DispatchGroup
/// Warning! This implementation is not thread safe: all functions have to be called from the same thread
class DispatchGroupParallelizationWorker: Worker {
private let group: DispatchGroup
private let queue: DispatchQueue
private let qos: DispatchQoS.QoSClass
private var observedErrors: [Error]
/// Default initializer
/// - Parameter qos: QoS of the background queue to execute actions
init(qos: DispatchQoS.QoSClass = .userInteractive) {
group = DispatchGroup()
queue = DispatchQueue(
label: "DispatchGroupParallelization",
qos: .userInteractive,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: .global(qos: qos)
)
observedErrors = []
self.qos = qos
}
func appendAction(_ action: @escaping () throws -> Void) {
group.enter()
queue.async {
do {
try action()
} catch {
// Errors are not expected to be frequent so just enqueing another block to the working group
self.group.enter()
self.queue.async(group: self.group, qos: self.qos.dispatchQoS, flags: .barrier) {
self.observedErrors.append(error)
self.group.leave()
}
}
self.group.leave()
}
}
func waitForResult() -> WorkerResult {
group.wait()
if observedErrors.isEmpty {
return .successes
}
defer {
observedErrors = []
}
return .errors(observedErrors)
}
}
extension DispatchQoS.QoSClass {
/// Trivial transform from DispatchQoS.QoSClass to DispatchQoS
var dispatchQoS: DispatchQoS {
switch self {
case .background: return .background
case .default: return .default
case .unspecified: return .unspecified
case .userInitiated: return .userInitiated
case .userInteractive: return .userInteractive
case .utility: return .utility
@unknown default:
return .default
}
}
}
@@ -0,0 +1,64 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
// Recognizes
protocol SwiftProductsArchitecturesRecognizer {
/// Scans Product dir to find which final archs Xcode generated for a target
/// Sample architecture list: ["x86_64", "x86_64-apple-ios-simulator"]
/// - Parameters:
/// - builtProductsDir: Location of the bulilt products dir to inspect - $(BUILT_PRODUCTS_DIR)
/// - moduleName: a name of the module to inspect
/// - Returns: list of architectures
func recognizeArchitectures(builtProductsDir: URL, moduleName: String) throws -> [String]
}
class DefaultSwiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRecognizer {
/// Extension of a directory that contains all swift{module|doc|...} files
private static let SwiftmoduleDirExtension = "swiftmodule"
private let dirAccessor: DirAccessor
init(dirAccessor: DirAccessor) {
self.dirAccessor = dirAccessor
}
func recognizeArchitectures(builtProductsDir: URL, moduleName: String) throws -> [String] {
/// Location where Xcode puts all swiftmodules
let moduleDirectory = builtProductsDir
.appendingPathComponent(moduleName)
.appendingPathExtension(Self.SwiftmoduleDirExtension)
// Skip folders (e.g. 'Project' dir that stores .sourceinfo, introduced in Xcode13)
let productFiles = try dirAccessor.items(at: moduleDirectory).filter { url in
try dirAccessor.itemType(atPath: url.path) == .file
}
/// files in a moduleDirectory have basename corresponding to the
/// architecture (e.g. 'x86_64-apple-ios-simulator.swiftmodule', 'x86_64.swiftmodule' ...)
let architectures = productFiles.map { file -> String in
// recursively delete extensions to get rid of potential fingerprint overrides in a product directory
var basenameFile = file
while !basenameFile.pathExtension.isEmpty {
basenameFile.deletePathExtension()
}
return basenameFile.lastPathComponent
}
// remove duplicates coming from files with different extensions (swiftmodule, swiftdoc etc.)
return Set(architectures).sorted()
}
}
@@ -0,0 +1,77 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Provider of all swift products location, expected by the Xcode
protocol SwiftProductsLocationProvider {
/// Destination of the ObjC header
/// - Parameters:
/// - targetName: target name of the swift target
/// - moduleName: name of the module
func objcHeaderLocation(targetName: String, moduleName: String) -> URL
/// Destination of the .swiftmodule file
/// - Parameters:
/// - moduleName: name of the module
/// - architecture: architecture of the swiftmodule
func swiftmoduleFileLocation(moduleName: String, architecture: String) -> URL
}
class DefaultSwiftProductsLocationProvider: SwiftProductsLocationProvider {
private let builtProductsDir: URL
private let derivedSourcesDir: URL
/// Default initializer
/// - Parameters:
/// - builtProductsDir: current $(BUILD_PRODUCTS_DIR)
/// - derivedSourcesDir: current $(DERIVED_SOURCES_DIR)
init(
builtProductsDir: URL,
derivedSourcesDir: URL
) {
self.derivedSourcesDir = derivedSourcesDir
self.builtProductsDir = builtProductsDir
}
func objcHeaderLocation(targetName: String, moduleName: String) -> URL {
// By default, Xcode generates ObjC headers for a Swift module in
// $(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME), where $(SWIFT_OBJC_INTERFACE_HEADER_NAME)
// has a format of "\(moduleName)-Swift.h"
// To generate a header location for some other target,
// we need to replaced the last component of $DERIVED_SOURCES_DIR with {{targetName}}.build
let derivedPathDirFormat = derivedSourcesDir.lastPathComponent
let targetSpecificDerivedSourcesDir = derivedSourcesDir
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("\(targetName).build")
.appendingPathComponent(derivedPathDirFormat)
return targetSpecificDerivedSourcesDir.appendingPathComponent("\(moduleName)-Swift.h")
}
func swiftmoduleFileLocation(moduleName: String, architecture: String) -> URL {
// swiftmodule should be generated in a DerivedData's Product dir with a format:
// "{{ModuleName}}.swiftmodule/{{arch}}.swiftmodule"
builtProductsDir
.appendingPathComponent("\(moduleName).swiftmodule")
.appendingPathComponent(architecture)
.appendingPathExtension("swiftmodule")
}
}
@@ -0,0 +1,150 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ThinningConsumerPostbuildPluginError: Error {
/// The aggregation target meta misses a filekey for targets
case missingArtifactKey(targetNames: [String])
/// The unzipped artifact is malformed. Is misses a binary file in a root directory
case missingBinaryForArtifact(artifact: URL)
/// Postbuild of some target(s) failed (potentially the unzipped artifacts is broken)
case failed(underlyingErrors: [Error])
}
/// Plugin that performs "postbuild" action for all thinned targets - moves binaries, swift products, decorates with
/// fingerprint overrides etc
class ThinningConsumerPostbuildPlugin: ThinningConsumerPlugin, ArtifactConsumerPostbuildPlugin {
private let targetTempDirsRoot: URL
private let builtProductsDir: URL
private let productModuleName: String
private let arch: String
private let thinnedTargets: [String]
private let artifactOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory
private let swiftProductOrganizerFactory: ThinningConsumerSwiftProductsOrganizerFactory
private let diskCopier: DiskCopier
private let artifactInspector: ArtifactInspector
private let swiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRecognizer
private let worker: Worker
init(
targetTempDir: URL,
builtProductsDir: URL,
productModuleName: String,
arch: String,
thinnedTargets: [String],
artifactOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory,
swiftProductOrganizerFactory: ThinningConsumerSwiftProductsOrganizerFactory,
artifactInspector: ArtifactInspector,
swiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRecognizer,
diskCopier: DiskCopier,
worker: Worker
) {
targetTempDirsRoot = targetTempDir.deletingLastPathComponent()
self.builtProductsDir = builtProductsDir
self.productModuleName = productModuleName
self.arch = arch
self.thinnedTargets = thinnedTargets
self.artifactOrganizerFactory = artifactOrganizerFactory
self.swiftProductOrganizerFactory = swiftProductOrganizerFactory
self.artifactInspector = artifactInspector
self.swiftProductsArchitecturesRecognizer = swiftProductsArchitecturesRecognizer
self.diskCopier = diskCopier
self.worker = worker
}
/// Performs the core part of the postbuild phase for a single thinned target
/// - Parameters:
/// - targetName: Name of the target
/// - productArchs: all architectures that should swift products
/// should be generated in DerivedData's 'Products' dir
/// - fileKey: fileKey that describes the artifact
private func performPostbuildFor(targetName: String, productArchs archs: [String], fileKey: String) throws {
// move all downloaded in prebuild phase
// headers+binaries+swiftmodule(s) to the corresponding `targetName` directory
let targetTempDir = targetTempDirsRoot.appendingPathComponent("\(targetName).build")
let artifactOrganizer = artifactOrganizerFactory.build(targetTempDir: targetTempDir)
let artifactLocation = artifactOrganizer.getActiveArtifactLocation()
// Move cached binary artifacts to the product dir
let binaryProducts = try artifactInspector.findBinaryProducts(fromArtifact: artifactLocation)
guard !binaryProducts.isEmpty else {
throw ThinningConsumerPostbuildPluginError.missingBinaryForArtifact(artifact: artifactLocation)
}
try binaryProducts.compactMap { $0 }.forEach { product in
try diskCopier.copy(file: product, directory: builtProductsDir)
}
// Move Swift module definitions
guard
let moduleName = try artifactInspector.recognizeModuleName(fromArtifact: artifactLocation, arch: arch)
else {
/// Skip targets without swiftmodules (e.g. ObjC targets)
return
}
// Swiftmodules in an artifact are cached from the "swiftc" step. Xcode along moving the swiftmodule files
// to the builtProductsDir, duplicates the swiftmodule definition for extra archs
// (e.g. "x86_64" -> ["x86_64, "x86_64-apple-ios-simulator"])
for arch in archs {
let productsOrganizer = swiftProductOrganizerFactory.build(
architecture: arch,
targetName: targetName,
moduleName: moduleName,
artifactLocation: artifactLocation
)
/// fileKey is equivalent of the fingerprint
try productsOrganizer.syncProducts(fingerprint: fileKey)
}
}
func run(meta: MainArtifactMeta) throws {
onRun()
// iterate all thinned targetName temp dirs and perform postbuild action
let allCachedTargetFileKeys = ThinningPlugin.extractAllProductArtifacts(meta: meta)
let thinnedTargetFileKeys = allCachedTargetFileKeys.filter { targetName, _ in
thinnedTargets.contains(targetName)
}
// Ensure all thinned targets keys are available in a meta
// (This is a second safety-net for. The same validation is done in the prebuild phase)
let missedThinnedTargets = Set(thinnedTargets).subtracting(Set(thinnedTargetFileKeys.keys))
guard missedThinnedTargets.isEmpty else {
let targetNames = Array(missedThinnedTargets)
let rawError = ThinningConsumerPostbuildPluginError.missingArtifactKey(targetNames: targetNames)
// Thin project requires all artifacts to be available locally - has to fail immediately
throw PluginError.unrecoverableError(rawError)
}
let archs = try swiftProductsArchitecturesRecognizer.recognizeArchitectures(
builtProductsDir: builtProductsDir,
moduleName: productModuleName
)
for (targetName, fileKey) in thinnedTargetFileKeys {
worker.appendAction {
try self.performPostbuildFor(targetName: targetName, productArchs: archs, fileKey: fileKey)
}
}
if case .errors(let errors) = worker.waitForResult() {
let rawError = ThinningConsumerPostbuildPluginError.failed(underlyingErrors: errors)
// Thin project requires all artifacts to be available locally - has to fail immediately
throw PluginError.unrecoverableError(rawError)
}
}
}
@@ -0,0 +1,118 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ThinningConsumerPrebuildPluginError: Error {
/// Preparing a target(s) is not possible (potentially the artifact is not available or broken)
case failedPreparation(underlyingErrors: [Error])
/// TEMP_TEMP_DIR env is customised, what is not supported in a thinning mode
case detectedOverwrittenTempDir
/// The target that should be cached was not generated on the remote side
case missingCachedTarget(missingTargets: [String])
}
/// Prebuild plugin that downloads all thinned targets artifacts and places them in the places it would be extracted
/// in a standard (non-thinned) prebuild step
class ThinningConsumerPrebuildPlugin: ThinningConsumerPlugin, ArtifactConsumerPrebuildPlugin {
private let tempDir: URL
private let targetName: String
private let thinnedTargets: [String]
private let artifactsOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory
private let networkClient: RemoteNetworkClient
private let worker: Worker
/// Default initializer
/// - Parameters:
/// - targetName: Target name of the current (aggregation) target
/// - tempDir: $(TEMP_DIR) of the current (aggregation) target
/// - thinnedTargets: an array of all targets that are thinned and should be downloaded and prepared
/// - artifactsOrganizerFactory: a factory that provides an artifact organiser
/// - networkClient: network client used for downloading artifacts
/// - worker: a manager that schedules blocks executions (potentially in parallel)
init(
targetName: String,
tempDir: URL,
thinnedTargets: [String],
artifactsOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory,
networkClient: RemoteNetworkClient,
worker: Worker
) {
self.targetName = targetName
self.tempDir = tempDir
self.thinnedTargets = thinnedTargets
self.artifactsOrganizerFactory = artifactsOrganizerFactory
self.networkClient = networkClient
self.worker = worker
}
/// Builds a $(TARGET_TEMP_DIR) for some other target, based on a pattern that current (aggregation) target was
/// called with
private func buildTempDir(forProductName otherProduct: String) throws -> URL {
guard tempDir.lastPathComponent == "\(targetName).build" else {
throw ThinningConsumerPrebuildPluginError.detectedOverwrittenTempDir
}
// Replace last component, which is exclusive for a target
return tempDir.deletingLastPathComponent().appendingPathComponent("\(otherProduct).build")
}
/// Downloads and prepares an artifact for some thinned target
private func downloadAndPrepareArtifactFor(productName: String, fileKey: String) throws {
let targetTempDir = try buildTempDir(forProductName: productName)
let targetSpecificOrganizer = artifactsOrganizerFactory.build(targetTempDir: targetTempDir)
let artifactPreparationResult = try targetSpecificOrganizer.prepareArtifactLocationFor(fileKey: fileKey)
switch artifactPreparationResult {
case .artifactExists(let artifactDir):
infoLog("Artifact exists locally at \(artifactDir)")
case .preparedForArtifact(let artifactPackage):
infoLog("Downloading artifact to \(artifactPackage)")
try networkClient.download(.artifact(id: fileKey), to: artifactPackage)
let unzippedURL = try targetSpecificOrganizer.prepare(artifact: artifactPackage)
try targetSpecificOrganizer.activate(extractedArtifact: unzippedURL)
}
}
func run(meta: MainArtifactMeta) throws {
onRun()
let allArtifactFileKeys = ThinningPlugin.extractAllProductArtifacts(meta: meta)
// Verify all thinned target's fileKeys are available in the meta
let artifactToFetchFileKeys = allArtifactFileKeys.filter { key, _ in
thinnedTargets.contains(key)
}
let missingCachedTargets = Set(thinnedTargets).subtracting(allArtifactFileKeys.keys)
guard missingCachedTargets.isEmpty else {
let missingTargets = Array(missingCachedTargets)
let rawError = ThinningConsumerPrebuildPluginError.missingCachedTarget(missingTargets: missingTargets)
// Thin project requires all artifacts to be available locally - has to fail immediately
throw PluginError.unrecoverableError(rawError)
}
for (productName, fileKey) in artifactToFetchFileKeys {
worker.appendAction {
try self.downloadAndPrepareArtifactFor(productName: productName, fileKey: fileKey)
}
}
if case .errors(let errors) = worker.waitForResult() {
let rawError = ThinningConsumerPrebuildPluginError.failedPreparation(underlyingErrors: errors)
// Thin project requires all artifacts to be available locally - has to fail immediately
throw PluginError.unrecoverableError(rawError)
}
}
}
@@ -0,0 +1,128 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ThinningCreatorPluginError: Error {
/// Consistency error: a target with enabled XCRemoteCache doesn't contain a single artifact product. Make sure
/// the DerivedData directory is cleared before a build
case noSingleTargetArtifactsGenerated(rootDir: URL)
}
/// Plugin that includes fileKeys of all cached targets in a target meta
/// If scans all directories in the DerivedData to find targets that recently prepared and uploaded artifacts to the
/// remote cache storage. It is important to enabled that plugin only for a target that is build as a last step of the
/// building process (so it can find all relevant build products in DerivedData) for each "configuration+arch" pair
/// Warning! This plugin assumes that producer's DerivedData are always cleaned before a build
class ThinningCreatorPlugin: ArtifactCreatorPlugin {
private let targetTempDir: URL
private let modeMarkerPath: String
private let dirScanner: DirScanner
/// Default Initializer
/// - Parameter targetTempDir: Location of current target-specific temp dir (TARGET_TEMP_DIR)
/// - Parameter modeMarkerPath: path of maker file that informs if a given target can reuse remote artifacts
/// - Parameter dirScanner: scanner to access disk and read files and directories hierarchy
init(targetTempDir: URL, modeMarkerPath: String, dirScanner: DirScanner) {
self.targetTempDir = targetTempDir
self.modeMarkerPath = modeMarkerPath
self.dirScanner = dirScanner
}
let customPathsRemapper: DependenciesRemapper? = nil
func extraMetaKeys(_ meta: MainArtifactMeta) throws -> [String: String] {
// Navigate to the root targetTempDir of all build products (for a specific Configuration+architecture)
let allTargetsTempDirRoot = targetTempDir.deletingLastPathComponent()
// iterate all temp directories to find generated and uploaded artifacts. We assume that the DerivedData
// was emptied before a build so all generated .zip files correspond to a current build
let allURLs = try dirScanner.items(at: allTargetsTempDirRoot)
struct TargetTuple {
let targetName: String
let fileKey: String
}
let uploadedTargetArtifacts = try allURLs.compactMap { tempDir -> TargetTuple? in
let potentialArtifacts = try findTargetPackageZip(tempDir: tempDir)
guard !potentialArtifacts.isEmpty else {
// there is no generated *.zip file, so given target didn't create an artifact - it could be
// just a helper target (like the target we integrate this plugin with)
return nil
}
// Find {{fileKey}} based on the .zip file basename
guard potentialArtifacts.count == 1 else {
throw ThinningCreatorPluginError.noSingleTargetArtifactsGenerated(
rootDir: tempDir
)
}
let fileKey = potentialArtifacts[0].deletingPathExtension().lastPathComponent
// Taking target name from tempDir, which has a structures "*.build"
let targetName = tempDir.deletingPathExtension().lastPathComponent
return TargetTuple(targetName: targetName, fileKey: fileKey)
}
// Build a dictionary that will be appended to the meta with a format:
// {
// "thinning_TargetName1": "ab2331a",
// "thinning_TargetName2": "23a2b1b"
// }
let extraKeysTuples = uploadedTargetArtifacts
.map { ("\(ThinningPlugin.fileKeyPrefix)\($0.targetName)", $0.fileKey) }
return Dictionary(uniqueKeysWithValues: extraKeysTuples)
}
private func findTargetPackageZip(tempDir: URL) throws -> [URL] {
// Producer mode:
// All targets that uploaded their artifacts, have it placed in the
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
// ProducerFast mode:
// If a target reused already existing artifact, it still has `$(TARGET_TEMP_DIR)/rc.enabled` marker file
// and the reused zip is placed in:
// `$(TARGET_TEMP_DIR)/xccache/{{fileKey}}.zip` location
let targetEnabledMarker = tempDir.appendingPathComponent(modeMarkerPath)
let targetReusedArtifactRootDir = tempDir.appendingPathComponent("xccache")
let targetGeneratedArtifactRootDir = tempDir
.appendingPathComponent("xccache")
.appendingPathComponent("produced")
let pathToDirWithZipArtifacts: URL
// try the `.producerFast` scenario first (the artifact was not locally
// generated but just reused from the remote cache)
if try dirScanner.itemType(atPath: targetEnabledMarker.path) == ItemType.file {
pathToDirWithZipArtifacts = targetReusedArtifactRootDir
} else {
// cover a case when a target was build locally and an artifact
// has just been created (locally)
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
return []
}
pathToDirWithZipArtifacts = targetGeneratedArtifactRootDir
}
let allFilesProduced = try dirScanner.items(at: pathToDirWithZipArtifacts)
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
return allArtifacts
}
func artifactToUpload(main: MainArtifactMeta) throws -> [Artifact] {
return []
}
}
@@ -0,0 +1,87 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Generator that produces all products in the DerivedData's Products locations, using provided disk copier
class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
private let destinationSwiftmodulePaths: [SwiftmoduleFileExtension: URL]
private let modulePathOutput: URL
private let objcHeaderOutput: URL
private let diskCopier: DiskCopier
init(
modulePathOutput: URL,
objcHeaderOutput: URL,
diskCopier: DiskCopier
) {
self.modulePathOutput = modulePathOutput
let modulePathBasename = modulePathOutput.deletingPathExtension()
let modulePathDir = modulePathOutput.deletingLastPathComponent()
let moduleName = modulePathBasename.lastPathComponent
// all swiftmodule-related should be located next to the ".swiftmodule"
// except of '.swiftsourceinfo', which should be placed in 'Project' dir
destinationSwiftmodulePaths = Dictionary(
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
.map { ext, _ in
switch ext {
case .swiftsourceinfo:
let dest = modulePathDir.appendingPathComponent("Project")
.appendingPathComponent(moduleName)
.appendingPathExtension(ext.rawValue)
return (ext, dest)
default:
return (ext, modulePathBasename.appendingPathExtension(ext.rawValue))
}
}
)
self.objcHeaderOutput = objcHeaderOutput
self.diskCopier = diskCopier
}
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> SwiftcProductsGeneratorOutput {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
let dest = destinationSwiftmodulePaths[ext]
guard let destination = dest else {
throw DiskSwiftcProductsGeneratorError.unknownSwiftmoduleFile
}
do {
// Move cached .swiftmodule to the expected location
try diskCopier.copy(file: url, destination: destination)
} catch {
if case .required = SwiftmoduleFileExtension.SwiftmoduleExtensions[ext] {
throw error
} else {
infoLog("Optional .\(ext) file not found in the artifact at: \(destination.path)")
}
}
}
// Build parent dir of the .swiftmodule file that contains a module
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
objcHeaderFile: objcHeaderOutput
)
}
}
@@ -0,0 +1,40 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Shared logic between thinning plugin producers and consumers
enum ThinningPlugin {
/// Prefix of the meta keys that correspond to the Thinning Plugin
static let fileKeyPrefix = "thinning_"
/// Finds all artifact fileKeys from the thinned artifact meta
/// Returns a dictionary with Product names keys and aritfact fileKey values
static func extractAllProductArtifacts(meta: MainArtifactMeta) -> [String: String] {
let rawKeys = meta.pluginsKeys
let filteredArtifacts = rawKeys.compactMap { key, value -> (String, String)? in
guard key.hasPrefix(fileKeyPrefix) else {
return nil
}
return (String(key.dropFirst(fileKeyPrefix.count)), value)
}
return Dictionary(uniqueKeysWithValues: filteredArtifacts)
}
}
@@ -0,0 +1,42 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Abstract class for consumer's consumer and producer plugins
class ThinningConsumerPlugin {
private var wasRun: Bool = false
deinit {
// initialised but never run plugin suggests that standard target fallbacks to the local development
// and DerivedData still misses build artifacts
guard wasRun else {
let errorMessage = """
\(type(of: self)) plugin has never been run, thinning cannot be supported. Verify you \
have active network connection to the remote cache server or fallback to the non-thinned mode.
"""
exit(1, errorMessage)
}
}
/// called when plugin is run
func onRun() {
wasRun = true
}
}
@@ -0,0 +1,84 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Moves all swift products files from the artifact to the products dir and generate fingerprint overrides
/// In a standard flow, moving all files is done automatically by Xcode, but for a thinning flow,
/// we need to put all .swiftmodule, .swiftdoc to the desired location in DerivedData's Products location manually
protocol SwiftProductsOrganizer {
func syncProducts(fingerprint: String) throws
}
/// Swift products organizer that generates swift products from an unzipped artifact
class UnzippedArtifactSwiftProductsOrganizer: SwiftProductsOrganizer {
private let arch: String
private let moduleName: String
private let artifactLocation: URL
private let productsGenerator: SwiftcProductsGenerator
private let fingerprintSyncer: FingerprintSyncer
/// Default initializer
/// - Parameters:
/// - arch: the architecture for which the the artifact was generated
/// - moduleName: name of the module
/// - artifactLocation: a location of the prepared(unzipped) artifact
/// - productsGenerator: a generator that will move files to the desired location
/// - fingerprintSyncer: a syncer to decorate swift products with a figerprint override
init(
arch: String,
moduleName: String,
artifactLocation: URL,
productsGenerator: SwiftcProductsGenerator,
fingerprintSyncer: FingerprintSyncer
) {
self.arch = arch
self.moduleName = moduleName
self.artifactLocation = artifactLocation
self.productsGenerator = productsGenerator
self.fingerprintSyncer = fingerprintSyncer
}
func syncProducts(fingerprint: String) throws {
// Zipped artifact contains *.swiftmodule file placed in "swiftmodule/{{arch}}/{{moduleName}}.swiftmodule"
let artifactSwiftmoduleDir = artifactLocation.appendingPathComponent("swiftmodule").appendingPathComponent(arch)
let artifactSwiftmoduleBase = artifactSwiftmoduleDir.appendingPathComponent(moduleName)
let artifactSwiftmoduleFiles = Dictionary(
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
.map { ext, _ in
(ext, artifactSwiftmoduleBase.appendingPathExtension(ext.rawValue))
}
)
// -Swift.h is placed in "include/{{arch}}/{{moduleName}}/{{moduleName}-Swift.h" location
let artifactSwiftModuleObjCFile = artifactLocation
.appendingPathComponent("include")
.appendingPathComponent(arch)
.appendingPathComponent(moduleName)
.appendingPathComponent("\(moduleName)-Swift.h")
let generatedModule = try productsGenerator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
try fingerprintSyncer.decorate(sourceDir: generatedModule.swiftmoduleDir, fingerprint: fingerprint)
try fingerprintSyncer.decorate(file: generatedModule.objcHeaderFile, fingerprint: fingerprint)
}
}
@@ -0,0 +1,269 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum PostbuildError: Error {
/// Called when trying to perform the postbuild even the remote cache is disabled
case disabledCache
}
/// Performs postbuilds actions:
/// * copies fingerprint overrides from a cache or generates these if a target was built from sources
/// * uploads an artifact to the remote server (if the producer mode is ON)
class Postbuild {
private let context: PostbuildContext
private let networkClient: RemoteNetworkClient
private let remapper: DependenciesRemapper
private let fingerprintAccumulator: ContextAwareFingerprintAccumulator
private let artifactsOrganizer: ArtifactOrganizer
private let artifactCreator: ArtifactCreator
private let fingerprintSyncer: FingerprintSyncer
private let dependenciesReader: DependenciesReader
private let dependencyProcessor: DependencyProcessor
private let fingerprintOverrideManager: FingerprintOverrideManager
private let dSYMOrganizer: DSYMOrganizer
private let modeController: CacheModeController
private let metaReader: MetaReader
private let metaWriter: MetaWriter
private let creatorPlugins: [ArtifactCreatorPlugin]
private let consumerPlugins: [ArtifactConsumerPostbuildPlugin]
init(
context: PostbuildContext,
networkClient: RemoteNetworkClient,
remapper: DependenciesRemapper,
fingerprintAccumulator: ContextAwareFingerprintAccumulator,
artifactsOrganizer: ArtifactOrganizer,
artifactCreator: ArtifactCreator,
fingerprintSyncer: FingerprintSyncer,
dependenciesReader: DependenciesReader,
dependencyProcessor: DependencyProcessor,
fingerprintOverrideManager: FingerprintOverrideManager,
dSYMOrganizer: DSYMOrganizer,
modeController: CacheModeController,
metaReader: MetaReader,
metaWriter: MetaWriter,
creatorPlugins: [ArtifactCreatorPlugin],
consumerPlugins: [ArtifactConsumerPostbuildPlugin]
) {
self.context = context
self.networkClient = networkClient
self.remapper = remapper
self.fingerprintAccumulator = fingerprintAccumulator
self.artifactsOrganizer = artifactsOrganizer
self.artifactCreator = artifactCreator
self.fingerprintSyncer = fingerprintSyncer
self.dependenciesReader = dependenciesReader
self.dependencyProcessor = dependencyProcessor
self.fingerprintOverrideManager = fingerprintOverrideManager
self.dSYMOrganizer = dSYMOrganizer
self.modeController = modeController
self.metaReader = metaReader
self.metaWriter = metaWriter
self.creatorPlugins = creatorPlugins
self.consumerPlugins = consumerPlugins
}
private func readMeta() throws -> MainArtifactMeta {
guard case .available(commit: let remoteCommit) = context.remoteCommit else {
throw PostbuildError.disabledCache
}
// Fetch meta from remote side - it should already be in the local cache, triggered by prebuild
let metaData = try networkClient.fetch(.meta(commit: remoteCommit))
return try metaReader.read(data: metaData)
}
/// Performs all extra actions for the consumer scenario
/// 1. Moves all fingerprint overrides from a cache dir to the product location
/// 2. Moves all optional dSYMs to the product location
public func performBuildCompletion() throws {
// artifact filekey is equivalent to context specific fingerprint of its content
let contextSpecificFingerprint = try artifactsOrganizer.getActiveArtifactFilekey()
try generateFingerprintOverrides(contextSpecificFingerprint: contextSpecificFingerprint)
let localArtifactLocation = artifactsOrganizer.getActiveArtifactLocation()
try dSYMOrganizer.syncDSYM(artifactPath: localArtifactLocation)
// Call consumer plugins (if any)
guard !consumerPlugins.isEmpty else {
// quit early to not unnecessary generate meta struct
return
}
let meta = try readMeta()
try consumerPlugins.forEach { plugin in
try plugin.run(meta: meta)
}
}
public func performBuildCleanup() throws {
try dSYMOrganizer.cleanup()
}
/// Deletes fingerprint overrides (if already set)
public func deleteFingerprintOverrides() throws {
try generateFingerprintOverrides(contextSpecificFingerprint: nil)
}
/// Generates fingerprint overrides in the target product location, based on all files used in the compilation
public func generateFingerprintOverrides() throws {
// Compute a local fingerprint and decorate the .swiftmodule files
let dependencies = try generateDependencies()
let fingerprint = try generateFingerprint(dependencies)
try generateFingerprintOverrides(contextSpecificFingerprint: fingerprint.contextSpecific)
}
/// Uploads only a meta to the remote server - useful when the file artifact (.zip) already exists on a remote
/// server and only a meta for a current commit sha has to be uploaded
public func performMetaUpload(meta: MainArtifactMeta, for commit: String) throws {
// Reset plugins keys as these are unique to each
var meta = meta
meta.pluginsKeys = [:]
meta = try creatorPlugins.reduce(meta) { prevMeta, plugin in
var meta = prevMeta
// add extra keys from the plugin. A plugin overrides previously defined keys in case of duplication
meta.pluginsKeys = try meta.pluginsKeys.merging(plugin.extraMetaKeys(prevMeta), uniquingKeysWith: { $1 })
return meta
}
let metaPath = try metaWriter.write(meta, locationDir: context.targetTempDir)
try networkClient.uploadSynchronously(metaPath, as: .meta(commit: commit))
}
/// Builds an artifact package and uploads it to the remote server
public func performBuildUpload(for commit: String) throws {
let dependencies = try generateDependencies()
let localFingerprint = try generateFingerprint(dependencies)
// Filekey has to be unique for the context to not mix builds Debug/Release, iphonesimulator/iphoneos etc
let fileKey = localFingerprint.contextSpecific
// Replace all local paths to the generic ones (e.g. $SRCROOT)
let remappers = [remapper] + creatorPlugins.compactMap(\.customPathsRemapper)
let remapper = DependenciesRemapperComposite(remappers)
let abstractFingerprintFiles = try remapper.replace(localPaths: dependencies.map(\.path))
// TODO: use `inputs` read by dependenciesReader
var meta = MainArtifactMeta(
dependencies: abstractFingerprintFiles,
fileKey: fileKey,
rawFingerprint: localFingerprint.raw,
generationCommit: commit,
targetName: context.targetName,
configuration: context.configuration,
platform: context.platform,
xcode: context.xcodeBuildNumber,
inputs: [],
pluginsKeys: [:]
)
meta = try creatorPlugins.reduce(meta) { prevMeta, plugin in
var meta = prevMeta
// add extra keys from the plugin. A plugin overrides previously defined keys in case of duplication
meta.pluginsKeys = try meta.pluginsKeys.merging(plugin.extraMetaKeys(prevMeta), uniquingKeysWith: { $1 })
return meta
}
// If a module has been built, try to decorate it with a fingerprint override
try generateFingerprintOverrides(contextSpecificFingerprint: localFingerprint.contextSpecific)
// Require that dSYM is generated to include in the artifact
_ = try dSYMOrganizer.relevantDSYMLocation()
let mainArtifact = try artifactCreator.createArtifact(artifactKey: fileKey, meta: meta)
// Send artifact packages with a binary (+provided by plugins) first
// In case of a failure, don't upload meta to not mislead a consumer that the artifact is available
let artifactsToUpload = try creatorPlugins.reduce([mainArtifact]) { prevArtifacts, plugin in
try prevArtifacts + plugin.artifactToUpload(main: meta)
}
try artifactsToUpload.forEach { artifact in
try networkClient.uploadSynchronously(artifact.package, as: .artifact(id: artifact.id))
}
try networkClient.uploadSynchronously(mainArtifact.meta, as: .meta(commit: commit))
}
public func controlNextRetrigger(executableURL: URL) throws {
// If no rc.enabled is present, we disable the Postbuild Build Phase
guard try modeController.isEnabled() else {
try modeController.disable()
return
}
// Instruct Xcode to retrigger that phase if executable has changed so fingerprint override(s) should be updated
// TODO: consider retriggering a phase also when any of the input files has changed
try modeController.enable(allowedInputFiles: [], dependencies: [executableURL])
}
/// Reads all relevant dependencies (e.g. Xcode-embedded dependencies are skipped)
private func generateDependencies() throws -> [URL] {
let dependencies = try dependenciesReader.findDependencies().map(URL.init(fileURLWithPath:))
let processedDependencies = dependencyProcessor.process(dependencies)
let fingerprintFiles = processedDependencies.map(fingerprintOverrideManager.getFingerprintFile)
return fingerprintFiles.map { $0.url }
}
private func generateFingerprint(_ files: [URL]) throws -> Fingerprint {
fingerprintAccumulator.reset()
for file in files {
do {
try fingerprintAccumulator.append(file)
} catch FingerprintAccumulatorError.missingFile(let content) {
printWarning("File at \(content.path) was not found on disc. Calculating fingerprint without it.")
}
}
return try fingerprintAccumulator.generate()
}
/// Generates fingerprint overrides for the current module
private func generateFingerprintOverrides(contextSpecificFingerprint: ContextSpecificFingerprint?) throws {
// generate fingperint override only for modules (no need for ObjC targets)
guard let modulename = context.moduleName else {
return
}
try decorateSwiftmodule(modulename, contextSpecificFingerprint)
}
// Add extra fingerprint override to a generated module
private func decorateSwiftmodule(_ modulename: String, _ contextSpecificFingerprint: ContextSpecificFingerprint?) throws {
let moduleSwiftProductURL = context.productsDir
.appendingPathComponent(context.modulesFolderPath)
.appendingPathComponent("\(modulename).swiftmodule")
let objcHeaderSwiftProductURL = context.derivedSourcesDir
.appendingPathComponent("\(modulename)-Swift.h")
// This header is obly valid if building a frameworks
let objcHeaderSwiftPublicPathURL = context.publicHeadersFolderPath?
.appendingPathComponent("\(modulename)-Swift.h")
if let fingerprint = contextSpecificFingerprint {
try fingerprintSyncer.decorate(
sourceDir: moduleSwiftProductURL,
fingerprint: fingerprint
)
try fingerprintSyncer.decorate(
file: objcHeaderSwiftProductURL,
fingerprint: fingerprint
)
if let objcPublic = objcHeaderSwiftPublicPathURL {
try fingerprintSyncer.decorate(
file: objcPublic,
fingerprint: fingerprint
)
}
} else {
try fingerprintSyncer.delete(sourceDir: moduleSwiftProductURL)
try fingerprintSyncer.delete(sourceDir: objcHeaderSwiftProductURL)
if let objcPublic = objcHeaderSwiftPublicPathURL {
try fingerprintSyncer.delete(file: objcPublic)
}
}
}
}
@@ -0,0 +1,153 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum MachOType: String, Codable {
case staticLib
case dynamicLib = "mh_dylib"
case executable = "mh_execute"
case bundle = "mh_bundle"
case relocatable = "mh_object"
case unknown
}
enum PostbuildContextError: Error {
/// URL address is not a valid URL
case invalidAddress(String)
/// ARCHS env does not contain any architecture to build
case missingArchitecture
}
public struct PostbuildContext {
var mode: Mode
var targetName: String
var targetTempDir: URL
var derivedFilesDir: URL
/// Location where all compilation outputs (.o) are placed
var compilationTempDir: URL
var configuration: String
var platform: String
var productsDir: URL
var moduleName: String?
/// Path to the *.swiftmodule directory (irrelevant when `module` is nil). Rrelative to `productsDir`
var modulesFolderPath: String
var executablePath: String
var srcRoot: URL
var xcodeDir: URL
var xcodeBuildNumber: String
/// Location of the file that specifies remote commit sha
var remoteCommitLocation: URL
/// Commit sha of the commit to use remote cache
var remoteCommit: RemoteCommitInfo
var recommendedCacheAddress: URL
/// All cache adresses to upload cache artifacts (for a producer)
var cacheAddresses: [URL]
/// Root directory where all statistics are stored
var statsLocation: URL
/// Force using the cached artifact and never fallback to the local compilation
var forceCached: Bool
var machOType: MachOType
var wasDsymGenerated: Bool
var dSYMPath: URL
// building architecture. Used to find all dependencies from *.d files
// Warning: if two architectures are built (e.g. for disabled "Build Archive
// Architecture Only"), a first architecture one is picked
let arch: String
let builtProductsDir: URL
/// Location to the product bundle. Can be nil for libraries
let bundleDir: URL?
var derivedSourcesDir: URL
/// List of all targets to downloaded from the thinning aggregation target
var thinnedTargets: [String]
/// Action type: build, indexbuild etc
var action: BuildActionType
let modeMarkerPath: String
/// location of the json file that define virtual files system overlay
/// (mappings of the virtual location file -> local file path)
let overlayHeadersPath: URL
/// Regexes of files that should not be included in the dependency list
let irrelevantDependenciesPaths: [String]
/// Location of public headers. Not always available (e.g. static libraries)
var publicHeadersFolderPath: URL?
/// XCRemoteCache is explicitly disabled
let disabled: Bool
}
extension PostbuildContext {
// swiftlint:disable:next function_body_length
init(_ config: XCRemoteCacheConfig, env: [String: String]) throws {
mode = config.mode
let targetNameValue: String = try env.readEnv(key: "TARGET_NAME")
targetName = targetNameValue
targetTempDir = try env.readEnv(key: "TARGET_TEMP_DIR")
derivedFilesDir = try env.readEnv(key: "DERIVED_FILE_DIR")
let archs: [String] = try env.readEnv(key: "ARCHS").split(separator: " ").map(String.init)
guard let firstArch = archs.first, !firstArch.isEmpty else {
throw PostbuildContextError.missingArchitecture
}
arch = firstArch
let variant: String = try env.readEnv(key: "CURRENT_VARIANT")
compilationTempDir = try env.readEnv(key: "OBJECT_FILE_DIR_\(variant)").appendingPathComponent(arch)
configuration = try env.readEnv(key: "CONFIGURATION")
platform = try env.readEnv(key: "PLATFORM_NAME")
xcodeBuildNumber = try env.readEnv(key: "XCODE_PRODUCT_BUILD_VERSION")
productsDir = try env.readEnv(key: "TARGET_BUILD_DIR")
moduleName = env.readEnv(key: "PRODUCT_MODULE_NAME")
modulesFolderPath = env.readEnv(key: "MODULES_FOLDER_PATH") ?? ""
executablePath = try env.readEnv(key: "EXECUTABLE_PATH")
srcRoot = try env.readEnv(key: "SRCROOT")
xcodeDir = try env.readEnv(key: "DEVELOPER_DIR")
remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: srcRoot)
remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
guard let address = URL(string: config.recommendedCacheAddress) else {
throw PostbuildContextError.invalidAddress(config.recommendedCacheAddress)
}
recommendedCacheAddress = address
statsLocation = URL(fileURLWithPath: config.statsDir.expandingTildeInPath, relativeTo: srcRoot)
cacheAddresses = try config.cacheAddresses.map(URL.build)
forceCached = !config.focusedTargets.isEmpty && !config.focusedTargets.contains(targetNameValue)
machOType = try MachOType(rawValue: env.readEnv(key: "MACH_O_TYPE")) ?? .unknown
wasDsymGenerated = try env.readEnv(key: "DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT")
dSYMPath = try env.readEnv(key: "DWARF_DSYM_FOLDER_PATH")
.appendingPathComponent(env.readEnv(key: "DWARF_DSYM_FILE_NAME"))
builtProductsDir = try env.readEnv(key: "BUILT_PRODUCTS_DIR")
if let contentsFolderPath: String = env.readEnv(key: "CONTENTS_FOLDER_PATH") {
bundleDir = productsDir.appendingPathComponent(contentsFolderPath)
} else {
bundleDir = nil
}
derivedSourcesDir = try env.readEnv(key: "DERIVED_SOURCES_DIR")
let thinFocusedTargetsString: String = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS") ?? ""
thinnedTargets = thinFocusedTargetsString.split(separator: ",").map(String.init)
action = (try? BuildActionType(rawValue: env.readEnv(key: "ACTION"))) ?? .unknown
modeMarkerPath = config.modeMarkerPath
/// Note: The file has yaml extension, even it is in the json format
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
irrelevantDependenciesPaths = config.irrelevantDependenciesPaths
let publicHeadersPathEnv: String? = env.readEnv(key: "PUBLIC_HEADERS_FOLDER_PATH")
if let publicHeadersPath = publicHeadersPathEnv, publicHeadersPathEnv != "/usr/local/include" {
// '/usr/local/include' is a value of PUBLIC_HEADERS_FOLDER_PATH when no public headers are automatically
// generated and it is up to a project configuration to place it in a common location (e.g. static library)
publicHeadersFolderPath = builtProductsDir.appendingPathComponent(publicHeadersPath)
}
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
}
}
@@ -0,0 +1,344 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
// swiftlint:disable type_body_length
/// Checks current mode from a configuration and based on that:
/// * triggers build completion
/// * triggers uploading artifacts to the server for a 'producer' mode
public class XCPostbuild {
public init() {}
// swiftlint:disable:next function_body_length cyclomatic_complexity
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: PostbuildContext
let cacheHitLogger: CacheHitLogger
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PostbuildContext(config, env: env)
updateProcessTag(context.targetName)
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
ExclusiveFileCounter(ExclusiveFile(file, mode: .override), countersCount: count)
}
let statsLogger = try FileStatsLogger(
statsLocation: context.statsLocation,
counterFactory: counterFactory,
fileManager: fileManager
)
cacheHitLogger = ActionSpecificCacheHitLogger(action: context.action, statsLogger: statsLogger)
} catch {
exit(1, "FATAL: Postbuild initialization failed with error: \(error)")
}
// Postbuild cannot disable marker, so NoopMarkerWriter used instead of a real file writer
let modeController = PhaseCacheModeController(
tempDir: context.targetTempDir,
mergeCommitFile: context.remoteCommitLocation,
phaseDependencyPath: config.postbuildDiscoveryPath,
markerPath: config.modeMarkerPath,
forceCached: context.forceCached,
dependenciesWriter: FileDependenciesWriter.init,
dependenciesReader: FileDependenciesReader.init,
markerWriter: NoopMarkerWriter.init,
fileManager: fileManager
)
do {
// Initialize dependencies
let primaryGitBranch = GitBranch(repoLocation: config.primaryRepo, branch: config.primaryBranch)
let gitClient = GitClientImpl(repoRoot: config.repoRoot, primary: primaryGitBranch, shell: shellGetStdout)
let envsRemapper = try PathDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
envs: env,
customMappings: config.outOfBandMappings
)
let envFingerprint = try EnvironmentFingerprintGenerator(
configuration: config,
env: env,
generator: FingerprintAccumulatorImpl(algorithm: MD5Algorithm(), fileManager: fileManager)
).generateFingerprint()
let fingerprintFilesGenerator = FingerprintAccumulatorImpl(
algorithm: MD5Algorithm(),
fileManager: fileManager
)
let fingerprintGenerator = FingerprintGenerator(
envFingerprint: envFingerprint,
fingerprintFilesGenerator,
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(
targetTempDir: context.targetTempDir,
// In postbuild we don't preprocess artifacts (no need to replace path placeholders)
artifactProcessors: [],
fileManager: fileManager
)
let metaWriter = JsonMetaWriter(fileWriter: fileManager, pretty: config.prettifyMetaFiles)
let fileRemapper = TextFileDependenciesRemapper(remapper: envsRemapper, fileAccessor: fileManager)
let artifactCreator = BuildArtifactCreator(
buildDir: context.productsDir,
tempDir: context.targetTempDir,
executablePath: context.executablePath,
moduleName: context.moduleName,
modulesFolderPath: context.modulesFolderPath,
dSYMPath: context.dSYMPath,
metaWriter: metaWriter,
artifactProcessor: UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager),
fileManager: fileManager
)
let dirAccessor = DirAccessorComposer(
fileAccessor: LazyFileAccessor(fileAccessor: fileManager),
dirScanner: fileManager
)
let fingerprintSyncer = FileFingerprintSyncer(
fingerprintOverrideExtension: config.fingerprintOverrideExtension,
dirAccessor: dirAccessor,
extensions: config.productFilesExtensionsWithContentOverride
)
let sessionFactory = DefaultURLSessionFactory(config: config)
var awsV4Signature: AWSV4Signature?
if !config.AWSAccessKey.isEmpty {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
)
}
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.uploadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
let remoteNetworkClient = try RemoteNetworkClientAbstractFactory(
mode: context.mode,
downloadStreamURL: context.recommendedCacheAddress,
upstreamStreamURL: context.cacheAddresses,
uploadBatchSize: config.uploadBatchSize,
networkClient: networkClient,
urlBuilderFactory: {
try URLBuilderImpl(
address: $0,
env: env,
envFingerprint: envFingerprint,
schemaVersion: config.schemaVersion
)
}
).build()
let fileReaderFactory: (URL) -> DependenciesReader = {
FileDependenciesReader($0, accessor: fileManager)
}
let dependenciesReader = TargetDependenciesReader(
context.compilationTempDir,
fileDependeciesReaderFactory: fileReaderFactory,
dirScanner: fileManager
)
var remappers: [DependenciesRemapper] = []
if !config.disableVFSOverlay {
// As the PostbuildContext assumes file location and filename (`all-product-headers.yaml`)
// do not fail in case of a missing headers overlay file. In the future, all overlay files could be
// captured from the swiftc invocation similarly is stored in the `history.compile`
// for the consumer mode
let overlayReader = JsonOverlayReader(
context.overlayHeadersPath,
mode: .bestEffort,
fileReader: fileManager
)
let overlayRemapper = OverlayDependenciesRemapper(
overlayReader: overlayReader
)
remappers.append(overlayRemapper)
}
remappers.append(envsRemapper)
let pathRemapper = DependenciesRemapperComposite(remappers)
let dependencyProcessor = DependencyProcessorImpl(
xcode: context.xcodeDir,
product: context.productsDir,
source: context.srcRoot,
intermediate: context.targetTempDir,
derivedFiles: context.derivedFilesDir,
bundle: context.bundleDir,
skippedRegexes: context.irrelevantDependenciesPaths
)
// Override fingerprints for all produced '.swiftmodule' files
let fingerprintOverrideManager = FingerprintOverrideManagerImpl(
overridingFileExtensions: config.productFilesExtensionsWithContentOverride,
fingerprintOverrideExtension: config.fingerprintOverrideExtension,
fileManager: fileManager
)
let binaryURL = context.productsDir.appendingPathComponent(context.executablePath)
let dSYMOrganizer = DynamicDSYMOrganizer(
productURL: binaryURL,
machOType: context.machOType,
dSYMPath: context.dSYMPath,
wasDsymGenerated: context.wasDsymGenerated,
fileManager: fileManager,
shellCall: shellCall
)
let metaReader = JsonMetaReader(fileAccessor: fileManager)
var creatorPlugins: [ArtifactCreatorPlugin] = []
var consumerPlugins: [ArtifactConsumerPostbuildPlugin] = []
if config.thinningEnabled {
// Engage all thinning plugins
if context.moduleName == config.thinningTargetModuleName {
switch context.mode {
case .consumer:
// no need to process artifacts in postbuild. Prebuild has already
// run a processor on a downloaded artifact
let artifactOrganizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
processors: [],
fileManager: fileManager
)
let swiftProductsLocationProvider =
DefaultSwiftProductsLocationProvider(
builtProductsDir: context.builtProductsDir,
derivedSourcesDir: context.derivedSourcesDir
)
let swiftOrganizerFactory = ThinningConsumerUnzippedArtifactSwiftProductsOrganizerFactory(
arch: context.arch,
productsLocationProvider: swiftProductsLocationProvider,
fingerprintSyncer: fingerprintSyncer,
diskCopier: CopyDiskCopier(fileManager: fileManager)
)
let swiftProductsArchitecturesRecognizer = DefaultSwiftProductsArchitecturesRecognizer(
dirAccessor: fileManager
)
let thinningPlugin = ThinningConsumerPostbuildPlugin(
targetTempDir: context.targetTempDir,
builtProductsDir: context.builtProductsDir,
productModuleName: config.thinningTargetModuleName,
arch: context.arch,
thinnedTargets: context.thinnedTargets,
artifactOrganizerFactory: artifactOrganizerFactory,
swiftProductOrganizerFactory: swiftOrganizerFactory,
artifactInspector: DefaultArtifactInspector(dirAccessor: fileManager),
swiftProductsArchitecturesRecognizer: swiftProductsArchitecturesRecognizer,
diskCopier: HardLinkDiskCopier(fileManager: fileManager),
worker: DispatchGroupParallelizationWorker(qos: .userInitiated)
)
consumerPlugins.append(thinningPlugin)
case .producer, .producerFast:
let thinningPlugin = ThinningCreatorPlugin(
targetTempDir: context.targetTempDir,
modeMarkerPath: context.modeMarkerPath,
dirScanner: fileManager
)
creatorPlugins.append(thinningPlugin)
}
}
}
let postbuildAction = Postbuild(
context: context,
networkClient: remoteNetworkClient,
remapper: pathRemapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: fingerprintSyncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: dependencyProcessor,
fingerprintOverrideManager: fingerprintOverrideManager,
dSYMOrganizer: dSYMOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: creatorPlugins,
consumerPlugins: consumerPlugins
)
// Trigger build completion
if context.disabled {
infoLog("XCRC fully disabled for \(context.targetName), \(context.platform), \(context.configuration)")
// Cutoff the process is disabled, but generate an "empty" list of dependencies
try? modeController.disable()
return
} else if try modeController.isEnabled() {
// Decorate .swiftmodule in the product dir with fingerprint(s) overrides from a cache artifact
try postbuildAction.performBuildCompletion()
} else if context.mode == .consumer {
// Delete previously set overrides - they are no longer valid. The compilation was
// done locally, most likely due to some local change
try postbuildAction.deleteFingerprintOverrides()
}
// Trigger uploading the artifact
switch (context.mode, try modeController.isEnabled(), context.remoteCommit) {
case (.producerFast, true, .available(commit: let commitToReuse)):
// Upload only updated meta. Artifact zip is already on a remote server
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
let metaData = try remoteNetworkClient.fetch(.meta(commit: commitToReuse))
let meta = try metaReader.read(data: metaData)
try postbuildAction.performMetaUpload(meta: meta, for: referenceCommit)
case (.producer, _, _), (.producerFast, _, _):
// Generate artifacts and upload to the remote server for a reference sha
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
try postbuildAction.performBuildUpload(for: referenceCommit)
default:
// Consumer does not upload anything
break
}
let executableURL = context.productsDir.appendingPathComponent(context.executablePath)
try postbuildAction.controlNextRetrigger(executableURL: executableURL)
// Populate stats event for a final RC state
// Doing it in a postmerge, as xcswiftc (and xccc) has a right to disable RC
if try modeController.isEnabled() {
try cacheHitLogger.logHit()
printToUser("Cached build for \(context.targetName) target")
} else {
try postbuildAction.performBuildCleanup()
try cacheHitLogger.logMiss()
// If producers reach this point, there were no issues with publishing
let actionName = context.mode == .consumer ? "Disabled" : "Published"
printToUser("\(actionName) remote cache for \(context.targetName)")
}
} catch PluginError.unrecoverableError(let error) {
exit(1, "\(error)")
} catch {
errorLog("Postbuild step failed with error: \(error)")
if context.mode == .producer {
// Producer cannot gracefully fail to not mark given sha as artifact-redy
exit(1, "Postbuild step failed \(error)")
}
// disable postbuild until the next merge-with-primary
do {
try modeController.disable()
// TODO: consider tracking errors in stats
try cacheHitLogger.logMiss()
printToUser("Disabled remote cache for \(context.targetName)")
} catch {
exit(1, "FATAL: Postbuild finishing failed with error: \(error)")
}
}
}
}
// swiftlint:enable type_body_length
@@ -0,0 +1,123 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum PrebuildResult: Equatable {
case disabled
case incompatible
case compatible(localDependencies: [URL])
}
/// Downloads meta info for a current commit and downloads+unzips the artifact package if fingerprints match
class Prebuild {
private let context: PrebuildContext
private let networkClient: RemoteNetworkClient
private let remapper: DependenciesRemapper
private let fingerprintAccumulator: ContextAwareFingerprintAccumulator
private let artifactsOrganizer: ArtifactOrganizer
private let globalCacheSwitcher: GlobalCacheSwitcher
private let metaReader: MetaReader
private let artifactConsumerPrebuildPlugins: [ArtifactConsumerPrebuildPlugin]
init(
context: PrebuildContext,
networkClient: RemoteNetworkClient,
remapper: DependenciesRemapper,
fingerprintAccumulator: ContextAwareFingerprintAccumulator,
artifactsOrganizer: ArtifactOrganizer,
globalCacheSwitcher: GlobalCacheSwitcher,
metaReader: MetaReader,
artifactConsumerPrebuildPlugins: [ArtifactConsumerPrebuildPlugin]
) {
self.context = context
self.networkClient = networkClient
self.remapper = remapper
self.fingerprintAccumulator = fingerprintAccumulator
self.artifactsOrganizer = artifactsOrganizer
self.globalCacheSwitcher = globalCacheSwitcher
self.metaReader = metaReader
self.artifactConsumerPrebuildPlugins = artifactConsumerPrebuildPlugins
}
// swiftlint:disable:next function_body_length
public func perform() throws -> PrebuildResult {
guard !context.disabled else {
return .disabled
}
guard case .available(let commit) = context.remoteCommit else {
return .incompatible
}
do {
let metaData = try networkClient.fetch(.meta(commit: commit))
let meta = try metaReader.read(data: metaData)
let localDependencies = try remapper.replace(
genericPaths: meta.dependencies
).map(URL.init(fileURLWithPath:))
let localFingerprint = try generateFingerprint(for: localDependencies)
if localFingerprint.raw != meta.rawFingerprint {
if context.forceCached {
printWarning("""
The generated target product is out-of-sync, target sources don't match the XCRemoteCache
generated artifacts that will be used in runtime. Make sure you didn't introduce
any modification of the target or its dependency,
otherwise the generated application may be corrupted.
""")
} else {
infoLog("""
Local fingerprint \(localFingerprint) does not match with remote one \(meta.rawFingerprint).
""")
return .incompatible
}
}
let artifactPreparationResult = try artifactsOrganizer.prepareArtifactLocationFor(fileKey: meta.fileKey)
switch artifactPreparationResult {
case .artifactExists(let artifactDir):
infoLog("Artifact exists locally at \(artifactDir)")
try artifactsOrganizer.activate(extractedArtifact: artifactDir)
case .preparedForArtifact(let artifactPackage):
infoLog("Downloading artifact to \(artifactPackage)")
try networkClient.download(.artifact(id: meta.fileKey), to: artifactPackage)
let unzippedURL = try artifactsOrganizer.prepare(artifact: artifactPackage)
try artifactsOrganizer.activate(extractedArtifact: unzippedURL)
infoLog("Artifact unzipped to \(unzippedURL)")
}
try artifactConsumerPrebuildPlugins.forEach { plugin in
try plugin.run(meta: meta)
}
return .compatible(localDependencies: localDependencies)
} catch PluginError.unrecoverableError(let error) {
exit(1, "\(error)")
} catch NetworkClientError.timeout {
if context.turnOffRemoteCacheOnFirstTimeout {
infoLog("Network timeout observed. Falling back to local builds for all targets.")
try globalCacheSwitcher.disable()
}
throw NetworkClientError.timeout
}
}
public func generateFingerprint(for files: [URL]) throws -> Fingerprint {
try files.forEach(fingerprintAccumulator.append)
return try fingerprintAccumulator.generate()
}
}
@@ -0,0 +1,76 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum PrebuildContextError: Error {
case missingEnv(String)
case invalidAddress(String)
}
public struct PrebuildContext {
let targetTempDir: URL
let productsDir: URL
let moduleName: String?
/// Commit sha of the commit to use remote cache
let remoteCommit: RemoteCommitInfo
/// Location of the file that specifies remote commit sha
let remoteCommitLocation: URL
let recommendedCacheAddress: URL
/// Force using the cached artifact and never fallback to the local compilation
let forceCached: Bool
/// A file that stores a list of all target compilation invocations so far
let compilationHistoryFile: URL
/// If true, any request timeout disables remote cache for all targets
let turnOffRemoteCacheOnFirstTimeout: Bool
/// Name of a target
let targetName: String
/// List of all targets to downloaded from the thinning aggregation target
var thinnedTargets: [String]?
/// location of the json file that define virtual files system overlay
/// (mappings of the virtual location file -> local file path)
let overlayHeadersPath: URL
/// XCRemoteCache is explicitly disabled
let disabled: Bool
}
extension PrebuildContext {
init(_ config: XCRemoteCacheConfig, env: [String: String]) throws {
targetTempDir = try env.readEnv(key: "TARGET_TEMP_DIR")
productsDir = try env.readEnv(key: "BUILT_PRODUCTS_DIR")
moduleName = env.readEnv(key: "PRODUCT_MODULE_NAME")
let srcRoot: URL = try env.readEnv(key: "SRCROOT")
remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: srcRoot)
remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
guard let address = URL(string: config.recommendedCacheAddress) else {
throw PrebuildContextError.invalidAddress(config.recommendedCacheAddress)
}
recommendedCacheAddress = address
let targetName: String = try env.readEnv(key: "TARGET_NAME")
forceCached = !config.focusedTargets.isEmpty && !config.focusedTargets.contains(targetName)
compilationHistoryFile = targetTempDir.appendingPathComponent(config.compilationHistoryFile)
turnOffRemoteCacheOnFirstTimeout = config.turnOffRemoteCacheOnFirstTimeout
self.targetName = targetName
let thinFocusedTargetsString: String? = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS")
thinnedTargets = thinFocusedTargetsString?.split(separator: ",").map(String.init)
/// Note: The file has yaml extension, even it is in the json format
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
}
}
@@ -0,0 +1,228 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public class XCPrebuild {
public init() {}
// swiftlint:disable:next function_body_length
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: PrebuildContext
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PrebuildContext(config, env: env)
updateProcessTag(context.targetName)
} catch {
// Fatal error:
exit(1, "FATAL: Prebuild initialization failed with error: \(error)")
}
// Xcode may call xcprebuild phase even none of compilation files has changed (e.g. when switching between
// simulator versions) and modifying 'mdate' of a marker file unnecessary invalidates compilation steps
// that have to repeat their "use-from-cache" flow
// To not introduce additional overhead, only marker writer file is saved in a lazy mode
let lazyMarkerWriterFactory: (URL, FileManager) -> MarkerWriter = { url, fileManager in
let lazyFileAccessor = LazyFileAccessor(fileAccessor: fileManager)
return FileMarkerWriter(url, fileAccessor: lazyFileAccessor)
}
let globalCacheSwitcher = FileGlobalCacheSwitcher(context.remoteCommitLocation, fileAccessor: fileManager)
let modeController = PhaseCacheModeController(
tempDir: context.targetTempDir,
mergeCommitFile: context.remoteCommitLocation,
phaseDependencyPath: config.prebuildDiscoveryPath,
markerPath: config.modeMarkerPath,
forceCached: context.forceCached,
dependenciesWriter: FileDependenciesWriter.init,
dependenciesReader: FileDependenciesReader.init,
markerWriter: lazyMarkerWriterFactory,
fileManager: fileManager
)
guard config.mode != .producer else {
// Prebuild phase for a producer is noop
// TODO: Consider a note to not adding that prebuildstep to the Xcode target
disableRemoteCache(
modeController: modeController,
errorMessage: "Prebuild step disabled, selected mode: \(config.mode)"
)
exit(0)
}
guard !modeController.shouldDisable(for: context.remoteCommit) else {
// Previous RC runs explicitly disabled using remote cache for that remote sha
// Short-circut early all `xc*` apps until remote commit change
disableRemoteCache(
modeController: modeController,
errorMessage: "Prebuild step was disabled for current commit: \(context.remoteCommit)"
)
exit(0)
}
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
context.compilationHistoryFile,
fileManager: fileManager
)
do {
let envFingerprint = try EnvironmentFingerprintGenerator(
configuration: config,
env: env,
generator: FingerprintAccumulatorImpl(algorithm: MD5Algorithm(), fileManager: fileManager)
).generateFingerprint()
let urlBuilder = try URLBuilderImpl(
address: context.recommendedCacheAddress,
env: env,
envFingerprint: envFingerprint,
schemaVersion: config.schemaVersion
)
let sessionFactory = DefaultURLSessionFactory(config: config)
var awsV4Signature: AWSV4Signature?
if !config.AWSAccessKey.isEmpty {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
)
}
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.downloadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
let cacheURL: URL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
let cacheURLBuilder = LocalURLBuilderImpl(cachePath: cacheURL)
let cacheNetworkClient = CachedNetworkClient(
localURLBuilder: cacheURLBuilder,
client: networkClient,
fileManager: fileManager
)
let client: NetworkClient = config.disableHttpCache ? networkClient : cacheNetworkClient
let remoteNetworkClient = RemoteNetworkClientImpl(client, urlBuilder)
let envsRemapper = try PathDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
envs: env,
customMappings: config.outOfBandMappings
)
var remappers: [DependenciesRemapper] = []
if !config.disableVFSOverlay {
// As PrebuildContext assumes file location and its filename (`all-product-headers.yaml`)
// do not fail in case of a missing headers overlay file
let overlayReader = JsonOverlayReader(
context.overlayHeadersPath,
mode: .bestEffort,
fileReader: fileManager
)
let overlayRemapper = OverlayDependenciesRemapper(
overlayReader: overlayReader
)
remappers.append(overlayRemapper)
}
remappers.append(envsRemapper)
let pathRemapper = DependenciesRemapperComposite(remappers)
let filesFingerprintGenerator = FingerprintAccumulatorImpl(
algorithm: MD5Algorithm(),
fileManager: fileManager
)
let fingerprintGenerator = FingerprintGenerator(
envFingerprint: envFingerprint,
filesFingerprintGenerator,
algorithm: MD5Algorithm()
)
let fileRemapper = TextFileDependenciesRemapper(
remapper: envsRemapper,
fileAccessor: fileManager
)
let artifactProcessor = UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: context.targetTempDir,
artifactProcessors: [artifactProcessor],
fileManager: fileManager
)
let metaReader = JsonMetaReader(fileAccessor: fileManager)
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
if config.thinningEnabled {
if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets {
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
processors: [artifactProcessor],
fileManager: fileManager
)
let aggregationPlugin = ThinningConsumerPrebuildPlugin(
targetName: context.targetName,
tempDir: context.targetTempDir,
thinnedTargets: thinnedTarget,
artifactsOrganizerFactory: organizerFactory,
networkClient: remoteNetworkClient,
worker: DispatchGroupParallelizationWorker(qos: .userInitiated)
)
consumerPlugins.append(aggregationPlugin)
}
}
let prebuildAction = Prebuild(
context: context,
networkClient: remoteNetworkClient,
remapper: pathRemapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
globalCacheSwitcher: globalCacheSwitcher,
metaReader: metaReader,
artifactConsumerPrebuildPlugins: consumerPlugins
)
let actionResult = try prebuildAction.perform()
switch actionResult {
case .incompatible:
infoLog("Remote cache cannot be used")
try modeController.disable()
case .compatible(localDependencies: let dependencies):
// TODO: pass `allowedInputFiles` observed in the build time
try modeController.enable(allowedInputFiles: dependencies, dependencies: dependencies)
case .disabled:
infoLog("XCRemoteCache is explicitly disabled")
try modeController.disable()
}
} catch {
disableRemoteCache(
modeController: modeController,
errorMessage: "Prebuild step failed with error: \(error)"
)
}
compilationHistoryOrganizer.reset()
}
private func disableRemoteCache(modeController: PhaseCacheModeController, errorMessage: String?) {
if let message = errorMessage {
errorLog(message)
}
do {
try modeController.disable()
} catch {
exit(1, "FATAL: Prebuild fallback to source-mode failed with error: \(error)")
}
}
}
@@ -0,0 +1,550 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Builds a cc (clang) command wrapper that noops when cached artifact is used
protocol CCWrapperBuilder {
/// Compiles CC wrapper and places output binary in a destination location
/// - Parameters:
/// - destination: output location of the binary
/// - commitSha: remote commit sha that is currently in use
func compile(to destination: URL, commitSha: String) throws
}
// swiftlint:disable:next type_body_length
class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
private static let AppleGenericVersioningSuffix = "_vers.c"
private let clangCommand: String
private let markerPath: String
private let cachedTargetMockFilename: String
private let prebuildDFilename: String
private let compilationHistoryFilename: String
private let shell: ShellOutFunction
private let fileManager: FileManager
init(
clangCommand: String,
markerPath: String,
cachedTargetMockFilename: String,
prebuildDFilename: String,
compilationHistoryFilename: String,
shellOut: @escaping ShellOutFunction,
fileManager: FileManager
) {
self.clangCommand = clangCommand
self.markerPath = markerPath
self.cachedTargetMockFilename = cachedTargetMockFilename
self.prebuildDFilename = prebuildDFilename
self.compilationHistoryFilename = compilationHistoryFilename
shell = shellOut
self.fileManager = fileManager
}
/// Compiles xccc app and places binary in the destination location
func compile(to destination: URL, commitSha: String) throws {
let compilationFile = fileManager.temporaryDirectory.appendingPathComponent("xccc.c")
let compilationContent = buildWrapperSource(
clangCommand: clangCommand,
markerFilename: markerPath,
commitSha: commitSha
)
fileManager.createFile(
atPath: compilationFile.path,
contents: compilationContent.data(using: .utf8),
attributes: nil
)
infoLog("ClangWrapperBuilder compiles file at \(compilationFile).")
// -O3: optimize for faster execution
let args = [
clangCommand,
"-arch",
"arm64",
"-arch",
"x86_64",
"-O3",
compilationFile.path,
"-o",
destination.path,
]
let compilationOutput = try shell("xcrun", args, URL(fileURLWithPath: "").path, nil)
infoLog("Clang compilation output: \(compilationOutput)")
}
/// Generates source of the cc wrapper
// swiftlint:disable line_length
// swiftlint:disable:next function_body_length
private func buildWrapperSource(clangCommand: String, markerFilename: String, commitSha: String) -> String {
return """
/**
Clang compiler wrapper manages compilation. When a marker file, placed in the `-MF/../../../\(markerFilename)`:
1) is missing - fallback to \(clangCommand)
2) exists - creates empty .o file and creates .d with the same content as a marker
(which is expected to be in the .d format)
3) otherwise, return 1 and prints a message to the error stream.
*/
#include <fcntl.h> /* For system call open */
#include <libgen.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <unistd.h>
/// checks if string str has suffix prefix
int isSuffixed(const char *str, const char *suffix)
{
int suffix_len = strlen(suffix);
return (strlen(str) > suffix_len && !strcmp(str + strlen(str) - suffix_len, suffix));
}
void createFile(const char *path, const char *content)
{
FILE *fp;
fp = fopen(path, "wb");
if (content) {
fwrite(content, 1, strlen(content), fp);
}
fclose(fp);
}
void createEmptyFile(const char *path)
{
createFile(path, NULL);
}
/// Writes empty .dia with no diagonostics messages (no errors, no warnings)
/// Clang implementation: https://clang.llvm.org/doxygen/SerializedDiagnosticPrinter_8cpp_source.html
void createPlaceholderDiaFile(const char *path)
{
// empty .dia file dumped using `xxd --include empty_sample.dia`
unsigned char empty_dia[] = {
0x44, 0x49, 0x41, 0x47, 0x01, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
0x07, 0x01, 0xb2, 0x40, 0xb4, 0x42, 0x39, 0xd0, 0x43, 0x38, 0x3c, 0x20,
0x81, 0x2d, 0x94, 0x83, 0x3c, 0xcc, 0x43, 0x3a, 0xbc, 0x83, 0x3b, 0x1c,
0x04, 0x88, 0x62, 0x80, 0x40, 0x71, 0x10, 0x24, 0x0b, 0x04, 0x29, 0xa4,
0x43, 0x38, 0x9c, 0xc3, 0x43, 0x22, 0x90, 0x42, 0x3a, 0x84, 0xc3, 0x39,
0xa4, 0x82, 0x3b, 0x98, 0xc3, 0x3b, 0x3c, 0x24, 0xc3, 0x2c, 0xc8, 0xc3,
0x38, 0xc8, 0x42, 0x38, 0xb8, 0xc3, 0x39, 0x94, 0xc3, 0x03, 0x52, 0x8c,
0x42, 0x38, 0xd0, 0x83, 0x2b, 0x84, 0x43, 0x3b, 0x94, 0xc3, 0x43, 0x42,
0x90, 0x42, 0x3a, 0x84, 0xc3, 0x39, 0x98, 0x02, 0x3b, 0x84, 0xc3, 0x39,
0x3c, 0x24, 0x86, 0x29, 0xa4, 0x03, 0x3b, 0x94, 0x83, 0x2b, 0x84, 0x43,
0x3b, 0x94, 0xc3, 0x83, 0x71, 0x98, 0x42, 0x3a, 0xe0, 0x43, 0x2a, 0xd0,
0xc3, 0x41, 0x90, 0xa8, 0x0a, 0xc8, 0x10, 0x25, 0x50, 0x08, 0x14, 0x02,
0x85, 0x28, 0x51, 0x04, 0x83, 0x4a, 0x16, 0x08, 0x0c, 0x82, 0xd4, 0x74,
0x40, 0x94, 0x40, 0x21, 0x50, 0x08, 0x14, 0xa2, 0x04, 0x0a, 0x81, 0x42,
0xa0, 0x90, 0x24, 0x10, 0x25, 0x30, 0xa8, 0xa6, 0x81, 0x28, 0x81, 0x42,
0xa0, 0x10, 0x18, 0xd4, 0xf5, 0x40, 0x94, 0x40, 0x21, 0x50, 0x08, 0x14,
0xa2, 0x04, 0x0a, 0x81, 0x42, 0xa0, 0x10, 0x18, 0x14, 0x00, 0x00, 0x00,
0x21, 0x0c, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
unsigned int empty_dia_len = 220;
FILE *fp;
fp = fopen(path, "wb");
fwrite(empty_dia, 1, empty_dia_len, fp);
fclose(fp);
}
bool fileExists(const char *path)
{
struct stat buffer;
return (stat(path, &buffer) == 0);
}
/// Copies content from sourcePath to the destination (creates one if doesn't exists)
/// Returns true for a success, false otherwise
bool copyFile(const char *sourcePath, const char *destinationPath)
{
int buf_size = 512;
char buffer[buf_size];
size_t size;
FILE *source = fopen(sourcePath, "r");
FILE *destination = fopen(destinationPath, "wb");
if (source == NULL || destination == NULL) {
return false;
}
while ((size = fread(buffer, 1, buf_size, source)) > 0) {
fwrite(buffer, 1, size, destination);
}
fclose(source);
fclose(destination);
return true;
}
bool isPresentInFile(const char *filePath, const char *search_line)
{
FILE * fp;
char * line = NULL;
size_t len = 0;
ssize_t read;
bool found = false;
size_t search_len = strlen(search_line);
fp = fopen(filePath, "r");
if (fp == NULL) {
return false;
}
while ((read = getline(&line, &len, fp)) != -1) {
// Check if a line starts with search_line followed by "" (last entry) or " \\\\n" (otherwise)
if (strncasecmp(line, search_line, search_len) == 0 &&
(strcmp(line + search_len, "") == 0 || strcmp(line + search_len, " \\\\\\n") == 0)
) {
found = true;
break;
}
}
free(line);
fclose(fp);
return found;
}
/// Decides if the file is a valid compilation unit, required to be considered in the allowed input files
/// If not, compilation step can be safelly skip for the consumer mode
bool isIrrelevantFile(const char *filePath)
{
// Skip Apple Generic Versioning file "{TargetName}_vers.c", generated during the compilation step
return isSuffixed(filePath, "\(Self.AppleGenericVersioningSuffix)");
}
/// Builds a concatenation strings. First string s1 may contain NULL characters
/// Returns the size of the output 'string'
size_t concat(char *s1, size_t s1_len, const char *s2, char **output)
{
size_t concat_len = strlen(s2);
const size_t size = s1_len + concat_len + 1;
char *new = realloc(s1, size);
memcpy(new + s1_len, s2, concat_len + 1);
*output = new;
return size - 1;
}
/// Adds NULL byte to the string s1
/// Returns the size of the output 'string'
size_t addZero(char *s1, size_t len, char **output)
{
char *delimiter = "\\0";
const size_t size = len + 1 + 1;
char *new = realloc(s1, size);
memcpy(new + len, delimiter, 1 + 1);
*output = new;
return size - 1;
}
bool appendCallToFile(const char *filePath, const char * args[], int len)
{
int fd = open(filePath, O_WRONLY|O_APPEND);
if (fd == -1) {
return false;
}
// prepare a string command to store
size_t command_len = 0;
char *command = NULL;
// print all arguments followed by {0x0}
for (int i = 0 ; i < len; i++) {
command_len = concat(command, command_len, args[i], &command);
command_len = addZero(command, command_len, &command);
}
// Finish with NULL to mirror execv format that expects NULL element at the end
command_len = addZero(command, command_len, &command);
// finish a command with a new line character
command_len = concat(command, command_len, "\\n", &command);
// acquire a lock
if (flock(fd, LOCK_EX) == -1) {
close(fd);
free(command);
return false;
}
struct stat st0;
fstat(fd, &st0);
if (st0.st_nlink == 0) {
// the file has been deleted, local compilation should happen
flock(fd, LOCK_UN);
close(fd);
free(command);
return false;
}
write(fd, command, command_len);
free(command);
if (flock(fd, LOCK_UN) == -1) {
close(fd);
return false;
}
close(fd);
return true;
}
/// Builds an array of strings from contiguous set of strings, terminated with NULL element
/// e.g. 'a{0x0}b{0x0}{0x0}' -> ['a', 'b', NULL]
char **buildArrayFromContiguousString(char *str) {
char **pointer = NULL;
char *pos = str;
int count = 0;
while (true) {
size_t len = strlen(pos);
count += 1;
pointer = realloc(pointer, count * sizeof(char*));
pointer[count - 1] = pos;
pos += (len + 1);
if (len == 0 ) {
return pointer;
}
}
}
// Calls all commands stored in filePath location
void fallbackPreviousCalls(const char *filePath)
{
int fd = open(filePath, O_RDONLY);
if (fd == -1) {
return;
}
// acquire a lock
if (flock(fd, LOCK_EX) == -1) {
close(fd);
return;
}
// make sure the file still exists, it might be deleted while we were waiting for a lock
if (access(filePath, F_OK) == -1) {
close(fd);
return;
}
struct stat st0;
fstat(fd, &st0);
if(st0.st_nlink == 0) {
// the file has been deleted - no need to fallback anything else
flock(fd, LOCK_UN);
close(fd);
return;
}
char * line = NULL;
size_t len = 0;
// iterate all lines in a file and execute commands one-by-one
FILE * file = fdopen(fd, "r");
ssize_t read;
while ((read = getline(&line, &len, file)) != -1) {
// Call all clang invocations one-by-one
pid_t pid = fork();
if (pid == 0) {
// child process
char **array = buildArrayFromContiguousString(line);
// forked process
execvp(line, array);
} else {
// hosting process
int stat;
wait(&stat);
if (!WIFEXITED(stat)) {
//the command finish incorrectly
exit(1);
}
if (WEXITSTATUS(stat)) {
// error in the "clang" call, quit with a status code
exit(WEXITSTATUS(stat));
}
}
}
free(line);
remove(filePath);
flock(fd, LOCK_UN);
fclose(file);
close(fd);
}
int main(int argc, const char * argv[])
{
const char *dependency_arg_name = "-MF";
const char *output_arg_name = "-o";
const char *serialize_diagnostics_arg_name = "--serialize-diagnostics";
const char *language_mode_arg_name = "-x";
const char *precompile_objc_header_arg_value = "objective-c-header";
const char *precompile_c_header_arg_value = "c-header";
const char *clang_cmd = "\(clangCommand)";
const char *markerFile = "\(markerFilename)";
const char *compilationHistoryFile = "\(compilationHistoryFilename)";
const char *prebuildDFile = "\(prebuildDFilename)";
// null termination args
const char *clang_args[argc + 1];
clang_args[0] = clang_cmd;
const char *dependency_file = NULL;
const char *output_file= NULL;
const char *input_file = NULL;
const char *diagnostics_file = NULL;
const char *language_mode = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], dependency_arg_name) == 0 && i < (argc - 1) ) {
// called with "-MF path" pattern and not the last argument
clang_args[i] = argv[i];
i += 1;
clang_args[i] = argv[i];
dependency_file = argv[i];
} else if (strcmp(argv[i], output_arg_name) == 0 && i < (argc - 1) ) {
// called with "-o path" pattern and not the last argument
clang_args[i] = argv[i];
i += 1;
clang_args[i] = argv[i];
output_file = argv[i];
} if (strcmp(argv[i], serialize_diagnostics_arg_name) == 0 && i < (argc - 1) ) {
// called with "--serialize-diagnostics path" pattern and not the last argument
clang_args[i] = argv[i];
i += 1;
clang_args[i] = argv[i];
diagnostics_file = argv[i];
} if (strcmp(argv[i], language_mode_arg_name) == 0 && i < (argc - 1) ) {
// called with "-x path" pattern and not the last argument
clang_args[i] = argv[i];
i += 1;
clang_args[i] = argv[i];
language_mode = argv[i];
} else if (
isSuffixed(argv[i],".m") ||
isSuffixed(argv[i],".mm") ||
isSuffixed(argv[i],".c") ||
isSuffixed(argv[i],".cc") ||
isSuffixed(argv[i],".cpp") ||
isSuffixed(argv[i],".c++") ||
isSuffixed(argv[i],".cxx") ||
isSuffixed(argv[i],".S") ||
isSuffixed(argv[i],".s")
) {
// a full list of extensions is taken from https://clang.llvm.org/docs/ClangFormatStyleOptions.html
// support for .m,.mm,.c,.cc,.cpp,.c++,.cxx input files
// .s and .S are assembly files
clang_args[i] = argv[i];
input_file = argv[i];
} else {
// pass original parameter transparently
clang_args[i] = argv[i];
}
}
// null-terminating the args array needed for local compilation fallback
clang_args[argc] = NULL;
// Verify mode. Even a target is cached, pch mode is not supported. Fallback to the local compilation
if (language_mode != NULL && (strcmp(language_mode, precompile_objc_header_arg_value) == 0 || strcmp(language_mode, precompile_c_header_arg_value) == 0)) {
return execvp(clang_cmd, (char *const*) clang_args);
}
// Verify all input arguments
if (dependency_file == NULL) {
fprintf(stderr, "error: missing %s input\\n", dependency_arg_name);
exit(1);
}
if (output_file == NULL) {
fprintf(stderr, "error: missing %s input\\n", output_arg_name);
exit(1);
}
if (input_file == NULL) {
fprintf(stderr, "error: missing input file\\n");
exit(1);
}
// Find tmp_dir
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
const char *tmp_dir = dirname(dirname(dirname(dependency_file)));
#pragma GCC diagnostic pop
// Find input in allowed files
char marker_path[1024];
sprintf(marker_path, "%s/%s", tmp_dir, markerFile);
// A file that keeps all clang invocations
char compilation_history_path[1024];
sprintf(compilation_history_path, "%s/%s", tmp_dir, compilationHistoryFile);
// Path of the prebuild.d dependency file
char prebuild_d_path[1024];
sprintf(prebuild_d_path, "%s/%s", tmp_dir, prebuildDFile);
if (fileExists(marker_path))
{
if (
isIrrelevantFile(input_file) ||
isPresentInFile(marker_path, input_file) ||
isSuffixed(input_file, "\(cachedTargetMockFilename).m")
) {
// Save .d files (copy a marker file)
bool copyResult = copyFile(marker_path, dependency_file);
if (!copyResult) {
fprintf(stderr, "error: .d file generation failed.\\n");
exit(1);
}
// Create empty .o file
createEmptyFile(output_file);
// Create .dia file (if specified)
if (diagnostics_file != NULL) {
createPlaceholderDiaFile(diagnostics_file);
}
// add to compilation_history_path file so other clang execution can retrigger it if a new file is found
bool appended = appendCallToFile(compilation_history_path, clang_args, argc);
if (appended) {
exit(0);
}
// Failed to save, most likely some other clang fallbacked to the local compilation already
// so local compilation should happen
} else {
// disable remote cache first to trigger prebuild phase in the next build
remove(marker_path);
// stop trying to reuse artifact for this specific remote commit sha
createFile(prebuild_d_path, "\(FileDependenciesWriter.skipForShaKey): \(commitSha)\\n");
// read from compilation_history_path, execute one by one all invocations
fallbackPreviousCalls(compilation_history_path);
}
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
/// execvp takes $PATH to consideration
return execvp(clang_cmd, clang_args);
#pragma GCC diagnostic pop
}
"""
} // swiftlint:disable:next file_length line_length
} // swiftlint:enable line_length
@@ -0,0 +1,107 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
typealias BuildSettings = [String: Any]
// Manages Xcode build settings
protocol BuildSettingsIntegrateAppender {
/// Appends XCRemoteCache-specific build settings
/// - Parameters:
/// - buildSettings: original build settings
/// - wrappers: definition of XCRemoteCache binaries location
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings
}
class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
private let mode: Mode
private let repoRoot: URL
private let fakeSrcRoot: URL
private let sdksExclude: [String]
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL, sdksExclude: [String]) {
self.mode = mode
self.repoRoot = repoRoot
self.fakeSrcRoot = fakeSrcRoot
self.sdksExclude = sdksExclude
}
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings {
var result = buildSettings
setBuildSetting(buildSettings: &result, key: "SWIFT_EXEC", value: wrappers.swiftc.path )
setBuildSetting(buildSettings: &result, key: "SWIFT_USE_INTEGRATED_DRIVER", value: "NO" )
// When generating artifacts, no need to shell-out all compilation commands to our wrappers
if case .consumer = mode {
setBuildSetting(buildSettings: &result, key: "CC", value: wrappers.cc.path )
setBuildSetting(buildSettings: &result, key: "LD", value: wrappers.ld.path )
// Setting LIBTOOL to '' breaks SwiftDriver intengration so resetting it to the original value
// 'libtool' for all excluded configurations
setBuildSetting(
buildSettings: &result,
key: "LIBTOOL",
value: wrappers.libtool.path,
excludedValue: "libtool"
)
setBuildSetting(buildSettings: &result, key: "LIPO", value: wrappers.lipo.path )
setBuildSetting(buildSettings: &result, key: "LDPLUSPLUS", value: wrappers.ldplusplus.path )
}
let existingSwiftFlags = result["OTHER_SWIFT_FLAGS"] as? String
let existingCFlags = result["OTHER_CFLAGS"] as? String
var swiftFlags = XcodeSettingsSwiftFlags(settingValue: existingSwiftFlags)
var clangFlags = XcodeSettingsCFlags(settingValue: existingCFlags)
// Overriding debug prefix map for Swift and ObjC to have consistent absolute path for all debug symbols
swiftFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
clangFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
setBuildSetting(buildSettings: &result, key: "OTHER_SWIFT_FLAGS", value: swiftFlags.settingValue )
setBuildSetting(buildSettings: &result, key: "OTHER_CFLAGS", value: clangFlags.settingValue )
setBuildSetting(buildSettings: &result, key: "XCRC_FAKE_SRCROOT", value: "\(fakeSrcRoot.path)" )
setBuildSetting(buildSettings: &result, key: "XCRC_PLATFORM_PREFERRED_ARCH", value:
"""
$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)
"""
)
explicitlyDisableSDKs(buildSettings: &result)
return result
}
private func setBuildSetting(buildSettings: inout BuildSettings, key: String, value: String?, excludedValue: String = "") {
buildSettings[key] = value
guard value != nil else {
// no need to exclude as the value will
return
}
// Erase all overrides for a given sdk so a default toolchain is used
for skippedSDK in sdksExclude {
buildSettings["\(key)[sdk=\(skippedSDK)]"] = excludedValue
}
}
// For all exlcuded SDKs passes XCRC_DISABLED=TRUE, which will cut-off early the pre_build phase
private func explicitlyDisableSDKs(buildSettings: inout BuildSettings) {
for skippedSDK in sdksExclude {
buildSettings["XCRC_DISABLED[sdk=\(skippedSDK)]"] = "YES"
}
}
}
@@ -0,0 +1,47 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
typealias OracleIdentifierType = String
/// Controls if the given type should be included or not
/// Example: controls if remote cache integration should be added for a given target or configuration
protocol IncludeOracle {
/// Decides if a given type should be included or not
/// - Parameter identifier: identifier of a type
func shouldInclude(identifier: OracleIdentifierType) -> Bool
}
struct IncludeExcludeOracle: IncludeOracle {
let excludes: [OracleIdentifierType]
let includes: [OracleIdentifierType]
func shouldInclude(identifier: OracleIdentifierType) -> Bool {
// exclude array has precedence
if excludes.contains(identifier) {
return false
}
guard !includes.isEmpty else {
return true
}
return includes.contains(identifier)
}
}
@@ -0,0 +1,26 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Integrates XCRemoteCache into the existing Xcode project
protocol Integrate {
/// Entry point for the XCRemoteCache integration
func run() throws
}
@@ -0,0 +1,62 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
struct IntegrateContext {
let projectPath: URL
let repoRoot: URL
let binaries: XCRCBinariesPaths
let mode: Mode
let configOverride: URL
let fakeSrcRoot: URL
let output: URL?
}
extension IntegrateContext {
init(
input: String,
repoRootPath: String,
mode: Mode,
configOverridePath: String,
env: [String: String],
binariesDir: URL,
fakeSrcRoot: String,
outputPath: String?
) throws {
projectPath = URL(fileURLWithPath: input)
let srcRoot = projectPath.deletingLastPathComponent()
repoRoot = URL(fileURLWithPath: repoRootPath, relativeTo: srcRoot)
self.mode = mode
configOverride = URL(fileURLWithPath: configOverridePath, relativeTo: srcRoot)
output = outputPath.flatMap(URL.init(fileURLWithPath:))
self.fakeSrcRoot = URL(fileURLWithPath: fakeSrcRoot)
binaries = XCRCBinariesPaths(
prepare: binariesDir.appendingPathComponent("xcprepare"),
cc: binariesDir.appendingPathComponent("xccc"),
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
libtool: binariesDir.appendingPathComponent("xclibtool"),
lipo: binariesDir.appendingPathComponent("xclipo"),
ld: binariesDir.appendingPathComponent("xcld"),
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
)
}
}
@@ -0,0 +1,25 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
public enum LLDBInitMode: String, Codable, CaseIterable {
/// Do not add anything to .lldbinit (might affect debugging experience)
case none
/// Installs lldb command in a ~/.lldbinit
case user
}
@@ -0,0 +1,111 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Saves integration specific lldb command to the .lldbinit file
protocol LLDBInitPatcher {
func enable() throws
}
/// Does nothing for patching
class NoopLLDBInitPatcher: LLDBInitPatcher {
func enable() throws {}
}
// Saves a custom lldb command (XCRC lldb command) in a .lldbinit file with a preamble comment
class FileLLDBInitPatcher: LLDBInitPatcher {
/// A preamble string. A line after that string is managed by XCRemoteCache
private static let preambleString = "#RemoteCacheCustomSourceMap"
private let fileLocation: URL
private let lldbCommand: String
private let fileAccessor: FileAccessor
/// Default initailizer
/// - Parameters:
/// - file: Location of the LLDB init file
/// - rootURL: Root location of the LLDB target source-map
/// - fakeSrcRoot: Arbitrary fake root location, shared between all producers and consumers
/// - fileManager: fileManager
init(
file: URL,
rootURL: URL,
fakeSrcRoot: URL,
fileAccessor: FileAccessor
) {
fileLocation = file
lldbCommand = "settings set target.source-map \(fakeSrcRoot.path) \(rootURL.path)"
self.fileAccessor = fileAccessor
}
private func findIndices(in collection: [String], value: String) -> [Int] {
collection.enumerated().reduce([]) { result, line -> [Int] in
if line.element == Self.preambleString {
return result + [line.offset]
}
return result
}
}
// Appends XCRC lldb command to the specifies file
// Note: Doesn't modify the file if it already contains a valid command
func enable() throws {
var finalLines: [String]
let xcrcLLDBCommandArray = [Self.preambleString, lldbCommand]
if let content = try? fileAccessor.contents(atPath: fileLocation.path) {
let contentString = String(data: content, encoding: .utf8)!
let originalContentLines = contentString.components(separatedBy: .newlines)
var contentLines = originalContentLines
let preambleIndices = findIndices(in: contentLines, value: Self.preambleString)
if !preambleIndices.isEmpty {
let firstLLDBCommandIndex = preambleIndices[0] + 1
if firstLLDBCommandIndex >= contentLines.count {
// corrupted file, append the script line at the bottom
contentLines.append(lldbCommand)
} else {
if preambleIndices.count == 1 && contentLines[firstLLDBCommandIndex] == lldbCommand {
// the file content is already valid
return
}
contentLines[firstLLDBCommandIndex] = lldbCommand
}
// Delete excessive XCRC lldb commands
for index in preambleIndices.dropFirst().reversed() {
let rangeEnd = min(index + 1, contentLines.count - 1)
contentLines.removeSubrange(index...rangeEnd)
}
} else {
contentLines += xcrcLLDBCommandArray
}
finalLines = contentLines
} else {
finalLines = xcrcLLDBCommandArray
}
// Save to disk
if finalLines.suffix(xcrcLLDBCommandArray.count) == xcrcLLDBCommandArray {
// always end with empty line when appending a command at the bottom
finalLines.append("")
}
let finalContent = finalLines.joined(separator: "\n").data(using: .utf8)
try fileAccessor.write(toPath: fileLocation.path, contents: finalContent)
}
}
@@ -0,0 +1,147 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public class XCIntegrate {
/// Separator of sequential command line arguments (e.g. configurations to exclude)
fileprivate static let inputSeparate: Character = ","
private let projectPath: String
private let mode: Mode
private let configurationsExclude: String
private let configurationsInclude: String
private let targetsExclude: String
private let targetsInclude: String
private let finalProducerTarget: String?
private let consumerEligibleConfigurations: String
private let consumerEligiblePlatforms: String
private let lldbMode: LLDBInitMode
private let fakeSrcRoot: String
private let sdksExclude: String
private let output: String?
public init(
input: String,
mode: Mode,
configurationsExclude: String,
configurationsInclude: String,
targetsExclude: String,
targetsInclude: String,
finalProducerTarget: String?,
consumerEligibleConfigurations: String,
consumerEligiblePlatforms: String,
lldbMode: LLDBInitMode,
fakeSrcRoot: String,
sdksExclude: String,
output: String?
) {
projectPath = input
self.mode = mode
self.configurationsExclude = configurationsExclude
self.configurationsInclude = configurationsInclude
self.targetsExclude = targetsExclude
self.targetsInclude = targetsInclude
self.finalProducerTarget = finalProducerTarget
self.consumerEligibleConfigurations = consumerEligibleConfigurations
self.consumerEligiblePlatforms = consumerEligiblePlatforms
self.lldbMode = lldbMode
self.fakeSrcRoot = fakeSrcRoot
self.sdksExclude = sdksExclude
self.output = output
}
// swiftlint:disable:next function_body_length
public func main() {
do {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let commandURL = URL(fileURLWithPath: ProcessInfo.processInfo.arguments[0])
// All binaries (xcprepare, xcprebuild etc.) should be placed next to each other
let binariesDir = commandURL.deletingLastPathComponent()
let srcRoot: URL = URL(fileURLWithPath: projectPath).deletingLastPathComponent()
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
let context = try IntegrateContext(
input: projectPath,
repoRootPath: config.repoRoot,
mode: mode,
configOverridePath: config.extraConfigurationFile,
env: env,
binariesDir: binariesDir,
fakeSrcRoot: fakeSrcRoot,
outputPath: output
)
let configurationOracle = IncludeExcludeOracle(
excludes: configurationsExclude.integrateArrayArguments,
includes: configurationsInclude.integrateArrayArguments
)
let targetOracle = IncludeExcludeOracle(
excludes: targetsExclude.integrateArrayArguments,
includes: targetsInclude.integrateArrayArguments
)
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
mode: context.mode,
repoRoot: context.repoRoot,
fakeSrcRoot: context.fakeSrcRoot,
sdksExclude: sdksExclude.integrateArrayArguments
)
let lldbPatcher: LLDBInitPatcher
switch lldbMode {
case .none:
lldbPatcher = NoopLLDBInitPatcher()
case .user:
let lldbInitFile = URL(fileURLWithPath: "~/.lldbinit".expandingTildeInPath)
lldbPatcher = FileLLDBInitPatcher(
file: lldbInitFile,
rootURL: context.repoRoot,
fakeSrcRoot: context.fakeSrcRoot,
fileAccessor: fileManager
)
}
let integrator = XcodeProjIntegrate(
project: context.projectPath,
mode: context.mode,
binaries: context.binaries,
configurationIncludeOracle: configurationOracle,
targetIncludeOracle: targetOracle,
finalProducerTarget: finalProducerTarget,
buildSettingsAppender: buildSettingsAppender,
consumerEligibleConfigurations: consumerEligibleConfigurations.integrateArrayArguments,
consumerEligiblePlatforms: consumerEligiblePlatforms.integrateArrayArguments,
configOverride: context.configOverride,
lldbPatcher: lldbPatcher,
output: context.output
)
try integrator.run()
} catch {
// XCIntegrate has no fallback
exit(1, "FATAL: Integrate initialization failed with error: \(error)")
}
}
}
private extension String {
var integrateArrayArguments: [String] {
split(separator: XCIntegrate.inputSeparate).map(String.init)
}
}
@@ -0,0 +1,33 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Representing locations of all XCRemoteCache binaries (including wrappers and phase scripts)
struct XCRCBinariesPaths {
let prepare: URL
let cc: URL
let swiftc: URL
let libtool: URL
let lipo: URL
let ld: URL
let ldplusplus: URL
let prebuild: URL
let postbuild: URL
}
@@ -0,0 +1,251 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
import PathKit
import XcodeProj
import Yams
enum XcodeProjIntegrateError: Error {
/// Thrown when backend server doesn't contain a commit sha with all artifacts ready
case noArtifactsToReuse
}
/// Integrates XCRemoteCache using third-party XcodeProj library
struct XcodeProjIntegrate: Integrate {
fileprivate static let BuildStepPrefix = "[RC] "
// IntegrationConfiguration represents a subset of the XCRemoteCacheConfig configuration
private struct IntegrationCacheConfig: Encodable {
let recommendedCacheAddress: URL?
let xcccFile: String
let mode: Mode
enum CodingKeys: String, CodingKey {
case xcccFile = "xccc_file"
case recommendedCacheAddress = "recommended_cache_address"
case mode
}
}
private let projectURL: URL
private let mode: Mode
private let binaries: XCRCBinariesPaths
private let configurationIncludeOracle: IncludeOracle
private let targetIncludeOracle: IncludeOracle
private let finalProducerTarget: String?
private let buildSettingsAppender: BuildSettingsIntegrateAppender
private let consumerEligibleConfigurations: [String]
private let consumerEligiblePlatforms: [String]
private let prebuildPhase: PBXShellScriptBuildPhase
private let postbuildPhase: PBXShellScriptBuildPhase
private let markPhase: PBXShellScriptBuildPhase
private let configOverride: URL
private let lldbPatcher: LLDBInitPatcher
private let output: URL?
init(
project: URL,
mode: Mode,
binaries: XCRCBinariesPaths,
configurationIncludeOracle: IncludeOracle,
targetIncludeOracle: IncludeOracle,
finalProducerTarget: String?,
buildSettingsAppender: BuildSettingsIntegrateAppender,
consumerEligibleConfigurations: [String],
consumerEligiblePlatforms: [String],
configOverride: URL,
lldbPatcher: LLDBInitPatcher,
output: URL?
) {
projectURL = project
self.mode = mode
self.binaries = binaries
self.configurationIncludeOracle = configurationIncludeOracle
self.targetIncludeOracle = targetIncludeOracle
self.finalProducerTarget = finalProducerTarget
self.buildSettingsAppender = buildSettingsAppender
self.consumerEligibleConfigurations = consumerEligibleConfigurations
self.consumerEligiblePlatforms = consumerEligiblePlatforms
self.configOverride = configOverride
self.lldbPatcher = lldbPatcher
self.output = output
prebuildPhase = PBXShellScriptBuildPhase(
name: "\(Self.BuildStepPrefix)RemoteCache_prebuild",
inputPaths: [binaries.prebuild.path],
outputPaths: ["$(TARGET_TEMP_DIR)/rc.enabled"],
shellScript: "\"$SCRIPT_INPUT_FILE_0\"",
dependencyFile: "$(TARGET_TEMP_DIR)/prebuild.d"
)
postbuildPhase = PBXShellScriptBuildPhase(
name: "\(Self.BuildStepPrefix)RemoteCache_postbuild",
inputPaths: [binaries.postbuild.path],
outputPaths: [
"""
$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/\
$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5
""",
"""
$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/\
$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)\
$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5
""",
],
shellScript: "\"$SCRIPT_INPUT_FILE_0\"",
dependencyFile: "$(TARGET_TEMP_DIR)/postbuild.d"
)
markPhase = PBXShellScriptBuildPhase(
name: "\(Self.BuildStepPrefix)RemoteCache_mark",
inputPaths: [binaries.prepare.path],
shellScript:
"\"$SCRIPT_INPUT_FILE_0\" mark " +
"--configuration \"$CONFIGURATION\" --platform \"$PLATFORM_NAME\""
)
}
/// Dump overrides to the XCRemoteCacheConfig into disk location
private func storeRCOverride(
_ override: IntegrationCacheConfig,
configOverrideLocation: URL
) throws {
// Store .rcinfo override
let encoder = YAMLEncoder()
let encodedYAML = try encoder.encode(override)
try encodedYAML.write(to: configOverrideLocation, atomically: false, encoding: .utf8)
}
// swiftlint:disable:next function_body_length
func run() throws {
let outputFile = output ?? projectURL
let projectRoot = projectURL.deletingLastPathComponent()
let projectPath = Path(projectURL.path)
let outputPath = Path(outputFile.path)
// Override all extra configs (default to 'user.rc', next to the main '.rcinfo' file)
let initialOverride = IntegrationCacheConfig(
recommendedCacheAddress: nil,
xcccFile: binaries.cc.path,
mode: mode
)
try storeRCOverride(initialOverride, configOverrideLocation: configOverride)
if case .consumer = mode {
// require successful preparation
do {
// Call xcprepare to probe if XCRemoteCache can be safely used
let args = ["--configuration"] + consumerEligibleConfigurations + ["--platform"] +
consumerEligiblePlatforms
let yamlString = try shellGetStdout(
binaries.prepare.path,
args: args,
inDir: projectRoot.path,
environment: nil
)
let decoder = YAMLDecoder()
let prepareResult = try decoder.decode(PrepareResult.self, from: yamlString, userInfo: [:])
guard case .preparedFor(_, recommendedCacheAddress: let remote) = prepareResult else {
throw XcodeProjIntegrateError.noArtifactsToReuse
}
// Override the configuration again to include recommended cache address provided by xcprepare
let finalOverride = IntegrationCacheConfig(
recommendedCacheAddress: remote,
xcccFile: binaries.cc.path,
mode: mode
)
try storeRCOverride(finalOverride, configOverrideLocation: configOverride)
} catch {
// integration cannot be done as `xccc` hasn't been generated
exit(1, "XCRemoteCache cannot be initialized with a consumer mode. Error: \(error).")
}
}
// modify .pbxproj
let xcodeproj = try XcodeProj(path: projectPath)
for target in xcodeproj.pbxproj.nativeTargets {
guard targetIncludeOracle.shouldInclude(identifier: target.name) else {
continue
}
guard let targetConfigurations = target.buildConfigurationList else {
fatalError("Missing buildConfigurationList. Cannot apply")
}
// Apply settings for only few configurations
let targetConfigurationsToIntegrate = targetConfigurations.buildConfigurations.filter {
configurationIncludeOracle.shouldInclude(identifier: $0.name)
}
guard !targetConfigurationsToIntegrate.isEmpty else {
// No need to append build phases if none of Configurations exist for that target
continue
}
for buildConfiguration in targetConfigurationsToIntegrate {
let initialSettings = buildConfiguration.buildSettings
let finalSettings = buildSettingsAppender.appendToBuildSettings(
buildSettings: initialSettings,
wrappers: binaries
)
buildConfiguration.buildSettings = finalSettings
}
addSharedBuildPhases(target: target, in: xcodeproj.pbxproj)
// Add producer build phase that marks given sha+configuration+platform as "ready to use"
if case .producer = mode, finalProducerTarget == target.name {
addFinalProducerBuildPhases(target: target, in: xcodeproj.pbxproj)
}
}
try xcodeproj.write(path: outputPath)
try lldbPatcher.enable()
}
/// Adds build phases for both producer and consumer
private func addSharedBuildPhases(target: PBXNativeTarget, in pbxproj: PBXProj) {
// delete all previous XCRC build phases
let previousRCPhases = target.buildPhases.filter(isRCPhase)
target.buildPhases.removeAll(where: previousRCPhases.contains)
if let sourceIndex = target.buildPhases.map(\.buildPhase).firstIndex(of: .sources) {
// add (pre|post)build phases only when a target has some compilation steps
// otherwise they make no sense (nothing to store in an artifact)
// add postbuild right after compilation as custom build steps may depend on files generated by
// the xcpostbuild (e.g. to copy -Swift.h.md5)
pbxproj.add(object: postbuildPhase)
target.buildPhases.insert(postbuildPhase, at: sourceIndex + 1)
pbxproj.add(object: prebuildPhase)
target.buildPhases.insert(prebuildPhase, at: sourceIndex)
}
}
/// Adds build phases as the very last producer target
private func addFinalProducerBuildPhases(target: PBXNativeTarget, in pbxproj: PBXProj) {
pbxproj.add(object: markPhase)
target.buildPhases.append(markPhase)
}
private func isRCPhase(_ phase: PBXBuildPhase) -> Bool {
phase.name()?.hasPrefix(Self.BuildStepPrefix) == true
}
}
@@ -0,0 +1,117 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Type to manage Xcode build setting with compilation flags (e.g. OTHER_CFLAGS or OTHER_SWIFT_FLAGS)
public protocol XcodeSettingsFlags {
var settingValue: String? { get }
mutating func assignFlag(key: String, value: String?)
}
/// Builds compilation flags string value
private struct XcodeSettingsBuilder {
static let inheritedExpression: String = "$(inherited)"
static func composeFlags(_ flags: [String]) -> String? {
if flags == [Self.inheritedExpression] {
return nil
}
return flags.joined(separator: " ")
}
}
/// Manages flags for OTHER_SWIFT_FLAGS Xcode's Build Setting
struct XcodeSettingsSwiftFlags: XcodeSettingsFlags {
private static let swiftFlagPrefix = "-"
private(set) var settingValue: String?
init(settingValue: String?) {
self.settingValue = settingValue
}
private func buildSwiftFlag(key: String, value: String) -> [String] {
[key, value]
}
mutating func assignFlag(key: String, value: String?) {
let flags: [String]
let formattedKey = Self.swiftFlagPrefix.appending(key)
switch (settingValue, value) {
case (nil, nil):
return
case (nil, .some(let value)):
flags = [XcodeSettingsBuilder.inheritedExpression, formattedKey, value]
case (.some(let existing), _):
var flagsComponents: [String] = existing.split(separator: " ").map(String.init)
// remove (if exists)
if let previousIndex = flagsComponents.firstIndex(of: formattedKey) {
// delete "-{key}" and "{value}"
flagsComponents.removeSubrange(previousIndex..<previousIndex + 2)
}
// add if setting a non nil value
if let newValue = value {
flagsComponents += buildSwiftFlag(key: formattedKey, value: newValue)
}
flags = flagsComponents
}
settingValue = XcodeSettingsBuilder.composeFlags(flags)
}
}
/// Manages flags for OTHER_CFLAGS Xcode's Build Setting
struct XcodeSettingsCFlags: XcodeSettingsFlags {
private static let prefix = "-f"
private(set) var settingValue: String?
init(settingValue: String?) {
self.settingValue = settingValue
}
private func buildCFlag(key: String, value: String) -> [String] {
["\(Self.prefix)\(key)=\(value)"]
}
mutating func assignFlag(key: String, value: String?) {
let flags: [String]
switch (settingValue, value) {
case (nil, nil):
return
case (nil, .some(let value)):
flags = [XcodeSettingsBuilder.inheritedExpression] + buildCFlag(key: key, value: value)
case (.some(let existing), _):
var flagsComponents: [String] = existing.split(separator: " ").map(String.init)
// remove (if exists)
let existingFlagIndex = flagsComponents.firstIndex { component -> Bool in
component.hasPrefix("\(Self.prefix)\(key)=")
}
if let index = existingFlagIndex {
flagsComponents.remove(at: index)
}
// add (if sets new)
if let newValue = value {
flagsComponents += buildCFlag(key: key, value: newValue)
}
flags = flagsComponents
}
settingValue = XcodeSettingsBuilder.composeFlags(flags)
}
}
@@ -0,0 +1,138 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum PrepareResult: Equatable {
struct ShaInfo: Equatable, Encodable {
/// Sha of the commit
let sha: String
/// Number of skipped commits to reach that sha from HEAD
/// For a repo with a merge strategy - number of merge commits from HEAD
let age: Int
}
case preparedFor(sha: ShaInfo, recommendedCacheAddress: URL)
case failed
}
protocol PrepareLogic {
func prepare() throws -> PrepareResult
}
enum PrepareError: Error {
/// Cannot find common commit sha with primary branch
case invalidSha
/// xcode-select does not specify current xcode
case missingXcodeSelectDirectory
}
class Prepare: PrepareLogic {
private let context: PrepareContext
private let gitClient: GitClient
private let networkClients: [RemoteNetworkClient]
private let ccBuilder: CCWrapperBuilder
private let fileAccessor: FileAccessor
private let cacheInvalidator: CacheInvalidator
private let globalCacheSwitcher: GlobalCacheSwitcher
init(
context: PrepareContext,
gitClient: GitClient,
networkClients: [RemoteNetworkClient],
ccBuilder: CCWrapperBuilder,
fileAccessor: FileAccessor,
globalCacheSwitcher: GlobalCacheSwitcher,
cacheInvalidator: CacheInvalidator
) {
self.context = context
self.gitClient = gitClient
self.networkClients = networkClients
self.ccBuilder = ccBuilder
self.fileAccessor = fileAccessor
self.cacheInvalidator = cacheInvalidator
self.globalCacheSwitcher = globalCacheSwitcher
}
/// Finds the best commit with generated artifacts to use
func prepare() throws -> PrepareResult {
do {
guard fileAccessor.fileExists(atPath: PhaseCacheModeController.xcodeSelectLink.path) else {
throw PrepareError.missingXcodeSelectDirectory
}
let commonSha: String
do {
commonSha = try gitClient.getCommonPrimarySha()
} catch let GitClientError.noCommonShaWithPrimaryRepo(remoteName, error) {
guard context.gracefullyHandleMissingCommonSha else {
throw GitClientError.noCommonShaWithPrimaryRepo(remoteName: remoteName, error: error)
}
infoLog("Cannot find a common sha with the primary branch: \(error). Gracefully disabling remote cache")
try disable()
return .failed
}
if context.offline {
// Optimistically take first common sha
return try enableCommit(sha: commonSha, age: 0)
}
// Remove old artifacts from local cache
cacheInvalidator.invalidateArtifacts()
// calling `git` is expensive, so optimistically tring the common sha first
if try isArtifactAvailable(for: commonSha) {
return try enableCommit(sha: commonSha, age: 0)
}
// Find a list of all potential commits that may have artifacts that can be used
let allCommonCommits = try gitClient.getPreviousCommits(starting: commonSha, maximum: context.maximumSha)
// First commit was checked already
for (index, sha) in allCommonCommits.dropFirst().enumerated() {
// Check if the marker file for a `sha` commit is available on the remote cache server
if try isArtifactAvailable(for: sha) {
// adding 1 because current HEAD was already checked
return try enableCommit(sha: sha, age: index + 1)
}
}
infoLog("No artifacts available")
try disable()
} catch {
try disable()
throw error
}
return .failed
}
private func isArtifactAvailable(for commit: String) throws -> Bool {
try networkClients.allSatisfy { networkClient in
try networkClient.fileExists(.marker(commit: commit))
}
}
private func enableCommit(sha: String, age: Int) throws -> PrepareResult {
try globalCacheSwitcher.enable(sha: sha)
try ccBuilder.compile(to: context.xcccCommand, commitSha: sha)
return .preparedFor(sha: .init(sha: sha, age: age), recommendedCacheAddress: context.recommendedCacheAddress)
}
private func disable() throws {
try globalCacheSwitcher.disable()
}
}
@@ -0,0 +1,84 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public enum PrepareContextError: Error {
/// Provided primary repo is not a valid location
case invalidPrimaryRepo(String)
/// Provided primary branch is not a defined or invalid
case invalidPrimaryBranch(String)
/// Remote Cache server address is not valid URL
case invalidRemoteCacheAddress(String)
}
public struct PrepareContext {
/// Path of the primary repository that produces cache artifacts
let primaryRepo: String
/// Main (primary) branch that produces cache artifacts
let primaryBranch: String
/// Path of the git repository
let repoRoot: URL
/// Location of the file that specifies remote commit sha
let remoteCommitLocation: URL
/// Maximum number of shas to look for a cache
let maximumSha: Int
/// skip making any HTTP requests and optimistically use a cache
let offline: Bool
/// Remote address of the remote server
var recommendedCacheAddress: URL
/// Remote addresses of all remote servers
let cacheAddresses: [URL]
/// Health path (relative to cacheAddresses) that determines request latency
let cacheHealthPath: String
/// Number of times to probe health path
let cacheHealthPathProbeCount: Int
/// clang wrapper output file
let xcccCommand: URL
/// gracefully disable remote cache for missing common sha with the primary branch
let gracefullyHandleMissingCommonSha: Bool
}
extension PrepareContext {
init(_ config: XCRemoteCacheConfig, offline: Bool) throws {
guard !config.primaryRepo.isEmpty else {
throw PrepareContextError.invalidPrimaryRepo(config.primaryRepo)
}
guard !config.primaryBranch.isEmpty else {
throw PrepareContextError.invalidPrimaryBranch(config.primaryBranch)
}
primaryRepo = config.primaryRepo
primaryBranch = config.primaryBranch
let sourceRoot = URL(fileURLWithPath: config.sourceRoot, isDirectory: true)
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: sourceRoot)
remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: repoRoot)
maximumSha = config.cacheCommitHistory
self.offline = offline
guard let address = URL(string: config.recommendedCacheAddress) else {
throw PrepareContextError.invalidRemoteCacheAddress(config.recommendedCacheAddress)
}
recommendedCacheAddress = address
xcccCommand = URL(fileURLWithPath: config.xcccFile, relativeTo: repoRoot)
cacheAddresses = try config.cacheAddresses.map(URL.build)
cacheHealthPath = config.cacheHealthPath
cacheHealthPathProbeCount = config.cacheHealthPathProbeCount
gracefullyHandleMissingCommonSha = config.gracefullyHandleMissingCommonSha
}
}
@@ -0,0 +1,50 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public enum PrepareMarkContextError: Error {
case invalidAddress(String)
}
public struct PrepareMarkContext {
/// Path of the git repository
let repoRoot: URL
/// Remote address of the remote server
let recommendedCacheAddress: URL
/// All remote servers to mark
let cacheAddresses: [URL]
/// XCRemoteCache is explicitly disabled
let disabled: Bool
}
extension PrepareMarkContext {
init(_ config: XCRemoteCacheConfig, env: [String: String]) throws {
let sourceRoot = URL(fileURLWithPath: config.sourceRoot, isDirectory: true)
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: sourceRoot)
guard let address = URL(string: config.recommendedCacheAddress) else {
errorLog("Invalid cache address: \(config.recommendedCacheAddress)")
throw PrepareMarkContextError.invalidAddress(config.recommendedCacheAddress)
}
recommendedCacheAddress = address
cacheAddresses = try config.cacheAddresses.map(URL.build)
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
}
}
@@ -0,0 +1,47 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Print current configuration to the console
public class XCConfig {
private let outputEncoder: XCRemoteCacheEncoder
public init(format: XCOutputFormat) {
outputEncoder = XCEncoderAbstractFactory().build(for: format)
}
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
} catch {
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
}
do {
let output = try outputEncoder.encode(config)
print(output)
} catch {
exit(1, "XCInfo failed with error: \(error)")
}
}
}
@@ -0,0 +1,216 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Switch between Online/Offline modes
public enum XCPrepareMode {
/// Find the best common sha with primary with available marker on the remote cache server
case online(configurations: [String], platforms: [String], customXcodeBuildNumber: String?)
/// Skip making any HTTP requests and optimistically use a cache
case offline
}
/// 1) Finds the best sha to use with Remote Cache. Saves it to the local file and prints to the console
/// 2) Compiles xccc wrapper from source
/// 3) Invalidates outdated local cache entries
public class XCPrepare {
private let offline: Bool
private let configurations: [String]
private let platforms: [String]
private let customXcodeBuildNumber: String?
private let outputEncoder: XCRemoteCacheEncoder
public init(_ mode: XCPrepareMode, format: XCOutputFormat) {
switch mode {
case .offline:
offline = true
configurations = []
platforms = []
customXcodeBuildNumber = nil
case .online(let configurations, let platforms, let customXcodeBuildNumber):
offline = false
self.platforms = platforms
self.configurations = configurations
self.customXcodeBuildNumber = customXcodeBuildNumber
}
outputEncoder = XCEncoderAbstractFactory().build(for: format)
}
// swiftlint:disable:next function_body_length
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
var context: PrepareContext
let xcodeVersion: String
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PrepareContext(config, offline: offline)
xcodeVersion = try customXcodeBuildNumber ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
} catch {
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
}
do {
// TODO: Refactor to not pass empty arguments to `URLBuilderImpl`
// URLs required by 'prepare' command are global for a project and don't required 'targetName'
// or 'envFingerprint' - these are valid only for a target level requests
let sessionFactory = DefaultURLSessionFactory(config: config)
var awsV4Signature: AWSV4Signature?
if !config.AWSAccessKey.isEmpty {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
)
}
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.downloadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
let serverProbe = try LowestLatencyNetworkServerProbe(
servers: context.cacheAddresses,
healthPath: context.cacheHealthPath,
probes: context.cacheHealthPathProbeCount,
fallbackServer: context.recommendedCacheAddress,
networkClient: networkClient
)
context.recommendedCacheAddress = try serverProbe.determineRemoteServer()
var networkClients: [RemoteNetworkClient] = []
for platform in platforms {
for configuration in configurations {
let urlBuilder = URLBuilderImpl(
address: context.recommendedCacheAddress,
configuration: configuration,
platform: platform,
targetName: "",
xcode: xcodeVersion,
envFingerprint: "",
schemaVersion: config.schemaVersion
)
networkClients.append(RemoteNetworkClientImpl(networkClient, urlBuilder))
}
}
let primaryGitBranch = GitBranch(repoLocation: context.primaryRepo, branch: context.primaryBranch)
let gitClient = GitClientImpl(
repoRoot: context.repoRoot.path,
primary: primaryGitBranch,
shell: shellGetStdout
)
let ccBuilder = TemplateBasedCCWrapperBuilder(
clangCommand: config.clangCommand,
markerPath: config.modeMarkerPath,
cachedTargetMockFilename: config.thinTargetMockFilename,
prebuildDFilename: config.prebuildDiscoveryPath,
compilationHistoryFilename: config.compilationHistoryFile,
shellOut: shellGetStdout,
fileManager: fileManager
)
let cacheURL: URL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
let localBuilder = LocalURLBuilderImpl(cachePath: cacheURL)
let cacheAddress = localBuilder.location(for: context.recommendedCacheAddress)
let cacheInvalidator = LocalCacheInvalidator(
localCacheURL: cacheAddress,
maximumAgeInDays: config.artifactMaximumAge
)
let fileAccessor = LazyFileAccessor(fileAccessor: FileManager.default)
let globalCacheSwitcher = FileGlobalCacheSwitcher(context.remoteCommitLocation, fileAccessor: fileAccessor)
let prepare = Prepare(
context: context,
gitClient: gitClient,
networkClients: networkClients,
ccBuilder: ccBuilder,
fileAccessor: fileAccessor,
globalCacheSwitcher: globalCacheSwitcher,
cacheInvalidator: cacheInvalidator
)
let prepareResult = try prepare.prepare()
try outputResult(prepareResult)
} catch GitClientError.missingPrimaryRepo(let repo) {
exit(1, """
XCRemoteCache's `xcprepare` failed to find git remote with \(repo) address.\
Check that your git configuration (`git remote -v`) specifies it.
""")
} catch {
exit(1, "Prepare failed with error: \(error)")
}
}
/// Prints to the standard output, result of the prepare command
private func outputResult(_ result: PrepareResult) throws {
let outputString = try outputEncoder.encode(result)
print(outputString)
}
}
extension PrepareResult: Encodable {
enum CodingKeys: String, CodingKey {
case result
case commit
case age
case recommendedRemoteAddress = "recommended_remote_address"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let result: Bool
let commit: String?
let age: Int?
let recommendedRemoteAddress: URL?
switch self {
case .failed:
result = false
commit = nil
age = nil
recommendedRemoteAddress = nil
case .preparedFor(let sha, let remoteAddress):
result = true
commit = sha.sha
age = sha.age
recommendedRemoteAddress = remoteAddress
}
try container.encode(result, forKey: .result)
try container.encode(commit, forKey: .commit)
try container.encode(age, forKey: .age)
try container.encode(recommendedRemoteAddress, forKey: .recommendedRemoteAddress)
}
}
extension PrepareResult: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Self.CodingKeys)
let result = try container.decode(Bool.self, forKey: .result)
if result {
let commit = try container.decode(String.self, forKey: .commit)
let age = try container.decode(Int.self, forKey: .age)
let recommendedRemoteAddress = try container.decode(URL.self, forKey: .recommendedRemoteAddress)
self = .preparedFor(sha: ShaInfo(sha: commit, age: age), recommendedCacheAddress: recommendedRemoteAddress)
} else {
self = .failed
}
}
}
@@ -0,0 +1,118 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Marks current sha as artifact-available on the remote side
public class XCPrepareMark {
private let configuration: String
private let platform: String
private let xcode: String?
private let commit: String?
public init(
configuration: String,
platform: String,
xcode: String?,
commit: String?
) {
self.configuration = configuration
self.platform = platform
self.xcode = xcode
self.commit = commit
}
// swiftlint:disable:next function_body_length
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: PrepareMarkContext
let xcodeVersion: String
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PrepareMarkContext(config, env: env)
xcodeVersion = try xcode ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
} catch {
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
}
guard !context.disabled else {
infoLog("XCRemoteCache explicitly disabled for marking.")
return
}
do {
let sessionFactory = DefaultURLSessionFactory(config: config)
var awsV4Signature: AWSV4Signature?
if !config.AWSAccessKey.isEmpty {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
)
}
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.uploadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
let remoteNetworkClient = try RemoteNetworkClientAbstractFactory(
mode: .producer,
downloadStreamURL: context.recommendedCacheAddress,
upstreamStreamURL: context.cacheAddresses,
uploadBatchSize: config.uploadBatchSize,
networkClient: networkClient
) { [configuration, platform] cacheAddress in
// Prepare URLs don't include target name or envFingperint, which are valid only for a target level
return URLBuilderImpl(
address: cacheAddress,
configuration: configuration,
platform: platform,
targetName: "",
xcode: xcodeVersion,
envFingerprint: "",
schemaVersion: config.schemaVersion
)
}.build()
let gitCommit = try getCommitToMark(context: context, config: config)
try remoteNetworkClient.createSynchronously(.marker(commit: gitCommit))
} catch {
exit(1, "Prepare failed with error: \(error)")
}
}
private func getCommitToMark(context: PrepareMarkContext, config: XCRemoteCacheConfig) throws -> String {
if let commit = commit {
return commit
}
let gitClient = GitClientImpl(
repoRoot: context.repoRoot.path,
primary: GitBranch(repoLocation: config.primaryRepo, branch: config.primaryBranch),
shell: shellGetStdout
)
return try gitClient.getCurrentSha()
}
}
@@ -0,0 +1,65 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Manages XCRemoteCache statistics: rests, print to the standard output etc
public class XCStats {
private let outputEncoder: XCRemoteCacheEncoder
private let reset: Bool
public init(format: XCOutputFormat, reset: Bool) {
self.reset = reset
outputEncoder = XCEncoderAbstractFactory().build(for: format)
}
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: XCStatsContext
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
try context = XCStatsContext(config, fileManager: fileManager)
} catch {
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
}
do {
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
ExclusiveFileCounter(ExclusiveFile(file, mode: .override), countersCount: count)
}
let statsCoordinator = try FileStatsCoordinator(
statsLocation: context.statsDir,
cacheLocationDir: context.cacheLocation,
counterFactory: counterFactory,
fileManager: fileManager
)
if reset {
try statsCoordinator.reset()
}
let stats = try statsCoordinator.readStats()
let output = try outputEncoder.encode(stats)
print(output)
} catch {
exit(1, "XCStats failed with error: \(error)")
}
}
}
@@ -0,0 +1,42 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public enum XCStatsContextError: Error {
case invalidAddress(String)
}
public struct XCStatsContext {
/// Path of the root directory with all statistic files
let statsDir: URL
/// Location of the local cache that stores all fetched artifacts, metas etc
let cacheLocation: URL
}
extension XCStatsContext {
init(_ config: XCRemoteCacheConfig, fileManager: FileManager) throws {
statsDir = URL(fileURLWithPath: config.statsDir.expandingTildeInPath)
let cacheURL: URL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
let cacheURLBuilder = LocalURLBuilderImpl(cachePath: cacheURL)
cacheLocation = cacheURLBuilder.localAddress
}
}
@@ -0,0 +1,108 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Wrapper for `libtool` or `ld` call for creating a binary (a static or a dynamic library)
/// Moves binary from a cache aritfact to the output location or
/// fallbacks to the standard command (when cached product is not applicable)
public class XCCreateBinary {
private let output: URL
private let tempDir: URL
private let dependencyInfo: URL
private let fallbackCommand: String
private let stepDescription: String
/// Initializer of a binary creator step
/// - Parameters:
/// - output: Destination of the binary to create
/// - filelist: location of a filelist file with all input files of that step
/// - dependencyInfo: location of the file to specify all dependencies of that step
/// - fallbackCommand: command of the fallback command
/// - stepDescription: descriptive name of the step
public init(
output: String,
filelist: String,
dependencyInfo: String,
fallbackCommand: String,
stepDescription: String
) {
self.output = URL(fileURLWithPath: output)
self.dependencyInfo = URL(fileURLWithPath: dependencyInfo)
// fileList is place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/$TARGET_NAME.LinkFileList
// TODO: find better (stable) technique to determine `$TARGET_TEMP_DIR`
tempDir = URL(fileURLWithPath: filelist)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
self.fallbackCommand = fallbackCommand
self.stepDescription = stepDescription
}
private func fallbackToDefault() -> Never {
let args = ProcessInfo().arguments
let paramList = [fallbackCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(fallbackCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
public func run() {
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
} catch {
errorLog("\(stepDescription) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
fallbackToDefault()
}
let markerURL = tempDir.appendingPathComponent(config.modeMarkerPath)
do {
let organizer = ZipArtifactOrganizer(
targetTempDir: tempDir,
// Creation binary doesn't call artifact preprocessing
artifactProcessors: [],
fileManager: fileManager
)
let dependenciesWriter = FileDatWriter(dependencyInfo, fileManager: fileManager)
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
guard fileManager.fileExists(atPath: markerURL.path) else {
fallbackToDefault()
}
let cachedArtifactDir = organizer.getActiveArtifactLocation()
let outputFilename = output.lastPathComponent
let cachedBinaryURL = cachedArtifactDir.appendingPathComponent(outputFilename)
try fileManager.spt_forceLinkItem(at: cachedBinaryURL, to: output)
try dependenciesWriter.enable(dependencies: markerReader.listFilesURLs(), outputs: [output])
} catch {
errorLog("\(stepDescription) failed with error: \(error). Fallbacking to \(fallbackCommand)")
do {
try fileManager.removeItem(at: markerURL)
fallbackToDefault()
} catch {
exit(1, "FATAL: \(stepDescription) failed with error: \(error)")
}
}
}
}
@@ -0,0 +1,82 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum MirroredLinkingSwiftcProductsGeneratorError: Error {
/// When the generation source list misses a path to the main "swiftmodule" file
case missingMainSwiftmoduleFileToGenerateFrom
}
/// Products generator that finds swift products destination based on the artifact dir structure. It uses
/// `LinkingSwiftcProductsGenerator` under the hood
///
/// Useful for cases where destination locations are not provided explicitly (e.g. in a thin projects)
class MirroredLinkingSwiftcProductsGenerator: SwiftcProductsGenerator {
private let arch: String
private let buildDir: URL
private let headersDir: URL
private let diskCopier: DiskCopier
/// Default initializer
/// - Parameters:
/// - arch: architecture of the build
/// - buildDir: directory where all *.swiftmodule products should be placed
/// - headersDir: directory where generated ObjC headers should be placed
/// - fileManager: fileManager instance
init(
arch: String,
buildDir: URL,
headersDir: URL,
diskCopier: DiskCopier
) {
self.arch = arch
self.buildDir = buildDir
self.headersDir = headersDir
self.diskCopier = diskCopier
}
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> SwiftcProductsGeneratorOutput {
/// Predict moduleName from the `*.swiftmodule` artifact
let foundSwiftmoduleFile = artifactSwiftModuleFiles[.swiftmodule]
guard let mainSwiftmoduleFile = foundSwiftmoduleFile else {
throw MirroredLinkingSwiftcProductsGeneratorError.missingMainSwiftmoduleFileToGenerateFrom
}
let moduleName = mainSwiftmoduleFile.deletingPathExtension().lastPathComponent
let modulePathOutput = buildDir
.appendingPathComponent("\(moduleName).swiftmodule")
.appendingPathComponent(arch)
.appendingPathExtension("swiftmodule")
let objcHeaderOutput = headersDir.appendingPathComponent("\(moduleName)-Swift.h")
let generator = DiskSwiftcProductsGenerator(
modulePathOutput: modulePathOutput,
objcHeaderOutput: objcHeaderOutput,
diskCopier: diskCopier
)
return try generator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftModuleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
}
}
@@ -0,0 +1,188 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum SwiftCResult {
/// Swiftc mock cannot be used and fallback to the compilation is required
case forceFallback
/// All compilation steps were mocked correctly
case success
}
/// Swiftc mocking compilation
protocol SwiftcProtocol {
/// Tries to performs mocked compilation (moving all cached files to the expected location)
/// If cached compilation products are not valid or incompatible, fallbacks to build-from-source
/// - Returns: `.forceFallback` if the cached compilation products are incompatible and fallback
/// to a standard 'swiftc' is required, `.success` otherwise
/// - Throws: An error if there was an unrecoverable, serious error (e.g. IO error)
func mockCompilation() throws -> SwiftCResult
}
/// Swiftc wrapper that mocks compilation with noop and moves all expected products from cache location
class Swiftc: SwiftcProtocol {
/// Reader of all input files of the compilation
private let inputFileListReader: ListReader
/// Reader of the marker file lists - list of dependencies to set for swiftc compilation
private let markerReader: ListReader
/// Checks if the input file exists in the file list
private let allowedFilesListScanner: FileListScanner
/// Manager of the downloaded artifact package
private let artifactOrganizer: ArtifactOrganizer
/// Reads all input and output files for the compilation from an input filemap
private let inputFilesReader: SwiftcInputReader
/// Write manager of the marker file
private let markerWriter: MarkerWriter
/// Generates products at the desired destination
private let productsGenerator: SwiftcProductsGenerator
private let context: SwiftcContext
private let fileManager: FileManager
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
private let touchFactory: (URL, FileManager) -> Touch
private let plugins: [SwiftcProductGenerationPlugin]
init(
inputFileListReader: ListReader,
markerReader: ListReader,
allowedFilesListScanner: FileListScanner,
artifactOrganizer: ArtifactOrganizer,
inputReader: SwiftcInputReader,
context: SwiftcContext,
markerWriter: MarkerWriter,
productsGenerator: SwiftcProductsGenerator,
fileManager: FileManager,
dependenciesWriterFactory: @escaping (URL, FileManager) -> DependenciesWriter,
touchFactory: @escaping (URL, FileManager) -> Touch,
plugins: [SwiftcProductGenerationPlugin]
) {
self.inputFileListReader = inputFileListReader
self.markerReader = markerReader
self.allowedFilesListScanner = allowedFilesListScanner
self.artifactOrganizer = artifactOrganizer
inputFilesReader = inputReader
self.context = context
self.markerWriter = markerWriter
self.productsGenerator = productsGenerator
self.fileManager = fileManager
self.dependenciesWriterFactory = dependenciesWriterFactory
self.touchFactory = touchFactory
self.plugins = plugins
}
// swiftlint:disable:next function_body_length
func mockCompilation() throws -> SwiftCResult {
let rcModeEnabled = markerReader.canRead()
guard rcModeEnabled else {
infoLog("Swiftc marker doesn't exist")
return .forceFallback
}
let inputFilesInputs = try inputFileListReader.listFilesURLs()
let markerAllowedFiles = try markerReader.listFilesURLs()
let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory(
dependencies: markerAllowedFiles,
fileManager: fileManager,
writerFactory: dependenciesWriterFactory
)
// Verify all input files to be present in a marker fileList
let disallowedInputs = try inputFilesInputs.filter { try !allowedFilesListScanner.contains($0) }
if !disallowedInputs.isEmpty {
// New file (disallowedFile) added without modifying the rest of the feature. Fallback to swiftc and
// ensure that compilation from source will be forced up until next merge/rebase with "primary" branch
infoLog("Swiftc new input file \(disallowedInputs)")
// Deleting marker to indicate that the remote cached artifact cannot be used
try markerWriter.disable()
// Save custom prebuild discovery content to make sure that the following prebuild
// phase will not try to reuse cached artifact (if present)
// In other words: let prebuild know that it should not try to reenable cache
// until the next merge with primary
switch context.mode {
case .consumer(commit: .available(let remoteCommit)):
let prebuildDiscoveryURL = context.tempDir.appendingPathComponent(context.prebuildDependenciesPath)
let prebuildDiscoverWriter = dependenciesWriterFactory(prebuildDiscoveryURL, fileManager)
try prebuildDiscoverWriter.write(skipForSha: remoteCommit)
case .consumer, .producer, .producerFast:
// Never skip prebuild phase and fallback to the swiftc compilation for:
// 1) Not enabled remote cache, 2) producer(s)
break
}
return .forceFallback
}
let artifactLocation = artifactOrganizer.getActiveArtifactLocation()
// Read swiftmodule location from XCRemoteCache
// arbitrary format swiftmodule/${arch}/${moduleName}.swift{module|doc|sourceinfo}
let moduleName = context.modulePathOutput.deletingPathExtension().lastPathComponent
let allCompilations = try inputFilesReader.read()
let artifactSwiftmoduleDir = artifactLocation
.appendingPathComponent("swiftmodule")
.appendingPathComponent(context.arch)
let artifactSwiftmoduleBase = artifactSwiftmoduleDir.appendingPathComponent(moduleName)
let artifactSwiftmoduleFiles = Dictionary(
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
.map { ext, _ in
(ext, artifactSwiftmoduleBase.appendingPathExtension(ext.rawValue))
}
)
// Build -Swift.h location from XCRemoteCache arbitrary format include/${arch}/${target}-Swift.h
let artifactSwiftModuleObjCDir = artifactLocation
.appendingPathComponent("include")
.appendingPathComponent(context.arch)
.appendingPathComponent(context.moduleName)
// Move cached xxxx-Swift.h to the location passed in arglist
// Alternatively, artifactSwiftModuleObjCFile could be built as a first .h file in artifactSwiftModuleObjCDir
let artifactSwiftModuleObjCFile = artifactSwiftModuleObjCDir
.appendingPathComponent(context.objcHeaderOutput.lastPathComponent)
_ = try productsGenerator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
try plugins.forEach {
try $0.generate(for: allCompilations)
}
// Save individual .d and touch .o for each .swift file
for compilation in allCompilations.files {
if let object = compilation.object {
// Touching .o is required to invalidate already existing .a or linked library
let touch = touchFactory(object, fileManager)
try touch.touch()
}
if let individualDeps = compilation.dependencies {
// swiftc product should be invalidated if any of dependencies file has changed
try cachedDependenciesWriterFactory.generate(output: individualDeps)
}
}
// Save .d for the entire module
try cachedDependenciesWriterFactory.generate(output: allCompilations.info.swiftDependencies)
// Generate .d file with all deps in the "-master.d" (e.g. for WMO)
if let wmoDeps = allCompilations.info.dependencies {
try cachedDependenciesWriterFactory.generate(output: wmoDeps)
}
infoLog("Swiftc noop for \(context.target)")
return .success
}
}
@@ -0,0 +1,105 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public struct SwiftcContext {
enum SwiftcMode: Equatable {
case producer
/// Commit sha of the commit to use during remote cache
case consumer(commit: RemoteCommitInfo)
/// Remote artifact exists and can be optimistically used in place of a local compilation
case producerFast
}
let objcHeaderOutput: URL
let moduleName: String
let modulePathOutput: URL
/// File that defines output files locations (.d, .swiftmodule etc.)
let filemap: URL
let target: String
/// File that contains input files for the swift module compilation
let fileList: URL
let tempDir: URL
let arch: String
let prebuildDependenciesPath: String
let mode: SwiftcMode
/// File that stores all compilation invocation arguments
let invocationHistoryFile: URL
public init(
config: XCRemoteCacheConfig,
objcHeaderOutput: String,
moduleName: String,
modulePathOutput: String,
filemap: String,
target: String,
fileList: String
) throws {
self.objcHeaderOutput = URL(fileURLWithPath: objcHeaderOutput)
self.moduleName = moduleName
self.modulePathOutput = URL(fileURLWithPath: modulePathOutput)
self.filemap = URL(fileURLWithPath: filemap)
self.target = target
self.fileList = URL(fileURLWithPath: fileList)
// modulePathOutput is place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/$TARGET_NAME.swiftmodule
// That may be subject to change for other Xcode versions
tempDir = URL(fileURLWithPath: modulePathOutput)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
arch = URL(fileURLWithPath: modulePathOutput).deletingLastPathComponent().lastPathComponent
let srcRoot: URL = URL(fileURLWithPath: config.sourceRoot)
let remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: srcRoot)
prebuildDependenciesPath = config.prebuildDiscoveryPath
switch config.mode {
case .consumer:
let remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
mode = .consumer(commit: remoteCommit)
case .producer:
mode = .producer
case .producerFast:
let remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
switch remoteCommit {
case .unavailable:
mode = .producer
case .available:
mode = .producerFast
}
}
invocationHistoryFile = URL(fileURLWithPath: config.compilationHistoryFile, relativeTo: tempDir)
}
init(
config: XCRemoteCacheConfig,
input: SwiftcArgInput
) throws {
try self.init(
config: config,
objcHeaderOutput: input.objcHeaderOutput,
moduleName: input.moduleName,
modulePathOutput: input.modulePathOutput,
filemap: input.filemap,
target: input.target,
fileList: input.fileList
)
}
}
@@ -0,0 +1,157 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Errors with reading swiftc inputs
enum SwiftcInputReaderError: Error {
case readingFailed
case invalidFormat
case missingField(String)
}
/// Reads SwiftC filemap that specifies all input and output files
/// for the compilation
protocol SwiftcInputReader {
func read() throws -> SwiftCompilationInfo
}
/// Modifies compilation info
protocol SwiftcInputWriter {
func write(_ info: SwiftCompilationInfo) throws
}
struct SwiftCompilationInfo: Encodable, Equatable {
var info: SwiftModuleCompilationInfo
var files: [SwiftFileCompilationInfo]
}
struct SwiftModuleCompilationInfo: Encodable, Equatable {
// not present for incremental builds
let dependencies: URL?
let swiftDependencies: URL
}
struct SwiftFileCompilationInfo: Encodable, Equatable {
let file: URL
// not present for WMO builds
let dependencies: URL?
// not present for 'indexbuild' builds
let object: URL?
// not present for WMO builds
let swiftDependencies: URL?
}
class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
private let file: URL
private let fileManager: FileManager
init(_ file: URL, fileManager: FileManager) {
self.file = file
self.fileManager = fileManager
}
func read() throws -> SwiftCompilationInfo {
guard let content = fileManager.contents(atPath: file.path) else {
throw SwiftcInputReaderError.readingFailed
}
guard let representation = try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any] else {
throw SwiftcInputReaderError.invalidFormat
}
return try SwiftCompilationInfo(from: representation)
}
func write(_ info: SwiftCompilationInfo) throws {
let data = try JSONSerialization.data(withJSONObject: info.dump(), options: [.prettyPrinted])
fileManager.createFile(atPath: file.path, contents: data, attributes: nil)
}
}
extension SwiftCompilationInfo {
init(from object: [String: Any]) throws {
info = try SwiftModuleCompilationInfo(from: object[""])
files = try object.reduce([]) { prev, new in
let (key, value) = new
if key.isEmpty {
return prev
}
let fileInfo = try SwiftFileCompilationInfo(name: key, from: value)
return prev + [fileInfo]
}
}
func dump() -> [String: Any] {
return files.reduce(["": info.dump()]) { prev, info in
var result = prev
result[info.file.path] = info.dump()
return result
}
}
}
extension SwiftModuleCompilationInfo {
init(from object: Any?) throws {
guard let dict = object as? [String: String] else {
throw SwiftcInputReaderError.invalidFormat
}
swiftDependencies = try dict.readURL(key: "swift-dependencies")
dependencies = dict.readURL(key: "dependencies")
}
func dump() -> [String: String] {
return [
"dependencies": dependencies?.path,
"swift-dependencies": swiftDependencies.path,
].compactMapValues { $0 }
}
}
extension SwiftFileCompilationInfo {
init(name: String, from inputObject: Any) throws {
guard let dict = inputObject as? [String: String] else {
throw SwiftcInputReaderError.invalidFormat
}
file = URL(fileURLWithPath: name)
dependencies = dict.readURL(key: "dependencies")
object = dict.readURL(key: "object")
swiftDependencies = dict.readURL(key: "swift-dependencies")
}
func dump() -> [String: String] {
return [
"dependencies": dependencies?.path,
"object": object?.path,
"swift-dependencies": swiftDependencies?.path,
].compactMapValues { $0 }
}
}
private extension Dictionary where Key == String, Value == String {
func readURL(key: String) throws -> URL {
guard let value = self[key].map(URL.init(fileURLWithPath:)) else {
throw SwiftcInputReaderError.missingField(key)
}
return value
}
func readURL(key: String) -> URL? {
return self[key].map(URL.init(fileURLWithPath:))
}
}
@@ -0,0 +1,141 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Performs the `swiftc` logic
/// Depending on the mode, tries to mock the compilation (consumer)
/// or generates and uploads artifacts (producer)
class SwiftcOrchestrator {
private let swiftc: SwiftcProtocol
private let mode: SwiftcContext.SwiftcMode
// swiftc command that should be called to generate artifacts
private let swiftcCommand: String
private let objcHeaderOutput: URL
private let moduleOutput: URL
private let arch: String
private let artifactBuilder: ArtifactSwiftProductsBuilder
private let shellOut: ShellOut
private let producerFallbackCommandProcessors: [ShellCommandsProcessor]
private let invocationStorage: InvocationStorage
init(
mode: SwiftcContext.SwiftcMode,
swiftc: SwiftcProtocol,
swiftcCommand: String,
objcHeaderOutput: URL,
moduleOutput: URL,
arch: String,
artifactBuilder: ArtifactSwiftProductsBuilder,
producerFallbackCommandProcessors: [ShellCommandsProcessor],
invocationStorage: InvocationStorage,
shellOut: ShellOut
) {
self.mode = mode
self.swiftc = swiftc
self.swiftcCommand = swiftcCommand
self.objcHeaderOutput = objcHeaderOutput
self.moduleOutput = moduleOutput
self.arch = arch
self.artifactBuilder = artifactBuilder
self.producerFallbackCommandProcessors = producerFallbackCommandProcessors
self.invocationStorage = invocationStorage
self.shellOut = shellOut
}
private var invocationArgs: [String] {
let args = ProcessInfo().arguments
// first arg is a path to the current command, drop it
return Array(args.dropFirst())
}
private func fallbackToDefault(command: String = "swiftc") {
defaultLog("Fallbacking to compilation using \(command).")
shellOut.switchToExternalProcess(command: command, invocationArgs: invocationArgs)
}
private func fallbackToDefaultAndWait(command: String = "swiftc", args: [String]) throws {
defaultLog("Fallbacking to compilation using \(command).")
do {
try shellOut.callExternalProcessAndWait(
command: command,
invocationArgs: Array(args.dropFirst()),
envs: ProcessInfo.processInfo.environment
)
} catch ShellError.statusError(_, let exitCode) {
exit(exitCode)
}
}
/// calls all invocations one-by-one
/// - Parameter invocations: array or invocations: a command and all arguments
private func callExternalInvocations(invocations: [[String]]) throws {
try invocations.forEach { fullInvocation in
guard let command = fullInvocation.first else {
throw InvocationStorageError.corruptedStorage
}
try fallbackToDefaultAndWait(command: command, args: fullInvocation)
}
}
func run() throws {
switch mode {
case .consumer(.available):
let compileStepResult = try swiftc.mockCompilation()
do {
if case .forceFallback = compileStepResult {
// last-time fallback (probably a new swift file was added to the target)
// we are responsible to call all gathered compilation steps in compilation history
let historyCommandsToCall = try invocationStorage.retrieveAll()
try callExternalInvocations(invocations: historyCommandsToCall)
fallbackToDefault(command: swiftcCommand)
} else {
// save the current compilation invocation to the history file
try invocationStorage.store(args: invocationArgs)
}
} catch {
// The critical section is protected by a lock. Some other process already called compilation history
// We only need to call our current step then
fallbackToDefault(command: swiftcCommand)
}
case .consumer:
fallbackToDefault(command: swiftcCommand)
case .producerFast:
let compileStepResult = try swiftc.mockCompilation()
if case .forceFallback = compileStepResult {
// cannot reuse cached artifact. Build it locally and upload to the server just as for the producer
fallthrough
}
case .producer:
var swiftcArgs = ProcessInfo().arguments
swiftcArgs = try producerFallbackCommandProcessors.reduce(swiftcArgs) { args, processor in
try processor.applyArgsRewrite(args)
}
try fallbackToDefaultAndWait(command: swiftcCommand, args: swiftcArgs)
// move generated .h to the location where artifact creator expects it
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
// move generated .swiftmodule to the location where artifact creator expects it
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
try producerFallbackCommandProcessors.forEach {
try $0.postCommandProcessing()
}
}
}
}
@@ -0,0 +1,28 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Extends the swiftc product generation (when consuming cached artifact(s))
protocol SwiftcProductGenerationPlugin {
/// Allows to extend the production generation
/// - Parameter for: info of all compilation files passed to the swiftc invocation
func generate(for: SwiftCompilationInfo) throws
}
@@ -0,0 +1,98 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum DiskSwiftcProductsGeneratorError: Error {
/// When a generator was asked to generate unknown swiftmodule extension file
/// Probably a programmer error: asking to generate excessive extensions, not listed in
/// `SwiftmoduleFileExtension.SwiftmoduleExtensions`
case unknownSwiftmoduleFile
}
struct SwiftcProductsGeneratorOutput {
let swiftmoduleDir: URL
let objcHeaderFile: URL
}
/// Generates swiftc product to the expected location
protocol SwiftcProductsGenerator {
/// Generates products from given files
/// - Returns: location dir where .swiftmodule and ObjC header files have been placed
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> SwiftcProductsGeneratorOutput
}
/// Generator that produces all products in the locations where Xcode expects it, using provided disk copier
class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
private let destinationSwiftmodulePaths: [SwiftmoduleFileExtension: URL]
private let modulePathOutput: URL
private let objcHeaderOutput: URL
private let diskCopier: DiskCopier
init(
modulePathOutput: URL,
objcHeaderOutput: URL,
diskCopier: DiskCopier
) {
self.modulePathOutput = modulePathOutput
let modulePathBasename = modulePathOutput.deletingPathExtension()
// all swiftmodule-related should be located next to the ".swiftmodule"
destinationSwiftmodulePaths = Dictionary(
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
.map { ext, _ in
(ext, modulePathBasename.appendingPathExtension(ext.rawValue))
}
)
self.objcHeaderOutput = objcHeaderOutput
self.diskCopier = diskCopier
}
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> SwiftcProductsGeneratorOutput {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
let dest = destinationSwiftmodulePaths[ext]
guard let destination = dest else {
throw DiskSwiftcProductsGeneratorError.unknownSwiftmoduleFile
}
do {
// Move cached .swiftmodule to the expected location
try diskCopier.copy(file: url, destination: destination)
} catch {
if case .required = SwiftmoduleFileExtension.SwiftmoduleExtensions[ext] {
throw error
} else {
infoLog("Optional .\(ext) file not found in the artifact at: \(destination.path)")
}
}
}
// Build parent dir of the .swiftmodule file that contains a module
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
objcHeaderFile: objcHeaderOutput
)
}
}
@@ -0,0 +1,153 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
public struct SwiftcArgInput {
let objcHeaderOutput: String
let moduleName: String
let modulePathOutput: String
let filemap: String
let target: String
let fileList: String
/// Manual initializer implementation required to be public
public init(
objcHeaderOutput: String,
moduleName: String,
modulePathOutput: String,
filemap: String,
target: String,
fileList: String
) {
self.objcHeaderOutput = objcHeaderOutput
self.moduleName = moduleName
self.modulePathOutput = modulePathOutput
self.filemap = filemap
self.target = target
self.fileList = fileList
}
}
public class XCSwiftc {
private let command: String
private let inputArgs: SwiftcArgInput
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
private let touchFactory: (URL, FileManager) -> Touch
public init(
command: String,
inputArgs: SwiftcArgInput,
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
touchFactory: @escaping (URL, FileManager) -> Touch
) {
self.command = command
self.inputArgs = inputArgs
dependenciesWriterFactory = dependenciesWriter
self.touchFactory = touchFactory
}
// swiftlint:disable:next function_body_length
public func run() {
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: SwiftcContext
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
context = try SwiftcContext(config: config, input: inputArgs)
} catch {
exit(1, "FATAL: Swiftc initialization failed with error: \(error)")
}
let swiftcCommand = config.swiftcCommand
let markerURL = context.tempDir.appendingPathComponent(config.modeMarkerPath)
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
let markerWriter = FileMarkerWriter(markerURL, fileAccessor: fileManager)
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
let artifactOrganizer = ZipArtifactOrganizer(
targetTempDir: context.tempDir,
// xcswiftc doesn't call artifact preprocessing
artifactProcessors: [],
fileManager: fileManager
)
// TODO: check for allowedFile comparing a list of all inputfiles, not dependencies from a marker
let makerReferencedFilesListScanner = FileListScannerImpl(markerReader, caseSensitive: false)
let allowedFilesListScanner = ExceptionsFilteredFileListScanner(
allowedFilenames: ["\(config.thinTargetMockFilename).swift"],
disallowedFilenames: [],
scanner: makerReferencedFilesListScanner
)
let artifactBuilder: ArtifactSwiftProductsBuilder = ArtifactSwiftProductsBuilderImpl(
workingDir: context.tempDir,
moduleName: context.moduleName,
fileManager: fileManager
)
let productsGenerator = DiskSwiftcProductsGenerator(
modulePathOutput: context.modulePathOutput,
objcHeaderOutput: context.objcHeaderOutput,
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
)
let allInvocationsStorage = ExistingFileStorage(
storageFile: context.invocationHistoryFile,
command: swiftcCommand
)
// When fallbacking to local compilation do not call historical `swiftc` invocations
// The current fallback invocation already compiles all files in a target
let invocationStorage = FilteredInvocationStorage(
storage: allInvocationsStorage,
retrieveIgnoredCommands: [swiftcCommand]
)
let shellOut = ProcessShellOut()
let swiftc = Swiftc(
inputFileListReader: fileListEditor,
markerReader: markerReader,
allowedFilesListScanner: allowedFilesListScanner,
artifactOrganizer: artifactOrganizer,
inputReader: inputReader,
context: context,
markerWriter: markerWriter,
productsGenerator: productsGenerator,
fileManager: fileManager,
dependenciesWriterFactory: dependenciesWriterFactory,
touchFactory: touchFactory,
plugins: []
)
let orchestrator = SwiftcOrchestrator(
mode: context.mode,
swiftc: swiftc,
swiftcCommand: swiftcCommand,
objcHeaderOutput: context.objcHeaderOutput,
moduleOutput: context.modulePathOutput,
arch: context.arch,
artifactBuilder: artifactBuilder,
producerFallbackCommandProcessors: [],
invocationStorage: invocationStorage,
shellOut: shellOut
)
do {
try orchestrator.run()
} catch {
exit(1, "Swiftc failed with error: \(error)")
}
}
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
public enum Mode: String, Codable, CaseIterable {
case consumer
case producer
case producerFast = "producer-fast"
}
@@ -0,0 +1,397 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
import Yams
public enum XCRemoteCacheConfigError: Error {
/// Obligatory configuration property is missing
case missingConfiguration(name: String)
}
public struct XCRemoteCacheConfig: Encodable {
/// Remote cache schema version. Bump that version if RC artifact generation introduces breaking changes
let schemaVersion = "5"
/// Mode: consumer|producer, defaults to consumer
var mode: Mode = .consumer
/// Address of all remote cache replicas. The best one (with the quickest response) will be chose in xcprepare step
/// Required to be non-empty array
var cacheAddresses: [String] = []
/// Address of the remote cache to use in the consumer mode
/// If not specified, the first item in `cacheAddresses` will be used
var recommendedCacheAddress: String = ""
/// Probe request path to the `cacheAddresses` (relative to `cacheAddresses`)
/// that determines the best cache to use (with the lowest latency)
var cacheHealthPath: String = "nginx-health"
/// Number of `cacheAddresses` probe requests
var cacheHealthPathProbeCount: Int = 3
/// Filepath to the file to the remote commit sha
var remoteCommitFile: String = "build/remote-cache/arc.rc"
/// Filepath to create xccc wrapper (that value should be equal to Xcode's CC BuildSetting)
var xcccFile: String = "build/bin/xccc"
/// Path, relative to $TARGET_TEMP_DIR which specifies prebuild discovery .d file
var prebuildDiscoveryPath: String = "prebuild.d"
/// Path, relative to $TARGET_TEMP_DIR which specifies postbuild discovery .d file
var postbuildDiscoveryPath: String = "postbuild.d"
/// Path, relative to $TARGET_TEMP_DIR of a maker file to enable (when exists) or disable (when missing)
/// Remote cache mode
/// Includes a list of all allowed input files to use remote cache
var modeMarkerPath: String = "rc.enabled"
/// Command for a standard C compilation (cc)
var clangCommand: String = "clang"
/// Command for a standard Swift compilation (swiftc)
var swiftcCommand: String = "swiftc"
/// Path of the primary repository that produces cache artifacts
var primaryRepo: String = ""
/// Main (primary) branch that produces cache artifacts (default to 'master')
var primaryBranch: String = "master"
/// Path to the git repo root
var repoRoot: String = "."
/// Number of historical commits to look for a cache artifacts
var cacheCommitHistory: Int = 10
/// Source root of the Xcode project
var sourceRoot: String
/// Fingerprint override extension (sample override `Module.swiftmodule/x86_64.swiftmodule.md5`)
var fingerprintOverrideExtension: String = "md5"
/// Optional configuration file that overrides project configuration
var extraConfigurationFile: String = "user.rcinfo"
/// Custom commit sha to publish artifact
var publishingSha: String?
/// Maximum age in days for artifact to be cached before being evicted
var artifactMaximumAge: Int = 30
/// Extra ENV keys that should be convoluted into the environment fingerprint
var customFingerprintEnvs: [String] = []
/// Root directory where all XCRemoteCache statistics (e.g. counters) are stored
var statsDir: String = "~/.xccache"
/// Number of retries for download requests
var downloadRetries: Int = 0
/// Number of retries for upload requests
var uploadRetries: Int = 3
/// Delay between retries in seconds
var retryDelay: Double = 10.0
/// Maximum number of simultaneous requests. 0 means no limits
var uploadBatchSize: Int = 0
/// Extra headers appended to all remote HTTP(S) requests
var requestCustomHeaders: [String: String] = [:]
/// Filename (without an extension) of the compilation input file that is used
/// as a fake compilation for the forced-cached target (aka thin target)
/// The filename has to be exclusive nor a suffix of any compilation file in a target
var thinTargetMockFilename: String = "standin"
/// A List of all targets that are not thin. If an empty array, all targets are meant to be non-thin
/// A 'thin' target is a target-level mode that forces the cached artifact
var focusedTargets: [String] = []
/// Disable cache for http requests to fecth metadata and download artifacts
var disableHttpCache: Bool = false
/// Path, relative to $TARGET_TEMP_DIR which gathers all compilation commands that should be e
/// xecuted if a target switches to local compilation
/// Example: A new `.swift` file invalidates remote arXcodeProjIntegrate.swifttifact and triggers local compilation
/// When that happens, all previously skipped clang build steps
/// need to be eventually called locally - this file lists all these commands
var compilationHistoryFile: String = "history.compile"
/// Timeout for remote response data interval (in seconds). If an interval between data chunks is
/// longer than a timeout, a request fails
var timeoutResponseDataChunksInterval: Double = 20
/// It true, any observed request timeout switches off remote cache for all targets
var turnOffRemoteCacheOnFirstTimeout: Bool = false
/// List of all extensions that should carry over source fingerprints. Extensions of all product files that
/// contain non-deterministic content (absolute paths, timestamp, etc) should be included
/// .h files may contain absolute paths if NS_ENUM is used in a public API from Swift code
var productFilesExtensionsWithContentOverride = ["swiftmodule", "h"]
/// If true, plugins for thinning support should be enabled
var thinningEnabled: Bool = false
/// Module name of a target that works as a helper for thinned targets
var thinningTargetModuleName: String = "ThinningRemoteCacheModule"
/// Opt-in pretty json formatting for meta files
var prettifyMetaFiles: Bool = false
/// Secret key for AWS V4 Signature, if this is set the Authentication Header will be added
var AWSSecretKey: String = ""
/// Access key for AWS V4 Signature
var AWSAccessKey: String = ""
/// Temporary security token provided by the AWS Security Token Service
var AWSSecurityToken: String?
/// Region for AWS V4 Signature (e.g. `eu`)
var AWSRegion: String = ""
/// Service for AWS V4 Signature (e.g. `storage`)
var AWSService: String = ""
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of
/// dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled
/// dependencies. Keys represent generic replacement and values are substrings that should be replaced
/// Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]`
/// `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`)
/// Warning: remapping order is not-deterministic so avoid remappings with multiple matchings
var outOfBandMappings: [String: String] = [:]
/// If true, SSL certificate validation is disabled
var disableCertificateVerification: Bool = false
/// A feature flag to disable virtual file system overlay support (temporary)
var disableVFSOverlay: Bool = false
/// A list of extra ENVs that should be used as placeholders in the dependency list
/// ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process
var customRewriteEnvs: [String] = []
/// Regexes of files that should not be included in a list of dependencies. Warning! Add entries here
/// with caution - excluding dependencies that are relevant might lead to a target overcaching
/// Note: The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude
/// all `.modulemap` files
var irrelevantDependenciesPaths: [String] = []
/// If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch
/// That might useful on CI, where a shallow clone is used
var gracefullyHandleMissingCommonSha: Bool = false
}
extension XCRemoteCacheConfig {
/// Merges existing config with the other config and returns a final result
/// `other` scheme overrides existing configuration
// swiftlint:disable:next function_body_length
func merged(with scheme: ConfigFileScheme) -> XCRemoteCacheConfig {
var merge = self
merge.mode = scheme.mode ?? mode
merge.recommendedCacheAddress = scheme.recommendedCacheAddress ?? recommendedCacheAddress
merge.cacheAddresses = scheme.cacheAddresses ?? cacheAddresses
merge.cacheHealthPath = scheme.cacheHealthPath ?? cacheHealthPath
merge.cacheHealthPathProbeCount = scheme.cacheHealthPathProbeCount ?? cacheHealthPathProbeCount
merge.remoteCommitFile = scheme.remoteCommitFile ?? remoteCommitFile
merge.xcccFile = scheme.xcccFile ?? xcccFile
merge.prebuildDiscoveryPath = scheme.prebuildDiscoveryPath ?? prebuildDiscoveryPath
merge.postbuildDiscoveryPath = scheme.postbuildDiscoveryPath ?? postbuildDiscoveryPath
merge.modeMarkerPath = scheme.modeMarkerPath ?? modeMarkerPath
merge.clangCommand = scheme.clangCommand ?? clangCommand
merge.swiftcCommand = scheme.swiftcCommand ?? swiftcCommand
merge.primaryRepo = scheme.primaryRepo ?? primaryRepo
merge.primaryBranch = scheme.primaryBranch ?? primaryBranch
merge.repoRoot = scheme.repoRoot ?? repoRoot
merge.cacheCommitHistory = scheme.cacheCommitHistory ?? cacheCommitHistory
merge.fingerprintOverrideExtension = scheme.fingerprintOverrideExtension ?? fingerprintOverrideExtension
merge.extraConfigurationFile = scheme.extraConfigurationFile ?? extraConfigurationFile
merge.publishingSha = scheme.publishingSha ?? publishingSha
merge.artifactMaximumAge = scheme.artifactMaximumAge ?? artifactMaximumAge
merge.customFingerprintEnvs = scheme.customFingerprintEnvs ?? customFingerprintEnvs
merge.statsDir = scheme.statsDir ?? statsDir
merge.downloadRetries = scheme.downloadRetries ?? downloadRetries
merge.uploadRetries = scheme.uploadRetries ?? uploadRetries
merge.retryDelay = scheme.retryDelay ?? retryDelay
merge.uploadBatchSize = scheme.uploadBatchSize ?? uploadBatchSize
merge.requestCustomHeaders = scheme.requestCustomHeaders ?? requestCustomHeaders
merge.thinTargetMockFilename = scheme.thinTargetMockFilename ?? thinTargetMockFilename
merge.focusedTargets = scheme.focusedTargets ?? focusedTargets
merge.disableHttpCache = scheme.disableHttpCache ?? disableHttpCache
merge.compilationHistoryFile = scheme.compilationHistoryFile ?? compilationHistoryFile
merge.timeoutResponseDataChunksInterval =
scheme.timeoutResponseDataChunksInterval ?? timeoutResponseDataChunksInterval
merge.turnOffRemoteCacheOnFirstTimeout =
scheme.turnOffRemoteCacheOnFirstTimeout ?? turnOffRemoteCacheOnFirstTimeout
merge.productFilesExtensionsWithContentOverride =
scheme.productFilesExtensionsWithContentOverride ?? productFilesExtensionsWithContentOverride
merge.thinningEnabled = scheme.thinningEnabled ?? thinningEnabled
merge.thinningTargetModuleName = scheme.thinningTargetModuleName ?? thinningTargetModuleName
merge.prettifyMetaFiles = scheme.prettifyMetaFiles ?? prettifyMetaFiles
merge.AWSAccessKey = scheme.AWSAccessKey ?? AWSAccessKey
merge.AWSSecretKey = scheme.AWSSecretKey ?? AWSSecretKey
merge.AWSSecurityToken = scheme.AWSSecurityToken ?? AWSSecurityToken
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
merge.AWSService = scheme.AWSService ?? AWSService
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
merge.disableCertificateVerification = scheme.disableCertificateVerification ?? disableCertificateVerification
merge.disableVFSOverlay = scheme.disableVFSOverlay ?? disableVFSOverlay
merge.customRewriteEnvs = scheme.customRewriteEnvs ?? customRewriteEnvs
merge.irrelevantDependenciesPaths = scheme.irrelevantDependenciesPaths ?? irrelevantDependenciesPaths
merge.gracefullyHandleMissingCommonSha =
scheme.gracefullyHandleMissingCommonSha ?? gracefullyHandleMissingCommonSha
return merge
}
/// Verifies all required properties and set defualts
/// - Throws: `XCRemoteCacheConfigError` if the configuration is invalid
/// - Returns: valid `XCRemoteCacheConfig` with configured defaults
func verifyAndApplyDefaults() throws -> XCRemoteCacheConfig {
var newConfig = self
guard let fallbackCacheAddress = cacheAddresses.first else {
throw XCRemoteCacheConfigError.missingConfiguration(name: "cache_addresses")
}
if recommendedCacheAddress.isEmpty {
newConfig.recommendedCacheAddress = fallbackCacheAddress
}
return newConfig
}
}
/// A scheme of the user-specific overrides of configs
struct ConfigFileScheme: Decodable {
let mode: Mode?
let recommendedCacheAddress: String?
let cacheAddresses: [String]?
let cacheHealthPath: String?
let cacheHealthPathProbeCount: Int?
let remoteCommitFile: String?
let xcccFile: String?
let prebuildDiscoveryPath: String?
let postbuildDiscoveryPath: String?
let modeMarkerPath: String?
let clangCommand: String?
let swiftcCommand: String?
let primaryRepo: String?
let primaryBranch: String?
let repoRoot: String?
let cacheCommitHistory: Int?
let fingerprintOverrideExtension: String?
let extraConfigurationFile: String?
let publishingSha: String?
let artifactMaximumAge: Int?
let customFingerprintEnvs: [String]?
let statsDir: String?
let downloadRetries: Int?
let uploadRetries: Int?
let retryDelay: Double?
let uploadBatchSize: Int?
let requestCustomHeaders: [String: String]?
let thinTargetMockFilename: String?
let focusedTargets: [String]?
let disableHttpCache: Bool?
let compilationHistoryFile: String?
let timeoutResponseDataChunksInterval: Double?
let turnOffRemoteCacheOnFirstTimeout: Bool?
let productFilesExtensionsWithContentOverride: [String]?
let thinningEnabled: Bool?
let thinningTargetModuleName: String?
let prettifyMetaFiles: Bool?
let AWSSecretKey: String?
let AWSAccessKey: String?
let AWSSecurityToken: String?
let AWSRegion: String?
let AWSService: String?
let outOfBandMappings: [String: String]?
let disableCertificateVerification: Bool?
let disableVFSOverlay: Bool?
let customRewriteEnvs: [String]?
let irrelevantDependenciesPaths: [String]?
let gracefullyHandleMissingCommonSha: Bool?
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
case mode
case recommendedCacheAddress = "recommended_cache_address"
case cacheAddresses = "cache_addresses"
case cacheHealthPath = "cache_health_path"
case cacheHealthPathProbeCount = "cache_health_path_probe_count"
case remoteCommitFile = "remote_commit_file"
case xcccFile = "xccc_file"
case prebuildDiscoveryPath = "prebuild_discovery_path"
case postbuildDiscoveryPath = "postbuild_discovery_path"
case modeMarkerPath = "mode_marker_path"
case clangCommand = "clang_command"
case swiftcCommand = "swiftc_command"
case primaryRepo = "primary_repo"
case primaryBranch = "primary_branch"
case repoRoot = "repo_root"
case cacheCommitHistory = "cache_commit_history"
case fingerprintOverrideExtension = "fingerprint_override_extension"
case extraConfigurationFile = "extra_configuration_file"
case publishingSha = "publishing_sha"
case artifactMaximumAge = "artifact_maximum_age"
case customFingerprintEnvs = "custom_fingerprint_envs"
case statsDir = "stats_dir"
case downloadRetries = "download_retries"
case uploadRetries = "upload_retries"
case retryDelay = "retry_delay"
case uploadBatchSize = "upload_batch_size"
case requestCustomHeaders = "request_custom_headers"
case thinTargetMockFilename = "thin_target_mock_filename"
case focusedTargets = "focused_targets"
case disableHttpCache = "disable_http_cache"
case compilationHistoryFile = "compilation_history_file"
case timeoutResponseDataChunksInterval = "timeout_response_data_chunks_interval"
case turnOffRemoteCacheOnFirstTimeout = "turn_off_remote_cache_on_first_timeout"
case productFilesExtensionsWithContentOverride = "product_files_extensions_with_content_override"
case thinningEnabled = "thinning_enabled"
case thinningTargetModuleName = "thinning_target_module_name"
case prettifyMetaFiles = "prettify_meta_files"
case AWSSecretKey = "aws_secret_key"
case AWSAccessKey = "aws_access_key"
case AWSSecurityToken = "aws_security_token"
case AWSRegion = "aws_region"
case AWSService = "aws_service"
case outOfBandMappings = "out_of_band_mappings"
case disableCertificateVerification = "disable_certificate_verification"
case disableVFSOverlay = "disable_vfs_overlay"
case customRewriteEnvs = "custom_rewrite_envs"
case irrelevantDependenciesPaths = "irrelevant_dependencies_paths"
case gracefullyHandleMissingCommonSha = "gracefully_handle_missing_common_sha"
}
}
enum XCRemoteCacheConfigReaderError: Error {
case missingConfigurationFile(URL)
case invalidConfiguration
}
class XCRemoteCacheConfigReader {
/// Name of the configuration file, required in $(SRCROOT) location
private static let configurationFile = ".rcinfo"
private let srcRoot: String
private let fileReader: FileReader
private lazy var yamlDecorer = YAMLDecoder(encoding: .utf8)
init(env: [String: String], fileReader: FileReader) throws {
let explicitSrcRoot: String? = env.readEnv(key: "SRCROOT")
srcRoot = explicitSrcRoot ?? FileManager.default.currentDirectoryPath
self.fileReader = fileReader
}
init(srcRootPath srcRoot: String, fileReader: FileReader) {
self.srcRoot = srcRoot
self.fileReader = fileReader
}
// Reads the final configuration by loading all extra configs
// until reaching a config that doesn't override `extraConfigurationFile`
func readConfiguration() throws -> XCRemoteCacheConfig {
let rootURL = URL(fileURLWithPath: srcRoot)
let configURL = URL(fileURLWithPath: Self.configurationFile, relativeTo: rootURL)
let userConfigs = try readUserConfig(configURL)
var config = XCRemoteCacheConfig(sourceRoot: srcRoot).merged(with: userConfigs)
var extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
var visitedFiles = Set([configURL])
while !visitedFiles.contains(extraConfURL) {
do {
let extraConfig = try readUserConfig(extraConfURL)
debugLog("Reading extra configuration from \(extraConfURL)")
config = config.merged(with: extraConfig)
visitedFiles.insert(extraConfURL)
// Advance extra configuration
extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
} catch {
infoLog("Extra config override failed with \(error). Skipping extra configuration")
// swiftlint:disable:next unneeded_break_in_switch
break
}
}
return try config.verifyAndApplyDefaults()
}
/// Reads user configuration from a file
private func readUserConfig(_ file: URL) throws -> ConfigFileScheme {
let configurationContent = try fileReader.contents(atPath: file.path)
guard let configurationData = configurationContent else {
throw XCRemoteCacheConfigReaderError.missingConfigurationFile(file)
}
guard let configurationString = String(data: configurationData, encoding: .utf8) else {
throw XCRemoteCacheConfigReaderError.invalidConfiguration
}
return try yamlDecorer.decode(from: configurationString)
}
}
@@ -0,0 +1,88 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Generates (producer) or moves (consumer) dSYM directory to include
protocol DSYMOrganizer {
/// Returns location of the existing dSYM directory, nil when dSYM is not required to share in the artifact
/// In the 'producer' mode, for a non-static library that haven't already generated dSYM (Product is just "DWARF"),
/// generates dSYM anyway to share debugging symbols with the artifact
///
/// - Returns: Path to the available dSYM package (generated or already existing)
func relevantDSYMLocation() throws -> URL?
/// Moves dSYM to the final destination, if one exists in the cached artifact
/// - Parameter artifactPath: location of the unzipped artifact from cache
func syncDSYM(artifactPath: URL) throws
/// Removes all leftovers from previous dSYM synchronizations
func cleanup() throws
}
class DynamicDSYMOrganizer: DSYMOrganizer {
private let productURL: URL
private let dSYMPath: URL
private let machOType: MachOType
private let wasDsymGenerated: Bool
private let fileManager: FileManager
private let shellCall: ShellCallFunction
init(
productURL: URL,
machOType: MachOType,
dSYMPath: URL,
wasDsymGenerated: Bool,
fileManager: FileManager,
shellCall: @escaping ShellCallFunction
) {
self.productURL = productURL
self.machOType = machOType
self.dSYMPath = dSYMPath
self.wasDsymGenerated = wasDsymGenerated
self.fileManager = fileManager
self.shellCall = shellCall
}
func relevantDSYMLocation() throws -> URL? {
guard [.dynamicLib, .executable, .bundle].contains(machOType) else {
return nil
}
guard wasDsymGenerated == false else {
// dSYM has already been regerated
return dSYMPath
}
try shellCall("dsymutil", [productURL.path, "-o", dSYMPath.path], nil, ProcessInfo.processInfo.environment)
return dSYMPath
}
func syncDSYM(artifactPath: URL) throws {
let dSYMFileName = dSYMPath.lastPathComponent
let cachedDSYMPath = artifactPath.appendingPathComponent(dSYMFileName)
if fileManager.fileExists(atPath: cachedDSYMPath.path) {
try fileManager.spt_forceLinkItem(at: cachedDSYMPath, to: dSYMPath)
}
}
func cleanup() throws {
if !wasDsymGenerated && fileManager.fileExists(atPath: dSYMPath.path) {
try fileManager.removeItem(at: dSYMPath)
}
}
}
@@ -0,0 +1,117 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum PhaseCacheModeControllerError: Error {
/// Trying to disable remote cached for a target that is forced to use the cached artifact
case cannotUseRemoteCacheForForcedCacheMode
}
/// Controls Remote Cache mode:
protocol CacheModeController {
/// Enables remote cache for a set of allowed of input files (.swift or .m)
/// Any compilation for a file that is not on that list should fallback to the compilation mode (if possible)
/// or stop the build with error
func enable(allowedInputFiles: [URL], dependencies: [URL]) throws
/// Disables remote cache and fallbacks to source-compilation
func disable() throws
/// Returns true if remote cache mode is enabled
func isEnabled() throws -> Bool
/// Returns true if the mode controller should be disabled for that remote commit. That happens when some
/// xcswift, xccc etc. commands disabled remote cache (e.g. new file was added to the compilation)
func shouldDisable(for commit: RemoteCommitInfo) -> Bool
}
class PhaseCacheModeController: CacheModeController {
/// Path to the symbolic link that changes if other xcode is selected with `xcode-select -s`
static let xcodeSelectLink: URL = URL(fileURLWithPath: "/var/db/xcode_select_link")
private let mergeCommitFile: URL
private let modeMarker: URL
private let forceCached: Bool
private let dependenciesWriter: DependenciesWriter
private let dependenciesReader: DependenciesReader
private let markerWriter: MarkerWriter
private let fileManager: FileManager
init(
tempDir: URL,
mergeCommitFile: URL,
phaseDependencyPath: String,
markerPath: String,
forceCached: Bool,
dependenciesWriter: (URL, FileManager) -> DependenciesWriter,
dependenciesReader: (URL, FileManager) -> DependenciesReader,
markerWriter: (URL, FileManager) -> MarkerWriter,
fileManager: FileManager
) {
self.mergeCommitFile = mergeCommitFile
modeMarker = tempDir.appendingPathComponent(markerPath)
self.fileManager = fileManager
self.forceCached = forceCached
let discoveryURL = tempDir.appendingPathComponent(phaseDependencyPath)
self.dependenciesWriter = dependenciesWriter(discoveryURL, fileManager)
self.dependenciesReader = dependenciesReader(discoveryURL, fileManager)
self.markerWriter = markerWriter(modeMarker, fileManager)
}
func enable(allowedInputFiles: [URL], dependencies: [URL]) throws {
// marker file contains filepaths that contribute to the build products
// and should invalidate all other target steps (swiftc,libtool etc.)
let targetSensitiveFiles = dependencies + [modeMarker, Self.xcodeSelectLink]
try markerWriter.enable(dependencies: targetSensitiveFiles)
// All rc-phases (prebuid & postbuild) should be reenabled when new remote
// merge commit or other Xcode is used
let allDependencies = dependencies + [mergeCommitFile, Self.xcodeSelectLink]
try dependenciesWriter.writeGeneric(dependencies: allDependencies)
}
func disable() throws {
guard !forceCached else {
throw PhaseCacheModeControllerError.cannotUseRemoteCacheForForcedCacheMode
}
try markerWriter.disable()
// Do not try to use remote cache anymore unless new remote cache merge commit or xcode is in use
try dependenciesWriter.writeGeneric(dependencies: [mergeCommitFile, Self.xcodeSelectLink])
}
func isEnabled() throws -> Bool {
return fileManager.fileExists(atPath: modeMarker.path)
}
/// Returns true if the phase dependency file contains a ["skipForSha": "some_sha"] entry and
/// "some_sha" is equal to the `commit` argument
func shouldDisable(for commit: RemoteCommitInfo) -> Bool {
guard case .available(let commitValue) = commit else {
return true
}
do {
let rawDependencies = try dependenciesReader.readFilesAndDependencies()
if let commitToSkip = rawDependencies[FileDependenciesWriter.skipForShaKey] {
return commitToSkip.contains(commitValue)
}
} catch {
// Gracefully don't disable a cache
// That may happen if building a target for the first time
errorLog("Couldn't verify if should disable RC for \(commitValue).")
}
return false
}
}
@@ -0,0 +1,54 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Performant DependenciesWriter manager that reuses generated dependencies file
/// between multiple files that produce the same dependencies
/// This class is not thread-safe
class CachedFileDependenciesWriterFactory {
private let dependencies: [URL]
private let fileManager: FileManager
private let factory: (URL, FileManager) -> DependenciesWriter
private var templateDependencyFile: URL?
init(
dependencies: [URL],
fileManager: FileManager,
writerFactory: @escaping (URL, FileManager) -> DependenciesWriter
) {
self.dependencies = dependencies
self.fileManager = fileManager
factory = writerFactory
}
func generate(output: URL) throws {
if let template = templateDependencyFile {
try fileManager.spt_forceCopyItem(at: template, to: output)
return
}
// Generate the template file (happens only once)
let writer = factory(output, fileManager)
try writer.writeGeneric(dependencies: dependencies)
if fileManager.fileExists(atPath: output.path) {
// the file has been correctly created
templateDependencyFile = output
}
}
}
@@ -0,0 +1,41 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Manages a file that collects all compilation invocations
protocol CompilationHistoryOrganizer {
/// Cleans a state of clang history invocations
func reset()
}
/// Manages a list of invocations stored in a file
class CompilationHistoryFileOrganizer: CompilationHistoryOrganizer {
private let file: URL
private let fileManager: FileManager
init(_ file: URL, fileManager: FileManager) {
self.file = file
self.fileManager = fileManager
}
func reset() {
fileManager.createFile(atPath: file.path, contents: nil, attributes: nil)
}
}
@@ -0,0 +1,79 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum DatWriterError: Error {
/// Called when a string to dump to a file cannot be safely converted to bytes
case invalidStringToSave(string: String)
}
/// Writes step input and output files in a MachO format
protocol DatWriter {
func enable(dependencies: [URL], outputs: [URL]) throws
}
/// Implementation of the depedency-info data file writer
/// Mirrors clang implementation from `MachOLinkingContext::createDependencyFile`
/// http://llvm.org/viewvc/llvm-project/lld/trunk/lib/ReaderWriter/MachO/MachOLinkingContext.cpp?view=markup
class FileDatWriter: DatWriter {
private static let inputFileOpcode = Data([0x10])
private static let outputFileOpcode = Data([0x40])
private static let separator = Data([0x0])
private let file: URL
private let fileManager: FileManager
init(_ file: URL, fileManager: FileManager) {
self.file = file
self.fileManager = fileManager
}
/// Saves input and output dependencies to the `self.file` location
///
/// Sample output:
/// `{0x0}cctools-959.0.1{0x0}{0x10}inputFile1.swift{0x0}{0x10}inputFile2.m{0x0}{0x40}outputLibrary.a{0x0}`
func enable(dependencies: [URL], outputs: [URL]) throws {
var data = Self.separator
try data.append("cctools-959.0.1".spt_utf8())
data.append(Self.separator)
try dependencies.forEach { file in
data.append(Self.inputFileOpcode)
try data.append(file.path.spt_utf8())
data.append(Self.separator)
}
try outputs.forEach { file in
data.append(Self.outputFileOpcode)
try data.append(file.path.spt_utf8())
data.append(Self.separator)
}
try fileManager.spt_writeToFile(atPath: file.path, contents: data)
}
}
private extension String {
func spt_utf8() throws -> Data {
guard let content = data(using: .utf8) else {
throw DatWriterError.invalidStringToSave(string: self)
}
return content
}
}
@@ -0,0 +1,26 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum DependenciesMapping {
/// Specifies which ENVs should be rewritten in the dependencies generation to make generic (paths agnostics)
/// list of dependencies
static let rewrittenEnvs = ["BUILD_DIR", "SRCROOT"]
}
@@ -0,0 +1,171 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
import Yams
enum DependenciesReaderError: Error {
case readingError
case invalidFile
case invalidFormat
}
/// Readers for dependencies for a Make-format (.d) file
public protocol DependenciesReader {
/// Finds all dependencies paths
func findDependencies() throws -> [String]
/// Finds all files that were compiled
func findInputs() throws -> [String]
/// Reads raw dependency dictionary representation:
/// * key is a filename of the dependency (or some "magicals", like Xcode's 'dependencies' or 'skipForSha')
/// * value is an array of dependencies related with 'key' file
func readFilesAndDependencies() throws -> [String: [String]]
}
/// Parser for a single .d file
public class FileDependenciesReader: DependenciesReader {
private let file: URL
private let fileManager: FileManager
public init(_ file: URL, accessor: FileManager) {
self.file = file
fileManager = accessor
}
public func findDependencies() throws -> [String] {
let yaml = try readRaw()
let dependencies = yaml.reduce(Set<String>()) { prev, arg1 -> Set<String> in
let (key, value) = arg1
switch key {
case "dependencies":
// 'clang' output formatting
return Set(parseDependencyFileList(value))
case let s where s.hasSuffix(".o") || s.hasSuffix(".bc"):
// 'swiftc' output formatting
// take dependencies from any .o or .bc file
// Note: For WMO, all .{o|bc} files have the same dependencies
return Set(parseDependencyFileList(value))
default:
return prev
}
}
return Array(dependencies)
}
public func findInputs() throws -> [String] {
exit(1, "TODO: implement")
}
public func readFilesAndDependencies() throws -> [String: [String]] {
let yaml = try readRaw()
// files are space delimited
return yaml.mapValues { $0.components(separatedBy: .whitespaces) }
}
func readRaw() throws -> [String: String] {
let fileData = try getFileData()
let fileString = try getFileStringFromData(fileData: fileData)
let yaml = try getYaml(fileString: fileString)
return yaml
}
func getFileData() throws -> Data {
guard let fileData = fileManager.contents(atPath: file.path) else {
throw DependenciesReaderError.readingError
}
return fileData
}
func getFileStringFromData(fileData: Data) throws -> String {
guard let fileString = String(data: fileData, encoding: .utf8) else {
throw DependenciesReaderError.invalidFile
}
return fileString
}
func getYaml(fileString: String) throws -> [String: String] {
guard let yaml = try Yams.load(yaml: fileString) as? [String: String] else {
throw DependenciesReaderError.invalidFile
}
return yaml
}
/// Parses the String to get the list of files
/// It iterates over the String using its UTF8View since it is more performant (String type operates
/// in a higher abstraction level and supports features that have a negative impact in the performance)
/// It supports escaping whitespace charaters, prefixed with "\\"
/// - Parameter string: string of whitespace charaters separated file paths
/// - Returns: Array of all file paths
func parseDependencyFileList(_ string: String) -> [String] {
var result: [String] = []
var prevChar: UTF8.CodeUnit?
// These index are used to move over the UTF8View of the string
// The goal is to optimize the memory used, since UTF8View uses
// the same memory as the original String without copying it
var startIndex = string.utf8.startIndex
var endIndex = startIndex
// This buffer is only used to save the part of the path that has been already parsed when finding a backslash
var buffer: String = ""
for c in string.utf8 {
switch c {
case UTF8.CodeUnit(ascii: "\n") where prevChar == UTF8.CodeUnit(ascii: "\\"):
startIndex = string.utf8.index(after: startIndex)
endIndex = startIndex
case UTF8.CodeUnit(ascii: " ") where startIndex == endIndex && buffer.isEmpty:
startIndex = string.utf8.index(after: startIndex)
endIndex = startIndex
case UTF8.CodeUnit(ascii: " ") where prevChar != UTF8.CodeUnit(ascii: "\\"):
// If a space is found and it is not escaped, then that's the end of the file path
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
result.append(buffer)
buffer = ""
prevChar = nil
startIndex = string.utf8.index(after: endIndex)
endIndex = startIndex
case UTF8.CodeUnit(ascii: "\\"):
// If a backslash is found it is not included in the file path
// The current parsed range of the UTF8View is saved in the buffer as a String
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
// The backslash is assigned as the previous char
prevChar = c
// The indexes are moved to the next char so we continue parsing the String
startIndex = string.utf8.index(after: endIndex)
endIndex = startIndex
default:
// As long as it is possible the indexes are used to track the range of the string that
// will be included in the file path (until it ends or until a backslash is found)
endIndex = string.utf8.index(after: endIndex)
// The char is assigned as the previous char
prevChar = c
}
}
if startIndex != endIndex {
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
result.append(buffer)
}
return result
}
}
@@ -0,0 +1,79 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Replaces paths formats between generic (placeholders-based) and local
protocol DependenciesRemapper {
/// Replaces all generic paths (with placeholders) to a local paths
func replace(genericPaths: [String]) throws -> [String]
/// Replaces all local paths to the generic dependencies paths
func replace(localPaths: [String]) throws -> [String]
}
class DependenciesRemapperComposite: DependenciesRemapper {
private let remappers: [DependenciesRemapper]
init(_ remappers: [DependenciesRemapper]) {
self.remappers = remappers
}
func replace(genericPaths: [String]) throws -> [String] {
try remappers.reversed().reduce(genericPaths) { prev, mapper in
try mapper.replace(genericPaths: prev)
}
}
func replace(localPaths: [String]) throws -> [String] {
try remappers.reduce(localPaths) { prev, mapper in
try mapper.replace(localPaths: prev)
}
}
}
final class StringDependenciesRemapper: DependenciesRemapper {
struct Mapping {
let generic: String
let local: String
}
private let mappings: [Mapping]
init(mappings: [Mapping]) {
self.mappings = mappings
}
func replace(genericPaths: [String]) throws -> [String] {
return genericPaths.map { path in
let localPath = mappings.reversed().reduce(path) { prevPath, mapping in
prevPath.replacingOccurrences(of: mapping.generic, with: mapping.local)
}
return localPath
}
}
func replace(localPaths: [String]) throws -> [String] {
return localPaths.map { path in
let result = mappings.reduce(path) { prevPath, mapping in
prevPath.replacingOccurrences(of: mapping.local, with: mapping.generic)
}
return result
}
}
}
@@ -0,0 +1,69 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Writer for dependencies in a Make-format (.d file)
public protocol DependenciesWriter {
/// Saves a list of dependencies for a set of files
/// - Parameter dependencies: The dictionary where filepath is a key and an array it
/// its dependencies filepath are values
func write(dependencies: [String: [String]]) throws
/// Saves a XCRetemoCache custom dependencies format (valid .d format) that indicates skipping that phase up,
/// if the remote commit is equal to the provided `skipForSha`
func write(skipForSha: String) throws
}
extension DependenciesWriter {
/// Write dependency list for a single file
func write(file: URL, dependencies: [URL]) throws {
try write(dependencies: [file.path: dependencies.map { $0.path }])
}
}
public class FileDependenciesWriter: DependenciesWriter {
static let skipForShaKey = "skipForSha"
private let file: URL
public init(_ file: URL, accessor: FileManager) {
self.file = file
}
public func write(dependencies: [String: [String]]) throws {
var content = ""
for (file, deps) in dependencies {
content.append(file + ": ")
content.append(deps.map { $0.replacingOccurrences(of: " ", with: "\\ ") }.joined(separator: " "))
content.append("\n")
}
try content.write(to: file, atomically: true, encoding: .utf8)
}
public func write(skipForSha sha: String) throws {
try write(dependencies: [Self.skipForShaKey: [sha]])
}
}
extension DependenciesWriter {
func writeGeneric(dependencies: [URL]) throws {
try write(dependencies: ["dependencies": dependencies.map { $0.path }])
}
}
@@ -0,0 +1,135 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Representation of a single compilation dependency
public struct Dependency: Equatable {
public enum Kind {
case xcode
case product
case source
case fingerprint
case intermediate
case derivedFile
// Product of the target itself
case ownProduct
// User-excluded path
case userExcluded
case unknown
}
public let url: URL
public let type: Kind
public init(url: URL, type: Kind) {
self.url = url
self.type = type
}
}
/// Processes raw compilation URL dependencies from .d files
protocol DependencyProcessor {
/// Processes a list of dependencies and provides a list of project-specific dependencies
/// - Parameter files: raw dependency locations
/// - Returns: array of project-specific dependencies
func process(_ files: [URL]) -> [Dependency]
}
/// Classifies raw dependencies and strips irrelevant dependencies
class DependencyProcessorImpl: DependencyProcessor {
private let xcodePath: String
private let productPath: String
private let sourcePath: String
private let intermediatePath: String
private let derivedFilesPath: String
private let bundlePath: String?
private let skippedRegexes: [String]
init(xcode: URL, product: URL, source: URL, intermediate: URL, derivedFiles: URL, bundle: URL?, skippedRegexes: [String]) {
xcodePath = xcode.path.dirPath()
productPath = product.path.dirPath()
sourcePath = source.path.dirPath()
intermediatePath = intermediate.path.dirPath()
derivedFilesPath = derivedFiles.path.dirPath()
bundlePath = bundle?.path.dirPath()
self.skippedRegexes = skippedRegexes
}
func process(_ files: [URL]) -> [Dependency] {
let dependencies = classify(files)
return dependencies.filter(isRelevantDependency)
}
private func classify(_ files: [URL]) -> [Dependency] {
return files.map { file -> Dependency in
let filePath = file.resolvingSymlinksInPath().path
if skippedRegexes.contains(where: { filePath.range(of: $0, options: .regularExpression) != nil }) {
return Dependency(url: file, type: .userExcluded)
} else if filePath.hasPrefix(xcodePath) {
return Dependency(url: file, type: .xcode)
} else if filePath.hasPrefix(intermediatePath) {
return Dependency(url: file, type: .intermediate)
} else if filePath.hasPrefix(derivedFilesPath) {
return Dependency(url: file, type: .derivedFile)
} else if let bundle = bundlePath, filePath.hasPrefix(bundle) {
// If a target produces a bundle, explicitly classify all
// of products to distinguish from other targets products
return Dependency(url: file, type: .ownProduct)
} else if filePath.hasPrefix(productPath) {
return Dependency(url: file, type: .product)
} else if filePath.hasPrefix(sourcePath) {
return Dependency(url: file, type: .source)
} else {
return Dependency(url: file, type: .unknown)
}
}
}
private func isRelevantDependency(_ dependency: Dependency) -> Bool {
// Generated modulemaps may not be an actual dependency. Swift selects them as a
// dependency because these contribute to the final module context but doesn't mean that given module has
// been imported and it should invalidate current target when modified
// TODO: Recognize if the generated module was actually imported and only then it should be considered
// as a valid Dependency
if dependency.type == .product && dependency.url.pathExtension == "modulemap" {
return false
}
// Skip:
// - A fingerprint generated includes Xcode version build number so no need to analyze prepackaged Xcode files
// - All files in `*/Interemediates/*` - this file are created on-fly for a given target
// - Some files may depend on its own product (e.g. .m may #include *-Swift.h) - we know products will match
// because in case of a hit, these will be taken from the artifact
// - Customized DERIVED_FILE_DIR may change a directory of
// derived files, which by default is under `*/Interemediates`
// - User-specified (in .rcinfo) files to exclude
let irrelevantDependenciesType: [Dependency.Kind] = [
.xcode, .intermediate, .ownProduct, .derivedFile, .userExcluded,
]
return !irrelevantDependenciesType.contains(dependency.type)
}
}
fileprivate extension String {
func dirPath() -> String {
hasSuffix("/") ? self : appending("/")
}
}
@@ -0,0 +1,54 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Verifies if the filename should be always disallowed/allowed. If a filename does not match with allowed/disallowed
/// entries, the decision is handled by the underlying `scanner`
/// Note: disallowed filenames have higher priorities than allowed ones
class ExceptionsFilteredFileListScanner: FileListScanner {
private let listScanner: FileListScanner
private let allowedFilenames: [String]
private let disallowedFilenames: [String]
/// Default initializer that specifies disallowed and allowed filenames (including an extention)
/// Valid filenames: ['file.swift', 'file.m']
/// Invalid filenames: ['somePath/file.swift', '/absolutePath/file.m']
///
/// - Parameters:
/// - allowedFilenames: a list of filenames which should always be allowed
/// - disallowedFilenames: a list of filenames which should always be disallowed
/// - scanner: underlying scanner that decides if non of allowed/disallowed pattern matches
init(allowedFilenames: [String], disallowedFilenames: [String], scanner: FileListScanner) {
self.allowedFilenames = allowedFilenames
self.disallowedFilenames = disallowedFilenames
listScanner = scanner
}
func contains(_ url: URL) throws -> Bool {
let filename = url.lastPathComponent
if disallowedFilenames.contains(filename) {
return false
}
if allowedFilenames.contains(filename) {
return true
}
return try listScanner.contains(url)
}
}
@@ -0,0 +1,46 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
protocol FileListScanner {
/// Returns true if the url is present in the file list
func contains(_ url: URL) throws -> Bool
}
/// Finds file on a list of files provied by ListReader
class FileListScannerImpl: FileListScanner {
private let fileList: ListReader
private let caseSensitive: Bool
init(_ fileList: ListReader, caseSensitive: Bool) {
self.fileList = fileList
self.caseSensitive = caseSensitive
}
func contains(_ url: URL) throws -> Bool {
if caseSensitive {
return try fileList.listFilesURLs().contains(url)
}
let lowerCasePath = url.path.lowercased()
return try fileList.listFilesURLs().lazy.contains { element in
element.path.lowercased() == lowerCasePath
}
}
}
@@ -0,0 +1,64 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Decides which location location should be considered in the fingerprint computation
/// Meant to replaces machine-specific products (like .swiftmodule) with its source-aware fingerprint representation
protocol FingerprintOverrideManager {
/// File extensions that should be replced by an override
var overridingFileExtensions: [String] { get }
/// Returns a file that should be considered in the fingerprint generation
func getFingerprintFile(_ url: Dependency) -> Dependency
}
/// Manager that rewrites dependencies to the fingerprint override
/// if the override file exists on a disk
public class FingerprintOverrideManagerImpl: FingerprintOverrideManager {
private let overrideExtension: String
private let fileManager: FileManager
let overridingFileExtensions: [String]
/// Initializer
/// @param overrideExtension: all extensions that require fingerprint override
/// @param fingerprintOverrideExtension: file extension of the fingerprint override
/// @param fileManager: fileManager instance to check file existance
public init(
overridingFileExtensions: [String],
fingerprintOverrideExtension: String,
fileManager: FileManager
) {
self.overridingFileExtensions = overridingFileExtensions
overrideExtension = fingerprintOverrideExtension
self.fileManager = fileManager
}
public func getFingerprintFile(_ dependency: Dependency) -> Dependency {
// Require overrides only it already exists on a disk
// If the dependency was not generated locally (e.g. distributed
// as a binary) and misses ".{{overrideExtension}}",
// the fingerprint of a raw file can be safely used
let fingerprintOverrideURL = dependency.url.appendingPathExtension(overrideExtension)
let isFileExistOnDisk = fileManager.fileExists(atPath: fingerprintOverrideURL.path)
if overridingFileExtensions.contains(dependency.url.pathExtension) && isFileExistOnDisk {
return Dependency(url: fingerprintOverrideURL, type: .fingerprint)
}
return dependency
}
}
@@ -0,0 +1,106 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum FingerprintSyncerError: Error {
case missingResourceValue(URL)
case invalidFingerprint
}
/// Syncs custom fingerprint overrides
protocol FingerprintSyncer {
/// Sets a fingerprint override for all files placed directly in a source location
func decorate(sourceDir: URL, fingerprint: String) throws
/// Deletes fingerprint overrides in the dir (if already created)
func delete(sourceDir: URL) throws
/// Sets a fingerprint override for a singe file placed
func decorate(file: URL, fingerprint: String) throws
/// Deletes fingerprint override for a file (if already created)
func delete(file: URL) throws
}
class FileFingerprintSyncer: FingerprintSyncer {
/// Extension of the file that keeps fingerprint override
private let fingerprintExtension: String
private let dirAccessor: DirAccessor
/// A list of all extensions that should be decorated with an override
private let extensions: [String]
init(
fingerprintOverrideExtension: String,
dirAccessor: DirAccessor,
extensions: [String]
) {
self.dirAccessor = dirAccessor
fingerprintExtension = fingerprintOverrideExtension
self.extensions = extensions
}
func decorate(sourceDir: URL, fingerprint: String) throws {
guard let fingerprintData = fingerprint.data(using: .utf8) else {
throw FingerprintSyncerError.invalidFingerprint
}
guard case .dir = try dirAccessor.itemType(atPath: sourceDir.path) else {
// no directory to decorate (no module was generated)
return
}
let allURLs = try dirAccessor.items(at: sourceDir)
// recursive search is not required as all files are located in a root dir
for file in allURLs {
if extensions.contains(file.pathExtension) {
let fingerprintFile = file.appendingPathExtension(fingerprintExtension)
try dirAccessor.write(toPath: fingerprintFile.path, contents: fingerprintData)
}
}
}
func delete(sourceDir: URL) throws {
guard case .dir = try dirAccessor.itemType(atPath: sourceDir.path) else {
// no directory to decorate (no module was generated)
return
}
let allURLs = try dirAccessor.items(at: sourceDir)
// recursive search is not required as all files are located in a root dir
for file in allURLs where file.pathExtension == fingerprintExtension {
try dirAccessor.removeItem(atPath: file.path)
}
}
func decorate(file: URL, fingerprint: String) throws {
guard let fingerprintData = fingerprint.data(using: .utf8) else {
throw FingerprintSyncerError.invalidFingerprint
}
let fingerprintFile = file.appendingPathExtension(fingerprintExtension)
try dirAccessor.write(toPath: fingerprintFile.path, contents: fingerprintData)
}
func delete(file: URL) throws {
guard case .file = try dirAccessor.itemType(atPath: file.path) else {
// no file to decorate (no module was generated)
return
}
let overrideURL = file.appendingPathExtension(fingerprintExtension)
guard case .file = try dirAccessor.itemType(atPath: overrideURL.path) else {
// no override
return
}
try dirAccessor.removeItem(atPath: overrideURL.path)
}
}
@@ -0,0 +1,87 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ListReaderError: Error {
/// The file to read a list doesn't exist or is not readable
case cannotReadFile
/// The file content is invalid (e.g. cannot be represented as a String)
case invalidContent
}
/// Reads a list of files
protocol ListReader {
/// Fetches all dependencies
/// - Throws: `ListReaderError`
func listFilesURLs() throws -> [URL]
/// Returns true if the reader is able to read a list of files
func canRead() -> Bool
}
protocol ListWriter {
/// Writes a new list of files
/// - Parameter list: files to save in a file list
func writerListFilesURLs(_ list: [URL]) throws
}
/// Reads&Writes files that list files using one-file-per-line format
class FileListEditor: ListReader, ListWriter {
private let file: URL
private let fileManager: FileManager
/// cached list of files
private var cachedFiles: [URL]?
init(_ file: URL, fileManager: FileManager) {
self.file = file
self.fileManager = fileManager
}
func listFilesURLs() throws -> [URL] {
if let files = cachedFiles {
return files
}
guard let content = fileManager.contents(atPath: file.path) else {
throw ListReaderError.cannotReadFile
}
guard let fileStrings = String(data: content, encoding: .utf8)?.split(separator: "\n") else {
throw ListReaderError.invalidContent
}
let files = fileStrings.map(escapeFilename).map(URL.init(fileURLWithPath:))
cachedFiles = files
return files
}
func canRead() -> Bool {
return fileManager.fileExists(atPath: file.path)
}
private func escapeFilename(_ path: String.SubSequence) -> String {
String(path).replacingOccurrences(of: "\\ ", with: " ")
}
private func unescapeFilename(_ path: String) -> String {
path.replacingOccurrences(of: " ", with: "\\ ")
}
func writerListFilesURLs(_ list: [URL]) throws {
let data = list.map(\.path).map(unescapeFilename).joined(separator: "\n").data(using: .utf8)!
fileManager.createFile(atPath: file.path, contents: data, attributes: nil)
}
}
@@ -0,0 +1,50 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Reads a list of files from a marker file
class FileMarkerReader: ListReader {
private let file: URL
private let fileManager: FileManager
private var cachedFiles: [URL]?
init(_ file: URL, fileManager: FileManager) {
self.file = file
self.fileManager = fileManager
}
func listFilesURLs() throws -> [URL] {
if let cachedResponse = cachedFiles {
return cachedResponse
}
// Skipping first marker line `dependencies: //`
let fileLines = try String(contentsOf: file).split(separator: "\n").dropFirst()
let files = fileLines.map { line in
line.replacingOccurrences(of: FileMarkerWriter.delimiter, with: "")
}
let filesURLs = files.map(URL.init(fileURLWithPath:))
cachedFiles = filesURLs
return filesURLs
}
func canRead() -> Bool {
return fileManager.fileExists(atPath: file.path)
}
}
@@ -0,0 +1,61 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Manage marker file entries
protocol MarkerWriter {
/// Saves all dependencies
func enable(dependencies: [URL]) throws
/// Disables mode marker
func disable() throws
}
/// Saves a marker using a format matching .d one
class FileMarkerWriter: MarkerWriter {
static let delimiter = " \\"
private let filePath: String
private let fileAccessor: FileAccessor
init(_ file: URL, fileAccessor: FileAccessor) {
filePath = file.path
self.fileAccessor = fileAccessor
}
func enable(dependencies: [URL]) throws {
let lines = ["dependencies: "] + dependencies.map { $0.path }
let fileContent = lines.joined(separator: "\(Self.delimiter)\n")
try fileAccessor.write(toPath: filePath, contents: fileContent.data(using: .utf8))
}
func disable() throws {
if fileAccessor.fileExists(atPath: filePath) {
try fileAccessor.removeItem(atPath: filePath)
}
}
}
/// Marker Writer that does nothing
class NoopMarkerWriter: MarkerWriter {
init(_ file: URL, fileManager: FileManager) {}
func enable(dependencies: [URL]) throws {}
func disable() throws {}
}
@@ -0,0 +1,67 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// File paths remapper according the virtual file system mappings
/// - Warning: this class is not thread safe
class OverlayDependenciesRemapper: DependenciesRemapper {
private let overlayReader: OverlayReader
private var mappings: [OverlayMapping]?
init(overlayReader: OverlayReader) {
self.overlayReader = overlayReader
}
/// Lazily Reads mappings from a file
/// - Warning: this function is not thread safe
private func getMappings() throws -> [OverlayMapping] {
guard let mappings = mappings else {
let mappings = try overlayReader.provideMappings()
self.mappings = mappings
return mappings
}
return mappings
}
private func mapPath(
_ path: String,
source: KeyPath<OverlayMapping, URL>,
destination: KeyPath<OverlayMapping, URL>
) throws -> String {
guard let mapping = try getMappings().first(where: { $0[keyPath: source].path == path }) else {
// TODO: support partial mappings, where a directory path can be replaced with some other directory
// no direct mapping found
return path
}
return mapping[keyPath: destination].path
}
func replace(genericPaths: [String]) throws -> [String] {
try genericPaths.map {
try mapPath($0, source: \.virtual, destination: \.local)
}
}
func replace(localPaths: [String]) throws -> [String] {
try localPaths.map {
try mapPath($0, source: \.local, destination: \.virtual)
}
}
}
@@ -0,0 +1,133 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Maps overlay's virtual URL with an actual (local) location
struct OverlayMapping: Hashable {
let virtual: URL
let local: URL
}
enum JsonOverlayReaderError: Error {
/// The source file is missing
case missingSourceFile(URL)
/// The file exists but its content is invalid
case invalidSourceContent(URL)
/// the overlay format is not supported - either contains a nested directory or a single file
case unsupportedFormat
}
/// Provides virtual file system overlay mappings
protocol OverlayReader {
func provideMappings() throws -> [OverlayMapping]
}
class JsonOverlayReader: OverlayReader {
enum Mode {
/// Interrupts the operation if the representation file is missing
case strict
/// Assume empty overlay mapping if the file doesn't exist
case bestEffort
}
private struct Overlay: Decodable {
enum OverlayType: String, Decodable {
case file
case directory
}
struct Content: Decodable {
let externalContents: String
let name: String
let type: OverlayType
enum CodingKeys: String, CodingKey {
case externalContents = "external-contents"
case name
case type
}
}
struct RootContent: Decodable {
let contents: [Content]
let name: String
let type: OverlayType
}
let roots: [RootContent]
}
private lazy var jsonDecoder = JSONDecoder()
private let json: URL
private let mode: Mode
private let fileReader: FileReader
init(_ json: URL, mode: Mode, fileReader: FileReader) {
self.json = json
self.mode = mode
self.fileReader = fileReader
}
func provideMappings() throws -> [OverlayMapping] {
guard let jsonContent = try fileReader.contents(atPath: json.path) else {
switch mode {
case .strict:
throw JsonOverlayReaderError.missingSourceFile(json)
case .bestEffort:
debugLog("overlay mapping file \(json) doesn't exist. Skipping overlay for the best-effort mode.")
return []
}
}
do {
let overlay: Overlay = try jsonDecoder.decode(Overlay.self, from: jsonContent)
let mappings: [OverlayMapping] = try overlay.roots.reduce([]) { prev, root in
switch root.type {
case .directory:
// iterate all contents
let dir = URL(fileURLWithPath: root.name)
let mappings: [OverlayMapping] = try root.contents.map { content in
switch content.type {
case .file:
let virtual = dir.appendingPathComponent(content.name)
let local = URL(fileURLWithPath: content.externalContents)
return .init(virtual: virtual, local: local)
case .directory:
throw JsonOverlayReaderError.unsupportedFormat
}
}
return prev + mappings
case .file:
throw JsonOverlayReaderError.unsupportedFormat
}
}
return mappings
} catch {
switch mode {
case .strict:
throw error
case .bestEffort:
errorLog("Overlay reader has failed with an error \(error). Best-effort mode - skipping an overlay.")
return []
}
}
}
}

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