Compare commits

...

174 Commits

Author SHA1 Message Date
Bartosz Polaczyk d837f6e14b Merge pull request #205 from polac24/bartosz/20230516-support-xcode143
Support Xcode14.3
2023-05-18 07:00:07 -07:00
Bartosz Polaczyk 5528d507b0 Support Xcode 14.3 2023-05-16 16:37:28 -07:00
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 ccda424791 Formatting 2022-02-28 22:55:04 +01: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
140 changed files with 5370 additions and 359 deletions
+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
args: --strict
macOS:
runs-on: macOS-latest
runs-on: macos-12
env:
XCODE_VERSION: ${{ '13.1' }}
XCODE_VERSION: ${{ '14.2' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
+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
+2 -2
View File
@@ -6,9 +6,9 @@ on:
jobs:
macOS:
name: Add macOS binaries to release
runs-on: macOS-latest
runs-on: macos-12
env:
XCODE_VERSION: ${{ '13.1' }}
XCODE_VERSION: ${{ '14.2' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
+1
View File
@@ -64,6 +64,7 @@ excluded:
- fastlane/
- DerivedData/
- e2eTests/XCRemoteCacheSample/Pods
- e2eTests/StandaloneSampleApp
attributes:
always_on_same_line:
+11 -2
View File
@@ -37,13 +37,22 @@
"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": "c75c3acc25460195cfd203a04dde165395bf00e0",
"version": "8.7.1"
"revision": "fae27b48bc14ff3fd9b02902e48c4665ce5a0793",
"version": "8.9.0"
}
},
{
+28 -2
View File
@@ -16,7 +16,8 @@ let package = Package(
.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.7.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(
@@ -31,8 +32,16 @@ let package = Package(
name: "xcswiftc",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtoolSupport",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtool",
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
),
.target(
name: "xclipo",
dependencies: ["XCRemoteCache"]
),
.target(
@@ -50,15 +59,32 @@ let package = Package(
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"]
dependencies: [
"xcprebuild",
"xcswiftc",
"xclibtool",
"xcpostbuild",
"xcprepare",
"xcld",
"xcldplusplus",
"xclipo",
]
),
.testTarget(
name: "XCRemoteCacheTests",
dependencies: ["XCRemoteCache"],
resources: [.copy("TestData")]
),
.testTarget(
name: "xclibtoolSupportTests",
dependencies: ["xclibtoolSupport"]
),
]
)
+50 -3
View File
@@ -8,6 +8,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
[![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)
@@ -151,6 +152,7 @@ xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consume
| `--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>
@@ -194,8 +196,11 @@ Configure Xcode targets that **should use** XCRemoteCache:
* `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>
@@ -264,7 +269,42 @@ $ 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`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
_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:
@@ -288,13 +328,15 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `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 | `user.rcinfo` | ⬜️ |
| `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 | `[]` | ⬜️ |
@@ -308,12 +350,15 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `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
@@ -351,6 +396,8 @@ XCRemoteCache supports Amazon S3 and Google Cloud Storage buckets to be used as
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
...
@@ -411,7 +458,7 @@ Note: This setup is not recommended and may not be supported in future XCRemoteC
* 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.
* Using "Precompiled prefix headers" for Objective-C targets is not yet supported. [PR is welcome]
* The new Swift driver (introduced by default in Xcode 14.0) is not supported and has to be disabled when using XCRemoteCache
## FAQ
+17 -1
View File
@@ -10,7 +10,7 @@ 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']
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld', 'xcldplusplus', 'xclipo']
PROJECT_NAME = 'XCRemoteCache'
SWIFTLINT_ENABLED = true
@@ -72,6 +72,22 @@ task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args|
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
@@ -42,6 +42,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
private let modulesFolderPath: String
private let dSYMPath: URL
private let metaWriter: MetaWriter
private let artifactProcessor: ArtifactProcessor
private let fileManager: FileManager
init(
@@ -52,6 +53,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
modulesFolderPath: String,
dSYMPath: URL,
metaWriter: MetaWriter,
artifactProcessor: ArtifactProcessor,
fileManager: FileManager
) {
self.buildDir = buildDir
@@ -62,6 +64,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
self.fileManager = fileManager
self.dSYMPath = dSYMPath
self.metaWriter = metaWriter
self.artifactProcessor = artifactProcessor
super.init(workingDir: tempDir, moduleName: moduleName, fileManager: fileManager)
}
@@ -87,6 +90,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
/// - 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
@@ -31,7 +31,7 @@ enum ArtifactOrganizerLocationPreparationResult: Equatable {
case preparedForArtifact(artifact: URL)
}
/// Prepares .zip artifact for the local operations
/// 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
@@ -48,10 +48,13 @@ protocol ArtifactOrganizer {
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, fileManager: FileManager) {
init(targetTempDir: URL, artifactProcessors: [ArtifactProcessor], fileManager: FileManager) {
cacheDir = targetTempDir.appendingPathComponent("xccache")
self.artifactProcessors = artifactProcessors
self.fileManager = fileManager
}
@@ -93,6 +96,10 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
// 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
}
@@ -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(".")
}
}
@@ -88,7 +88,7 @@ class ArtifactSwiftProductsBuilderImpl: ArtifactSwiftProductsBuilder {
throw ArtifactSwiftProductsBuilderError.populatingNonExistingObjCHeader
}
try fileManager.createDirectory(at: moduleObjCURL, withIntermediateDirectories: true, attributes: nil)
try fileManager.spt_forceLinkItem(at: headerURL, to: headerArtifactURL)
try fileManager.spt_forceCopyItem(at: headerURL, to: headerArtifactURL)
}
func includeModuleDefinitionsToTheArtifact(arch: String, moduleURL: URL) throws {
@@ -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)
}
}
@@ -31,6 +31,8 @@ enum SwiftmoduleFileExtension: String {
case swiftdoc
case swiftsourceinfo
case swiftinterface
case privateSwiftinterface = "private.swiftinterface"
case abiJson = "abi.json"
}
extension SwiftmoduleFileExtension {
@@ -40,5 +42,7 @@ extension SwiftmoduleFileExtension {
.swiftdoc: .required,
.swiftsourceinfo: .optional,
.swiftinterface: .optional,
.privateSwiftinterface: .optional,
.abiJson: .optional,
]
}
@@ -0,0 +1,37 @@
// 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
class FallbackXCLibtoolLogic: XCLibtoolLogic {
private let fallbackCommand: String
init(fallbackCommand: String) {
self.fallbackCommand = fallbackCommand
}
func run() {
let args = ProcessInfo().arguments
let paramList = [fallbackCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(fallbackCommand, cargs)
exit(1)
}
}
@@ -19,31 +19,42 @@
import Foundation
enum XCLibtoolCreateUniversalBinaryError: Error {
enum XCCreateUniversalBinaryError: Error {
/// Missing ar libraries that should constitute an universal build
case missingInputLibrary
}
/// Wrapper for `libtool` call for creating an universal binary
class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
/// 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]) throws {
init(
output: String,
inputs: [String],
toolName: String,
fallbackCommand: String
) throws {
self.output = URL(fileURLWithPath: output)
guard let firstInput = inputs.first else {
throw XCLibtoolCreateUniversalBinaryError.missingInputLibrary
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() {
@@ -52,10 +63,10 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
let config: XCRemoteCacheConfig
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
} catch {
errorLog("Libtool initialization failed with error: \(error). Fallbacking to libtool")
errorLog("\(toolName) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
fallbackToDefault()
}
@@ -74,22 +85,21 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
// that these are already an universal binary
try fileManager.spt_forceLinkItem(at: firstInputURL, to: output)
} catch {
errorLog("Libtool failed with error: \(error). Fallbacking to libtool")
errorLog("\(toolName) failed with error: \(error). Fallbacking to \(fallbackCommand)")
do {
try fileManager.removeItem(at: markerURL)
fallbackToDefault()
} catch {
exit(1, "FATAL: libtool failed with error: \(error)")
exit(1, "FATAL: \(fallbackCommand) failed with error: \(error)")
}
}
}
private func fallbackToDefault() -> Never {
let args = ProcessInfo().arguments
let command = "libtool"
let paramList = [command] + args.dropFirst()
let paramList = [fallbackCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(command, cargs)
execvp(fallbackCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
@@ -20,11 +20,13 @@
import Foundation
/// Represents a mode that libtool was called
public enum XCLibtoolMode {
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])
/// print the toolchain version
case version
}
public class XCLibtool {
@@ -44,7 +46,14 @@ public class XCLibtool {
stepDescription: "Libtool"
)
case .createUniversalBinary(let output, let inputs):
logic = try XCLibtoolCreateUniversalBinary(output: output, inputs: inputs)
logic = try XCCreateUniversalBinary(
output: output,
inputs: inputs,
toolName: "Libtool",
fallbackCommand: "libtool"
)
case .version:
logic = FallbackXCLibtoolLogic(fallbackCommand: "libtool")
}
}
@@ -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()
}
}
@@ -27,13 +27,19 @@ protocol ThinningConsumerArtifactsOrganizerFactory {
}
class ThinningConsumerZipArtifactsOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory {
private let processors: [ArtifactProcessor]
private let fileManager: FileManager
init(fileManager: FileManager) {
init(processors: [ArtifactProcessor], fileManager: FileManager) {
self.processors = processors
self.fileManager = fileManager
}
func build(targetTempDir: URL) -> ArtifactOrganizer {
ZipArtifactOrganizer(targetTempDir: targetTempDir, fileManager: fileManager)
ZipArtifactOrganizer(
targetTempDir: targetTempDir,
artifactProcessors: processors,
fileManager: fileManager
)
}
}
@@ -58,7 +58,7 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
@@ -79,6 +79,9 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
}
// Build parent dir of the .swiftmodule file that contains a module
return modulePathOutput.deletingLastPathComponent()
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
objcHeaderFile: objcHeaderOutput
)
}
}
@@ -73,11 +73,12 @@ class UnzippedArtifactSwiftProductsOrganizer: SwiftProductsOrganizer {
.appendingPathComponent(moduleName)
.appendingPathComponent("\(moduleName)-Swift.h")
let generatedModuleDir = try productsGenerator.generateFrom(
let generatedModule = try productsGenerator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
try fingerprintSyncer.decorate(sourceDir: generatedModuleDir, fingerprint: fingerprint)
try fingerprintSyncer.decorate(sourceDir: generatedModule.swiftmoduleDir, fingerprint: fingerprint)
try fingerprintSyncer.decorate(file: generatedModule.objcHeaderFile, fingerprint: fingerprint)
}
}
@@ -238,13 +238,32 @@ class Postbuild {
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)
}
}
}
}
@@ -40,6 +40,7 @@ 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
@@ -73,7 +74,7 @@ public struct PostbuildContext {
let builtProductsDir: URL
/// Location to the product bundle. Can be nil for libraries
let bundleDir: URL?
let derivedSourcesDir: URL
var derivedSourcesDir: URL
/// List of all targets to downloaded from the thinning aggregation target
var thinnedTargets: [String]
/// Action type: build, indexbuild etc
@@ -82,6 +83,12 @@ public struct PostbuildContext {
/// 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 {
@@ -91,6 +98,7 @@ extension PostbuildContext {
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
@@ -133,5 +141,13 @@ extension PostbuildContext {
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
}
}
@@ -34,7 +34,7 @@ public class XCPostbuild {
let context: PostbuildContext
let cacheHitLogger: CacheHitLogger
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PostbuildContext(config, env: env)
updateProcessTag(context.targetName)
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
@@ -87,8 +87,14 @@ public class XCPostbuild {
fingerprintFilesGenerator,
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
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,
@@ -97,6 +103,7 @@ public class XCPostbuild {
modulesFolderPath: context.modulesFolderPath,
dSYMPath: context.dSYMPath,
metaWriter: metaWriter,
artifactProcessor: UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager),
fileManager: fileManager
)
let dirAccessor = DirAccessorComposer(
@@ -114,6 +121,7 @@ public class XCPostbuild {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
@@ -122,6 +130,7 @@ public class XCPostbuild {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.uploadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -129,6 +138,7 @@ public class XCPostbuild {
mode: context.mode,
downloadStreamURL: context.recommendedCacheAddress,
upstreamStreamURL: context.cacheAddresses,
uploadBatchSize: config.uploadBatchSize,
networkClient: networkClient,
urlBuilderFactory: {
try URLBuilderImpl(
@@ -170,7 +180,9 @@ public class XCPostbuild {
product: context.productsDir,
source: context.srcRoot,
intermediate: context.targetTempDir,
bundle: context.bundleDir
derivedFiles: context.derivedFilesDir,
bundle: context.bundleDir,
skippedRegexes: context.irrelevantDependenciesPaths
)
// Override fingerprints for all produced '.swiftmodule' files
let fingerprintOverrideManager = FingerprintOverrideManagerImpl(
@@ -197,7 +209,10 @@ public class XCPostbuild {
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 =
@@ -259,7 +274,12 @@ public class XCPostbuild {
)
// Trigger build completion
if try modeController.isEnabled() {
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 {
@@ -20,6 +20,7 @@
import Foundation
enum PrebuildResult: Equatable {
case disabled
case incompatible
case compatible(localDependencies: [URL])
}
@@ -57,6 +58,9 @@ class Prebuild {
// 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
}
@@ -46,6 +46,8 @@ public struct PrebuildContext {
/// 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 {
@@ -69,5 +71,6 @@ extension PrebuildContext {
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
}
}
@@ -29,7 +29,7 @@ public class XCPrebuild {
let config: XCRemoteCacheConfig
let context: PrebuildContext
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PrebuildContext(config, env: env)
updateProcessTag(context.targetName)
} catch {
@@ -78,6 +78,10 @@ public class XCPrebuild {
exit(0)
}
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
context.compilationHistoryFile,
fileManager: fileManager
)
do {
let envFingerprint = try EnvironmentFingerprintGenerator(
configuration: config,
@@ -96,6 +100,7 @@ public class XCPrebuild {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
@@ -104,6 +109,7 @@ public class XCPrebuild {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.downloadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -146,9 +152,14 @@ public class XCPrebuild {
filesFingerprintGenerator,
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
context.compilationHistoryFile,
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)
@@ -156,7 +167,10 @@ public class XCPrebuild {
if config.thinningEnabled {
if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets {
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(fileManager: .default)
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
processors: [artifactProcessor],
fileManager: fileManager
)
let aggregationPlugin = ThinningConsumerPrebuildPlugin(
targetName: context.targetName,
tempDir: context.targetTempDir,
@@ -188,7 +202,9 @@ public class XCPrebuild {
case .compatible(localDependencies: let dependencies):
// TODO: pass `allowedInputFiles` observed in the build time
try modeController.enable(allowedInputFiles: dependencies, dependencies: dependencies)
compilationHistoryOrganizer.reset()
case .disabled:
infoLog("XCRemoteCache is explicitly disabled")
try modeController.disable()
}
} catch {
disableRemoteCache(
@@ -196,6 +212,7 @@ public class XCPrebuild {
errorMessage: "Prebuild step failed with error: \(error)"
)
}
compilationHistoryOrganizer.reset()
}
private func disableRemoteCache(modeController: PhaseCacheModeController, errorMessage: String?) {
@@ -395,6 +395,9 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
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)";
@@ -409,6 +412,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
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) ) {
@@ -429,6 +433,12 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
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") ||
@@ -442,6 +452,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
) {
// 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 {
@@ -450,6 +461,14 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
}
}
// 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);
@@ -520,8 +539,6 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
}
}
// null-terminating the args array
clang_args[argc] = NULL;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
/// execvp takes $PATH to consideration
@@ -34,21 +34,33 @@ 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) {
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
result["SWIFT_EXEC"] = wrappers.swiftc.path
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 {
result["CC"] = wrappers.cc.path
result["LD"] = wrappers.ld.path
result["LIBTOOL"] = wrappers.libtool.path
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
@@ -60,14 +72,36 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
swiftFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
clangFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
result["OTHER_SWIFT_FLAGS"] = swiftFlags.settingValue
result["OTHER_CFLAGS"] = clangFlags.settingValue
setBuildSetting(buildSettings: &result, key: "OTHER_SWIFT_FLAGS", value: swiftFlags.settingValue )
setBuildSetting(buildSettings: &result, key: "OTHER_CFLAGS", value: clangFlags.settingValue )
result["XCRC_FAKE_SRCROOT"] = "\(fakeSrcRoot.path)"
result["XCRC_PLATFORM_PREFERRED_ARCH"] =
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"
}
}
}
@@ -52,7 +52,9 @@ extension IntegrateContext {
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")
)
@@ -34,6 +34,7 @@ public class XCIntegrate {
private let consumerEligiblePlatforms: String
private let lldbMode: LLDBInitMode
private let fakeSrcRoot: String
private let sdksExclude: String
private let output: String?
public init(
@@ -48,6 +49,7 @@ public class XCIntegrate {
consumerEligiblePlatforms: String,
lldbMode: LLDBInitMode,
fakeSrcRoot: String,
sdksExclude: String,
output: String?
) {
projectPath = input
@@ -61,6 +63,7 @@ public class XCIntegrate {
self.consumerEligiblePlatforms = consumerEligiblePlatforms
self.lldbMode = lldbMode
self.fakeSrcRoot = fakeSrcRoot
self.sdksExclude = sdksExclude
self.output = output
}
@@ -74,7 +77,7 @@ public class XCIntegrate {
let binariesDir = commandURL.deletingLastPathComponent()
let srcRoot: URL = URL(fileURLWithPath: projectPath).deletingLastPathComponent()
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
let context = try IntegrateContext(
@@ -98,7 +101,8 @@ public class XCIntegrate {
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
mode: context.mode,
repoRoot: context.repoRoot,
fakeSrcRoot: context.fakeSrcRoot
fakeSrcRoot: context.fakeSrcRoot,
sdksExclude: sdksExclude.integrateArrayArguments
)
let lldbPatcher: LLDBInitPatcher
switch lldbMode {
@@ -25,7 +25,9 @@ struct XCRCBinariesPaths {
let cc: URL
let swiftc: URL
let libtool: URL
let lipo: URL
let ld: URL
let ldplusplus: URL
let prebuild: URL
let postbuild: URL
}
@@ -230,10 +230,12 @@ struct XcodeProjIntegrate: Integrate {
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)
pbxproj.add(object: postbuildPhase)
target.buildPhases.append(postbuildPhase)
}
}
@@ -77,7 +77,18 @@ class Prepare: PrepareLogic {
guard fileAccessor.fileExists(atPath: PhaseCacheModeController.xcodeSelectLink.path) else {
throw PrepareError.missingXcodeSelectDirectory
}
let commonSha = try gitClient.getCommonPrimarySha()
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
@@ -52,6 +52,8 @@ public struct PrepareContext {
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 {
@@ -77,5 +79,6 @@ extension PrepareContext {
cacheAddresses = try config.cacheAddresses.map(URL.build)
cacheHealthPath = config.cacheHealthPath
cacheHealthPathProbeCount = config.cacheHealthPathProbeCount
gracefullyHandleMissingCommonSha = config.gracefullyHandleMissingCommonSha
}
}
@@ -31,10 +31,12 @@ public struct PrepareMarkContext {
let recommendedCacheAddress: URL
/// All remote servers to mark
let cacheAddresses: [URL]
/// XCRemoteCache is explicitly disabled
let disabled: Bool
}
extension PrepareMarkContext {
init(_ config: XCRemoteCacheConfig) throws {
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 {
@@ -43,5 +45,6 @@ extension PrepareMarkContext {
}
recommendedCacheAddress = address
cacheAddresses = try config.cacheAddresses.map(URL.build)
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
}
}
@@ -32,7 +32,7 @@ public class XCConfig {
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
} catch {
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
}
@@ -61,7 +61,7 @@ public class XCPrepare {
var context: PrepareContext
let xcodeVersion: String
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PrepareContext(config, offline: offline)
xcodeVersion = try customXcodeBuildNumber ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
} catch {
@@ -78,6 +78,7 @@ public class XCPrepare {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
@@ -86,6 +87,7 @@ public class XCPrepare {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.downloadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -46,13 +46,18 @@ public class XCPrepareMark {
let context: PrepareMarkContext
let xcodeVersion: String
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
context = try PrepareMarkContext(config)
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?
@@ -60,6 +65,7 @@ public class XCPrepareMark {
awsV4Signature = AWSV4Signature(
secretKey: config.AWSSecretKey,
accessKey: config.AWSAccessKey,
securityToken: config.AWSSecurityToken,
region: config.AWSRegion,
service: config.AWSService,
date: Date(timeIntervalSinceNow: 0)
@@ -68,6 +74,7 @@ public class XCPrepareMark {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.uploadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -75,6 +82,7 @@ public class XCPrepareMark {
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
@@ -36,7 +36,7 @@ public class XCStats {
let config: XCRemoteCacheConfig
let context: XCStatsContext
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
try context = XCStatsContext(config, fileManager: fileManager)
} catch {
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
@@ -70,7 +70,7 @@ public class XCCreateBinary {
let config: XCRemoteCacheConfig
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
} catch {
errorLog("\(stepDescription) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
@@ -78,7 +78,12 @@ public class XCCreateBinary {
}
let markerURL = tempDir.appendingPathComponent(config.modeMarkerPath)
do {
let organizer = ZipArtifactOrganizer(targetTempDir: tempDir, fileManager: fileManager)
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 {
@@ -55,7 +55,7 @@ class MirroredLinkingSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
/// Predict moduleName from the `*.swiftmodule` artifact
let foundSwiftmoduleFile = artifactSwiftModuleFiles[.swiftmodule]
guard let mainSwiftmoduleFile = foundSwiftmoduleFile else {
@@ -26,14 +26,19 @@ enum DiskSwiftcProductsGeneratorError: Error {
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 files have been placed
/// - Returns: location dir where .swiftmodule and ObjC header files have been placed
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL
) throws -> SwiftcProductsGeneratorOutput
}
/// Generator that produces all products in the locations where Xcode expects it, using provided disk copier
@@ -64,7 +69,7 @@ class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
@@ -85,6 +90,9 @@ class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
}
// Build parent dir of the .swiftmodule file that contains a module
return modulePathOutput.deletingLastPathComponent()
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
objcHeaderFile: objcHeaderOutput
)
}
}
@@ -70,7 +70,7 @@ public class XCSwiftc {
let context: SwiftcContext
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
context = try SwiftcContext(config: config, input: inputArgs)
} catch {
@@ -83,7 +83,12 @@ public class XCSwiftc {
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
let artifactOrganizer = ZipArtifactOrganizer(targetTempDir: context.tempDir, 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(
@@ -83,6 +83,10 @@ public struct XCRemoteCacheConfig: Encodable {
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
@@ -107,7 +111,8 @@ public struct XCRemoteCacheConfig: Encodable {
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
var productFilesExtensionsWithContentOverride = ["swiftmodule"]
/// .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
@@ -118,6 +123,8 @@ public struct XCRemoteCacheConfig: Encodable {
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`)
@@ -136,6 +143,14 @@ public struct XCRemoteCacheConfig: Encodable {
/// 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 {
@@ -168,6 +183,8 @@ extension XCRemoteCacheConfig {
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
@@ -184,12 +201,16 @@ extension XCRemoteCacheConfig {
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
}
@@ -234,6 +255,8 @@ struct ConfigFileScheme: Decodable {
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]?
@@ -247,12 +270,15 @@ struct ConfigFileScheme: Decodable {
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 {
@@ -280,6 +306,8 @@ struct ConfigFileScheme: Decodable {
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"
@@ -293,12 +321,15 @@ struct ConfigFileScheme: Decodable {
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"
}
}
@@ -311,31 +342,42 @@ class XCRemoteCacheConfigReader {
/// Name of the configuration file, required in $(SRCROOT) location
private static let configurationFile = ".rcinfo"
private let srcRoot: String
private let fileManager: FileManager
private let fileReader: FileReader
private lazy var yamlDecorer = YAMLDecoder(encoding: .utf8)
init(env: [String: String], fileManager: FileManager) throws {
init(env: [String: String], fileReader: FileReader) throws {
let explicitSrcRoot: String? = env.readEnv(key: "SRCROOT")
srcRoot = explicitSrcRoot ?? fileManager.currentDirectoryPath
self.fileManager = fileManager
srcRoot = explicitSrcRoot ?? FileManager.default.currentDirectoryPath
self.fileReader = fileReader
}
init(srcRootPath srcRoot: String, fileManager: FileManager) {
init(srcRootPath srcRoot: String, fileReader: FileReader) {
self.srcRoot = srcRoot
self.fileManager = fileManager
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)
let extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
do {
let extraConfig = try readUserConfig(extraConfURL)
config = config.merged(with: extraConfig)
} catch {
infoLog("Extra config override failed with \(error). Skipping extra configuration")
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()
@@ -343,7 +385,7 @@ class XCRemoteCacheConfigReader {
/// Reads user configuration from a file
private func readUserConfig(_ file: URL) throws -> ConfigFileScheme {
let configurationContent = fileManager.contents(atPath: file.path)
let configurationContent = try fileReader.contents(atPath: file.path)
guard let configurationData = configurationContent else {
throw XCRemoteCacheConfigReaderError.missingConfigurationFile(file)
}
@@ -110,7 +110,7 @@ class PhaseCacheModeController: CacheModeController {
} 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).")
debugLog("Couldn't verify if should disable RC for \(commitValue).")
}
return false
}
@@ -51,34 +51,22 @@ public class FileDependenciesReader: DependenciesReader {
public func findDependencies() throws -> [String] {
let yaml = try readRaw()
struct ParseState {
var buffer: String = ""
var prevChar: Character?
var result: [String] = []
func with(buffer: String? = nil, prevChar: Character? = nil, result: [String]? = nil) -> ParseState {
var new = self
new.buffer = buffer ?? new.buffer
new.prevChar = prevChar ?? new.prevChar
new.result = result ?? new.result
return new
}
}
let dependencies = yaml.reduce(Set<String>()) { prev, arg1 -> Set<String> in
let (key, value) = arg1
switch key {
case "dependencies":
// 'clang' output formatting
return Set(splitDependencyFileList(value))
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(splitDependencyFileList(value))
return Set(parseDependencyFileList(value))
default:
return prev
}
}
return Array(dependencies)
}
@@ -92,56 +80,92 @@ public class FileDependenciesReader: DependenciesReader {
return yaml.mapValues { $0.components(separatedBy: .whitespaces) }
}
private func readRaw() throws -> [String: String] {
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
}
// .d matches the .yaml format
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
}
/// Splits space or new line separated files into a set of files
/// 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
private func splitDependencyFileList(_ string: String) -> [String] {
struct ParseState {
var buffer: String = ""
var prevChar: Character?
var result: [String] = []
func with(buffer: String? = nil, prevChar: Character? = nil, result: [String]? = nil) -> ParseState {
var new = self
new.buffer = buffer ?? new.buffer
new.prevChar = prevChar ?? new.prevChar
new.result = result ?? new.result
return new
}
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
}
}
let parseResult = string.reduce(ParseState()) { total, char in
switch char {
case "\n" where total.prevChar == "\\":
return total
case " " where total.buffer.isEmpty:
return total
case " " where total.prevChar == "\\":
return total.with(buffer: "\(total.buffer) ")
case " ":
return total.with(buffer: "", prevChar: nil, result: total.result + [total.buffer])
case "\\":
return total.with(prevChar: "\\")
default:
return total.with(buffer: "\(total.buffer)\(char)", prevChar: char, result: total.result)
}
if startIndex != endIndex {
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
result.append(buffer)
}
if !parseResult.buffer.isEmpty {
return parseResult.result + [parseResult.buffer]
}
return parseResult.result
return result
}
}
@@ -50,7 +50,7 @@ public class FileDependenciesWriter: DependenciesWriter {
var content = ""
for (file, deps) in dependencies {
content.append(file + ": ")
content.append(deps.joined(separator: " "))
content.append(deps.map { $0.replacingOccurrences(of: " ", with: "\\ ") }.joined(separator: " "))
content.append("\n")
}
try content.write(to: file, atomically: true, encoding: .utf8)
@@ -27,8 +27,11 @@ public struct Dependency: Equatable {
case source
case fingerprint
case intermediate
case derivedFile
// Product of the target itself
case ownProduct
// User-excluded path
case userExcluded
case unknown
}
@@ -55,14 +58,18 @@ class DependencyProcessorImpl: DependencyProcessor {
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, bundle: URL?) {
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] {
@@ -73,10 +80,14 @@ class DependencyProcessorImpl: DependencyProcessor {
private func classify(_ files: [URL]) -> [Dependency] {
return files.map { file -> Dependency in
let filePath = file.resolvingSymlinksInPath().path
if filePath.hasPrefix(xcodePath) {
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
@@ -107,7 +118,12 @@ class DependencyProcessorImpl: DependencyProcessor {
// - 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
let irrelevantDependenciesType: [Dependency.Kind] = [.xcode, .intermediate, .ownProduct]
// - 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)
}
}
@@ -30,6 +30,10 @@ protocol FingerprintSyncer {
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 {
@@ -78,4 +82,25 @@ class FileFingerprintSyncer: FingerprintSyncer {
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)
}
}
@@ -33,8 +33,13 @@ protocol DirScanner {
/// Returns all items in a directory (shallow search)
/// - Parameter at: url of an existing directory to search
/// - Throws: an error if dir doesn't exist or I/O error
/// - Throws: an error if a dir doesn't exist or on I/O error
func items(at dir: URL) throws -> [URL]
/// Returns all items in a directory (recursive search)
/// - Parameter at: url of an existing directory to search
/// - Throws: an error if a dir doesn't exist or on I/O error
func recursiveItems(at dir: URL) throws -> [URL]
}
typealias DirAccessor = FileAccessor & DirScanner
@@ -54,4 +59,18 @@ extension FileManager: DirScanner {
let resolvedDir = dir.resolvingSymlinksInPath()
return try contentsOfDirectory(at: resolvedDir, includingPropertiesForKeys: nil, options: [])
}
func recursiveItems(at dir: URL) throws -> [URL] {
// Iterating DFS
var queue: [URL] = [dir]
var results: [URL] = []
while let item = queue.popLast() {
if try itemType(atPath: item.path) == .dir {
try queue.append(contentsOf: items(at: item))
} else {
results.append(item)
}
}
return results
}
}
@@ -52,4 +52,8 @@ class DirAccessorComposer: DirAccessor {
func items(at dir: URL) throws -> [URL] {
try dirScanner.items(at: dir)
}
func recursiveItems(at dir: URL) throws -> [URL] {
try dirScanner.recursiveItems(at: dir)
}
}
@@ -52,7 +52,9 @@ class EnvironmentFingerprintGenerator {
}
try fill(envKeys: Self.defaultEnvFingerprintKeys + customFingerprintEnvs)
try generator.append(version)
return try generator.generate()
let result = try generator.generate()
generatedFingerprint = result
return result
}
/// Creates a fingerprint of the environemtn, by hashing all ENVs specified in keys
@@ -23,17 +23,21 @@ struct AWSV4Signature {
let secretKey: String
let accessKey: String
let securityToken: String?
let region: String
let service: String
let date: Date
func addSignatureHeaderTo(request: inout URLRequest) {
request.setValue(request.url?.host, forHTTPHeaderField: "host")
request.setValue(StringToSign.ISO8601BasicFormatter.string(from: date), forHTTPHeaderField: "x-amz-date")
request.setValue((request.httpBody ?? Data()).sha256(), forHTTPHeaderField: "x-amz-content-sha256")
if let securityToken = securityToken {
request.setValue(securityToken, forHTTPHeaderField: "x-amz-security-token")
}
let canonicalRequest = CanonicalRequest(request: request)
let stringToSign = StringToSign(
region: region,
@@ -34,7 +34,15 @@ struct CanonicalRequest {
if url.path.isEmpty {
path = "/"
} else {
path = url.path
if let escapedPath = url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
path = escapedPath
} else {
path = "/"
printWarning("""
Escaping the path \(url.path) failed and a placeholder is used instead. \
Make sure the path doesn't contain invalid characters.
""")
}
}
return
"\(httpMethod)\n" +
@@ -29,12 +29,14 @@ class NetworkClientImpl: NetworkClient {
private let session: URLSession
private let fileManager: FileManager
private let maxRetries: Int
private let retryDelay: TimeInterval
private let awsV4Signature: AWSV4Signature?
init(session: URLSession, retries: Int, fileManager: FileManager, awsV4Signature: AWSV4Signature?) {
init(session: URLSession, retries: Int, retryDelay: TimeInterval, fileManager: FileManager, awsV4Signature: AWSV4Signature?) {
self.session = session
self.fileManager = fileManager
maxRetries = retries
self.maxRetries = retries
self.retryDelay = retryDelay
self.awsV4Signature = awsV4Signature
}
@@ -75,7 +77,12 @@ class NetworkClientImpl: NetworkClient {
func download(_ url: URL, to location: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
var request = URLRequest(url: url)
setupAuthenticationSignatureIfPresent(&request)
makeDownloadRequest(request, output: location, completion: completion)
makeDownloadRequest(
request,
output: location,
retries: maxRetries,
completion: completion
)
}
func upload(_ file: URL, as url: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
@@ -123,7 +130,7 @@ class NetworkClientImpl: NetworkClient {
dataTask.resume()
}
private func makeDownloadRequest(_ request: URLRequest, output: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
private func makeDownloadRequest(_ request: URLRequest, output: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
guard fileManager.fileExists(atPath: output.path) == false else {
infoLog("Download file found in the destination, skipping download.")
completion(.success(()))
@@ -133,6 +140,17 @@ class NetworkClientImpl: NetworkClient {
let dataTask = session.downloadTask(with: request) { [fileManager] fileURL, _, error in
guard let fileURL = fileURL else {
let networkError = error.map(NetworkClientError.build) ?? .inconsistentSession
if retries > 0 {
infoLog("Download request failed with \(networkError). Left retries: \(retries).")
self.retryDownload(
request,
output: output,
retries: retries,
completion: completion,
after: self.retryDelay
)
return
}
errorLog("Download request failed: \(networkError)")
completion(.failure(networkError))
return
@@ -173,7 +191,13 @@ class NetworkClientImpl: NetworkClient {
if let error = responseError {
if retries > 0 {
infoLog("Upload request failed with \(error). Left retries: \(retries).")
self.makeUploadRequest(request, input: input, retries: retries - 1, completion: completion)
self.retryUpload(
request,
input: input,
retries: retries,
completion: completion,
after: self.retryDelay
)
return
}
errorLog("Upload request failed: \(error)")
@@ -184,6 +208,30 @@ class NetworkClientImpl: NetworkClient {
}
dataTask.resume()
}
private func retryUpload(_ request: URLRequest, input: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
guard let self = self else { return }
self.makeUploadRequest(
request,
input: input,
retries: retries - 1,
completion: completion
)
}
}
private func retryDownload(_ request: URLRequest, output: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
guard let self = self else { return }
self.makeDownloadRequest(
request,
output: output,
retries: retries - 1,
completion: completion
)
}
}
}
private extension NetworkClientError {
@@ -27,11 +27,20 @@ class RemoteNetworkClientAbstractFactory {
private let upstreamStreamURL: [URL]
private let networkClient: NetworkClient
private let urlBuilderFactory: (URL) throws -> URLBuilder
private let uploadBatchSize: Int
init(mode: Mode, downloadStreamURL: URL, upstreamStreamURL: [URL], networkClient: NetworkClient, urlBuilderFactory: @escaping (URL) throws -> URLBuilder) {
init(
mode: Mode,
downloadStreamURL: URL,
upstreamStreamURL: [URL],
uploadBatchSize: Int,
networkClient: NetworkClient,
urlBuilderFactory: @escaping (URL) throws -> URLBuilder
) {
self.mode = mode
self.downloadStreamURL = downloadStreamURL
self.upstreamStreamURL = upstreamStreamURL
self.uploadBatchSize = uploadBatchSize
self.networkClient = networkClient
self.urlBuilderFactory = urlBuilderFactory
}
@@ -49,7 +58,8 @@ class RemoteNetworkClientAbstractFactory {
return ReplicatedRemotesNetworkClient(
networkClient,
download: downloadURLBuilder,
uploads: upstreamBuilders
uploads: upstreamBuilders,
uploadBatchSize: uploadBatchSize
)
case .consumer:
return RemoteNetworkClientImpl(networkClient, downloadURLBuilder)
@@ -23,10 +23,12 @@ import Foundation
class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
private let networkClient: NetworkClient
private let uploadURLBuilders: [URLBuilder]
private let uploadBatchSize: Int
init(_ networkClient: NetworkClient, download: URLBuilder, uploads uploadURLBuilders: [URLBuilder]) {
init(_ networkClient: NetworkClient, download: URLBuilder, uploads uploadURLBuilders: [URLBuilder], uploadBatchSize: Int) {
self.networkClient = networkClient
self.uploadURLBuilders = uploadURLBuilders
self.uploadBatchSize = uploadBatchSize
super.init(networkClient, download)
}
@@ -39,6 +41,9 @@ class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
let group = DispatchGroup()
var results: [Result<Void, NetworkClientError>] = Array(repeating: .failure(.noResponse), count: urls.count)
urls.enumerated().forEach { index, url in
if uploadBatchSize > 0 && index > 0 && index % uploadBatchSize == 0 {
group.wait()
}
group.enter()
networkClient.upload(file, as: url) { receivedResult in
results[index] = receivedResult
@@ -58,6 +63,9 @@ class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
let group = DispatchGroup()
var results: [Result<Void, NetworkClientError>] = Array(repeating: .failure(.noResponse), count: urls.count)
urls.enumerated().forEach { index, url in
if uploadBatchSize > 0 && index > 0 && index % uploadBatchSize == 0 {
group.wait()
}
group.enter()
networkClient.create(url) { receivedResult in
results[index] = receivedResult
@@ -55,4 +55,11 @@ extension Dictionary where Key == String, Value == String {
}
return value == "YES"
}
func readEnv(key: String) throws -> Bool? {
guard let value = self[key] else {
return nil
}
return value == "YES"
}
}
+11 -2
View File
@@ -21,7 +21,7 @@ import Foundation
import XCRemoteCache
/// Wrapper for a `LD` program that copies the dynamic executable from a cached-downloaded location
/// Fallbacks to a standard `clang` when the Ramote cache is not applicable (e.g. modified sources)
/// Fallbacks to a standard `clang` when the Remote cache is not applicable (e.g. modified sources)
public class XCLDMain {
public func main() {
let args = ProcessInfo().arguments
@@ -48,7 +48,16 @@ public class XCLDMain {
i += 1
}
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
exit(1, "Missing 'output' argument. Args: \(args)")
let ldCommand = "clang"
print("Fallbacking to compilation using \(ldCommand).")
let args = ProcessInfo().arguments
let paramList = [ldCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(ldCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
+75
View File
@@ -0,0 +1,75 @@
// 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 XCRemoteCache
/// Wrapper for a `LDPLUSPLUS` program that copies the dynamic executable from a cached-downloaded location
/// Fallbacks to a standard `clang++` when the Remote cache is not applicable (e.g. modified sources)
public class XCLDPlusPlusMain {
public func main() {
let args = ProcessInfo().arguments
var output: String?
var filelist: String?
var dependencyInfo: String?
var i = 0
while i < args.count {
switch args[i] {
case "-o":
output = args[i + 1]
i += 1
case "-filelist":
filelist = args[i + 1]
i += 1
case "-dependency_info":
// Skip following `-Xlinker` argument. Sample call:
// `clang -dynamiclib ... -Xlinker -dependency_info -Xlinker /path/Target_dependency_info.dat`
dependencyInfo = args[i + 2]
i += 2
default:
break
}
i += 1
}
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
let ldCommand = "clang++"
print("Fallbacking to compilation using \(ldCommand).")
let args = ProcessInfo().arguments
let paramList = [ldCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(ldCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
// TODO: consider using `clang_command` from .rcinfo
/// concrete clang path should be taken from the current toolchain
let fallbackCommand = "clang++"
XCCreateBinary(
output: outputInput,
filelist: filelistInput,
dependencyInfo: dependencyInfoInput,
fallbackCommand: fallbackCommand,
stepDescription: "xcldplusplus"
).run()
}
}
+22
View File
@@ -0,0 +1,22 @@
// 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 XCRemoteCache
XCLDPlusPlusMain().main()
+9 -38
View File
@@ -18,53 +18,24 @@
// under the License.
import Foundation
import xclibtoolSupport
import XCRemoteCache
public enum XCLibtoolMainError: Error {
case missingOutput
case unsupportedMode
}
/// Wrapper for a `libtool` program that copies the build executable (e.g. .a) from a cached-downloaded location
/// Fallbacks to a standard `libtool` when the Ramote cache is not applicable (e.g. modified sources)
public class XCLibtoolMain {
public init() { }
public func main() {
let args = ProcessInfo().arguments
var output: String?
// all input arguments library '.a'. Used to create an universal binary
var inputLibraries: [String] = []
var filelist: String?
var dependencyInfo: String?
var i = 0
while i < args.count {
switch args[i] {
case "-o":
output = args[i + 1]
i += 1
case "-filelist":
filelist = args[i + 1]
i += 1
case "-dependency_info":
dependencyInfo = args[i + 1]
i += 1
case let input where input.hasSuffix(".a"):
inputLibraries.append(input)
default:
break
}
i += 1
}
guard let outputInput = output else {
exit(1, "Missing 'output' argument. Args: \(args)")
}
let mode: XCLibtoolMode
if let filelistInput = filelist, let dependencyInfoInput = dependencyInfo {
// libtool is creating a library
mode = .createLibrary(output: outputInput, filelist: filelistInput, dependencyInfo: dependencyInfoInput)
} else if !inputLibraries.isEmpty {
// multiple input libraries suggest creating an universal binary
mode = .createUniversalBinary(output: outputInput, inputs: inputLibraries)
} else {
// unknown mode
exit(1, "Unsupported mode. Args: \(args)")
}
do {
let mode = try XCLibtoolHelper.buildMode(args: args)
try XCLibtool(mode).run()
} catch {
exit(1, "Failed with: \(error). Args: \(args)")
@@ -0,0 +1,78 @@
// 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
import XCRemoteCache
public enum XCLibtoolHelperError: Error {
case missingOutput
case unsupportedMode
}
public class XCLibtoolHelper {
public static func buildMode(args: [String]) throws -> XCLibtoolMode {
var output: String?
// all input arguments are '*.a' or no path extension. Used to create an universal binary
var inputLibraries: [String] = []
var filelist: String?
var dependencyInfo: String?
var asksForVersion = false
var i = 0
while i < args.count {
switch args[i] {
case "-V":
asksForVersion = true
case "-o":
output = args[i + 1]
i += 1
case "-filelist":
filelist = args[i + 1]
i += 1
case "-dependency_info":
dependencyInfo = args[i + 1]
i += 1
case let input where ["", "a"].contains(URL(string: args[i])?.pathExtension):
// Support for static frameworks (no extension) and static libraries (.a)
inputLibraries.append(input)
default:
break
}
i += 1
}
if asksForVersion {
return .version
}
guard let outputInput = output else {
throw XCLibtoolHelperError.missingOutput
}
let mode: XCLibtoolMode
if let filelistInput = filelist, let dependencyInfoInput = dependencyInfo {
// libtool is creating a library
mode = .createLibrary(output: outputInput, filelist: filelistInput, dependencyInfo: dependencyInfoInput)
} else if !inputLibraries.isEmpty {
// multiple input libraries suggest creating an universal binary
mode = .createUniversalBinary(output: outputInput, inputs: inputLibraries)
} else {
// unknown mode
throw XCLibtoolHelperError.unsupportedMode
}
return mode
}
}
+71
View File
@@ -0,0 +1,71 @@
// 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
import XCRemoteCache
/// Wrapper for a `lipo` program that links any of input binaries to the destination paths
/// Fallbacks to a standard `lipo` when the Ramote cache is not applicable (e.g. modified sources)
public class XCLipoMain {
public init() { }
public func main() {
let args = ProcessInfo().arguments
var output: String?
var create = false
var inputs: [String] = []
var i = 1
while i < args.count {
switch args[i] {
case "-output":
output = args[i + 1]
i += 1
case "-create":
create = true
default:
inputs.append(args[i])
}
i += 1
}
let lipoCommand = "lipo"
guard let output = output, !inputs.isEmpty, create else {
print("Fallbacking to compilation using \(lipoCommand).")
let args = ProcessInfo().arguments
let paramList = [lipoCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(lipoCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
do {
try XCLipo(
output: output,
inputs: inputs,
fallbackCommand: lipoCommand,
stepDescription: "xclipo"
).run()
} catch {
exit(1, "Failed with: \(error). Args: \(args)")
}
}
}
+22
View File
@@ -0,0 +1,22 @@
// 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 XCRemoteCache
XCLipoMain().main()
+6
View File
@@ -229,6 +229,11 @@ struct XCPrepareMain: ParsableCommand {
)
var fakeSrcRoot: String
@Option(name: .customLong("sdks-exclude"), default: "", help: """
comma separated list of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*")
""", transform: nonEmptyString)
var sdksExclude: String
func run() throws {
XCIntegrate(
@@ -243,6 +248,7 @@ struct XCPrepareMain: ParsableCommand {
consumerEligiblePlatforms: consumerEligiblePlatforms,
lldbMode: lldbInit,
fakeSrcRoot: fakeSrcRoot,
sdksExclude: sdksExclude,
output: output
).main()
}
+10 -2
View File
@@ -60,8 +60,16 @@ public class XCSwiftcMain {
let targetInputInput = target,
let swiftFileListInput = swiftFileList
else {
print("Missing argument. Args: \(args)")
exit(1)
let swiftcCommand = "swiftc"
print("Fallbacking to compilation using \(swiftcCommand).")
let args = ProcessInfo().arguments
let paramList = [swiftcCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(swiftcCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
let swiftcArgsInput = SwiftcArgInput(
objcHeaderOutput: objcHeaderOutputInput,
@@ -28,6 +28,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
private var swiftmoduleDocFile: URL!
private var swiftmoduleSourceInfoFile: URL!
private var swiftmoduleInterfaceFile: URL!
private var privateSwiftmoduleInterfaceFile: URL!
private var abiJsonFile: URL!
private var workingDir: URL!
private var builder: ArtifactSwiftProductsBuilderImpl!
@@ -39,6 +41,9 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
swiftmoduleDocFile = moduleDir.appendingPathComponent("MyModule.swiftdoc")
swiftmoduleSourceInfoFile = moduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
swiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.swiftinterface")
privateSwiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.private.swiftinterface")
abiJsonFile = moduleDir.appendingPathComponent("MyModule.abi.json")
workingDir = rootDir.appendingPathComponent("working")
builder = ArtifactSwiftProductsBuilderImpl(
workingDir: workingDir,
@@ -98,6 +103,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
try fileManager.spt_createEmptyFile(swiftmoduleInterfaceFile)
try fileManager.spt_createEmptyFile(privateSwiftmoduleInterfaceFile)
try fileManager.spt_createEmptyFile(abiJsonFile)
let builderSwiftmoduleDir =
builder
.buildingArtifactSwiftModulesLocation()
@@ -110,6 +117,10 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
let expectedBuildedSwiftInterfaceFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftinterface")
let expectedPrivateSwiftmoduleInterfaceFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.private.swiftinterface")
let expectedAbiJsonFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.abi.json")
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
@@ -117,6 +128,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduledocFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftInterfaceFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedPrivateSwiftmoduleInterfaceFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedAbiJsonFile.path))
}
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
@@ -32,6 +32,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
private var swiftdocURL: URL!
private var swiftSourceInfoURL: URL!
private var swiftInterfaceURL: URL!
private var privateSwiftInterfaceURL: URL!
private var abiJsonURL: URL!
private var executablePath: String!
private var executableURL: URL!
private var creator: BuildArtifactCreator!
@@ -53,11 +55,16 @@ class BuildArtifactCreatorTests: FileXCTestCase {
.appendingPathComponent("Target.swiftsourceinfo")
swiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
.appendingPathComponent("Target.swiftinterface")
privateSwiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
.appendingPathComponent("Target.private.swiftinterface")
abiJsonURL = workDirectory.appendingPathComponent("Objects-normal")
.appendingPathComponent("Target.abi.json")
executablePath = "libTarget.a"
executableURL = buildDir.appendingPathComponent(executablePath)
dSYM = executableURL.deletingPathExtension().appendingPathExtension(".dSYM")
try fileManager.spt_createEmptyFile(executableURL)
try fileManager.spt_createEmptyFile(headerURL)
let artifactProcessor = NoopArtifactProcessor()
creator = BuildArtifactCreator(
buildDir: buildDir,
@@ -67,6 +74,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
modulesFolderPath: "",
dSYMPath: dSYM,
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: false),
artifactProcessor: artifactProcessor,
fileManager: fileManager
)
}
@@ -122,6 +130,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
try fileManager.spt_createEmptyFile(swiftdocURL)
try fileManager.spt_createEmptyFile(swiftSourceInfoURL)
try fileManager.spt_createEmptyFile(swiftInterfaceURL)
try fileManager.spt_createEmptyFile(privateSwiftInterfaceURL)
try fileManager.spt_createEmptyFile(abiJsonURL)
try creator.includeModuleDefinitionsToTheArtifact(arch: "arch", moduleURL: swiftmoduleURL)
let artifact = try creator.createArtifact(artifactKey: "key", meta: sampleMeta)
@@ -136,6 +146,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftdoc"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftsourceinfo"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftinterface"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.private.swiftinterface"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.abi.json"),
])
}
@@ -0,0 +1,101 @@
// 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.
@testable import XCRemoteCache
import XCTest
class TextFileDependenciesRemapperTests: FileXCTestCase {
let stringsRemapper = StringDependenciesRemapper(
mappings: [
.init(generic: "$(SRCROOT)", local: "/example"),
])
let fileAccessor = FileAccessorFake(mode: .strict)
var remapper: TextFileDependenciesRemapper!
override func setUp() {
super.setUp()
remapper = TextFileDependenciesRemapper(
remapper: stringsRemapper,
fileAccessor: fileAccessor
)
}
func testRemapsGenericPlaceholders() throws {
try fileAccessor.write(toPath: "/file", contents: "Some $(SRCROOT).")
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "Some /example.")
}
func testRemapsLocalPathsToPlaceholders() throws {
try fileAccessor.write(toPath: "/file", contents: "Some /example.")
try remapper.remap(fromLocal: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "Some $(SRCROOT).")
}
func testPersistsEmptyLines() throws {
let multilineData = """
Line1
Line 2
""".data(using: .utf8)
try fileAccessor.write(toPath: "/file", contents: multilineData)
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), multilineData)
}
func testPersistsEmptyLineAtTheEnd() throws {
// swiftlint:disable trailing_whitespace
let multilineData = """
Line1
Line 2
""".data(using: .utf8)
// swiftlint:enable trailing_whitespace
try fileAccessor.write(toPath: "/file", contents: multilineData)
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), multilineData)
}
func testReplacesMultipletimesInLine() throws {
try fileAccessor.write(toPath: "/file", contents: "$(SRCROOT) and $(SRCROOT)")
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "/example and /example")
}
func testReplacesInMultipleLine() throws {
try fileAccessor.write(toPath: "/file", contents: "$(SRCROOT)\n$(SRCROOT)")
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "/example\n/example")
}
}
@@ -0,0 +1,71 @@
// 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.
@testable import XCRemoteCache
import XCTest
class UnzippedArtifactProcessorTests: FileXCTestCase {
private let fileAccessor = FileAccessorFake(mode: .strict)
private let remapper = StringDependenciesRemapper(mappings: [.init(generic: "$(SRCROOT)", local: "/local")])
private var fileRemapper: FileDependenciesRemapper!
private var processor: UnzippedArtifactProcessor!
override func setUp() {
super.setUp()
fileRemapper = TextFileDependenciesRemapper(remapper: remapper, fileAccessor: fileAccessor)
processor = UnzippedArtifactProcessor(
fileRemapper: fileRemapper,
dirScanner: fileAccessor
)
}
func testProcessingRawArtifactReplacesPlaceholders() throws {
try fileAccessor.write(toPath: "/artifact/include/file", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/file"), "Some /local")
}
func testProcessingRawArtifactReplacesInNestedInclude() throws {
try fileAccessor.write(toPath: "/artifact/include/nested/file", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/nested/file"), "Some /local")
}
func testProcessingRawArtifactDoesntReplacesInNonIncludeDir() throws {
try fileAccessor.write(toPath: "/artifact/some/file", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/some/file"), "Some $(SRCROOT)")
}
func testDoesntProcessEmptyFiles() throws {
try fileAccessor.write(toPath: "/artifact/include/.hidden", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/.hidden"), "Some $(SRCROOT)")
}
}
@@ -54,7 +54,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
func testPreparePlacesArtifactInTheActiveLocation() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
@@ -64,7 +68,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
func testPreparingExistingArtifact() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
_ = try organizer.prepare(artifact: zipURL)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
@@ -75,7 +83,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
func testPreparePlacesArtifactInTheFileKeyRelatedLocation() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt", zipFileName: "abb32_fileKey")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let expectedArtifactLocation = workingDirectory.appendingPathComponent("abb32_fileKey")
let preparedArtifact = try organizer.prepare(artifact: zipURL)
@@ -89,7 +101,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
let artifactLocation = workingDirectory.appendingPathComponent("xccache")
.appendingPathComponent(sampleFileKey, isDirectory: true)
try fileManager.createDirectory(at: artifactLocation, withIntermediateDirectories: true, attributes: nil)
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let result = try organizer.prepareArtifactLocationFor(fileKey: sampleFileKey)
if case .artifactExists(artifactDir: let u) = result {
@@ -105,7 +121,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
.appendingPathComponent("xccache")
.appendingPathComponent(sampleFileKey)
.appendingPathExtension("zip")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let result = try organizer.prepareArtifactLocationFor(fileKey: sampleFileKey)
@@ -126,7 +146,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
try fileManager.createDirectory(at: activeArtifact, withIntermediateDirectories: true, attributes: nil)
try fileManager.spt_forceSymbolicLink(at: activeLink, withDestinationURL: activeArtifact)
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let fileKey = try organizer.getActiveArtifactFilekey()
@@ -67,7 +67,8 @@ class ThinningDiskSwiftcProductsGeneratorTests: FileXCTestCase {
artifactSwiftModuleObjCFile: headerFile
)
XCTAssertEqual(generatedModulePath, destinationSwiftModuleDir)
XCTAssertEqual(generatedModulePath.swiftmoduleDir, destinationSwiftModuleDir)
XCTAssertEqual(generatedModulePath.objcHeaderFile, objCHeader)
XCTAssertEqual(fileManager.contents(atPath: expectedSwiftSourceInfoFile.path), "sourceInfo".data(using: .utf8))
XCTAssertEqual(fileManager.contents(atPath: objCHeader.path), "header".data(using: .utf8))
}
@@ -30,6 +30,7 @@ class UnzippedArtifactSwiftProductsOrganizerTests: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
let destination = SwiftcProductsGeneratorOutput(swiftmoduleDir: destination, objcHeaderFile: "")
generator = SwiftcProductsGeneratorSpy(generatedDestination: destination)
dirAccessor = DirAccessorFake()
syncer = FileFingerprintSyncer(
@@ -26,6 +26,7 @@ class PostbuildContextTests: FileXCTestCase {
private static let SampleEnvs = [
"TARGET_NAME": "TARGET_NAME",
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
"DERIVED_FILE_DIR": "DERIVED_FILE_DIR",
"ARCHS": "x86_64",
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal" ,
"CONFIGURATION": "CONFIGURATION",
@@ -43,6 +44,7 @@ class PostbuildContextTests: FileXCTestCase {
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
"CURRENT_VARIANT": "normal",
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
]
override func setUpWithError() throws {
@@ -129,4 +131,59 @@ class PostbuildContextTests: FileXCTestCase {
XCTAssertEqual(context.compilationTempDir, "/OBJECT_FILE_DIR_custom/x86_64")
}
func testGenericPublicHeaderDestinationIsSkipped() throws {
var envs = Self.SampleEnvs
envs["PUBLIC_HEADERS_FOLDER_PATH"] = "/usr/local/include"
let context = try PostbuildContext(config, env: envs)
XCTAssertNil(context.publicHeadersFolderPath)
}
func testPublicHeaderFolderIsRelativeToProductsDir() throws {
var envs = Self.SampleEnvs
envs["BUILT_PRODUCTS_DIR"] = "/MyBuiltProductsDir"
envs["PUBLIC_HEADERS_FOLDER_PATH"] = "MyModule.grameworks/Headers"
let context = try PostbuildContext(config, env: envs)
XCTAssertEqual(context.publicHeadersFolderPath, "/MyBuiltProductsDir/MyModule.grameworks/Headers")
}
func testPublicHeaderFolderPathEnvIsOptional() throws {
var envs = Self.SampleEnvs
envs.removeValue(forKey: "PUBLIC_HEADERS_FOLDER_PATH")
let context = try PostbuildContext(config, env: envs)
XCTAssertNil(context.publicHeadersFolderPath)
}
func testDisabledEnvIsFalseByDefault() throws {
var envs = Self.SampleEnvs
envs.removeValue(forKey: "XCRC_DISABLED")
let context = try PostbuildContext(config, env: envs)
XCTAssertFalse(context.disabled)
}
func testDisabledIsTrueForYesEnv() throws {
var envs = Self.SampleEnvs
envs["XCRC_DISABLED"] = "YES"
let context = try PostbuildContext(config, env: envs)
XCTAssertTrue(context.disabled)
}
func testDisabledIsFalseForNonYesEnv() throws {
var envs = Self.SampleEnvs
envs["XCRC_DISABLED"] = "NO"
let context = try PostbuildContext(config, env: envs)
XCTAssertFalse(context.disabled)
}
}
@@ -27,6 +27,7 @@ class PostbuildTests: FileXCTestCase {
mode: .producer,
targetName: "",
targetTempDir: "",
derivedFilesDir: "",
compilationTempDir: "",
configuration: "",
platform: "",
@@ -53,7 +54,10 @@ class PostbuildTests: FileXCTestCase {
thinnedTargets: [],
action: .build,
modeMarkerPath: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
irrelevantDependenciesPaths: [],
publicHeadersFolderPath: nil,
disabled: false
)
private var network = RemoteNetworkClientImpl(
NetworkClientFake(fileManager: .default),
@@ -78,7 +82,9 @@ class PostbuildTests: FileXCTestCase {
product: "/Product",
source: "/Source",
intermediate: "/Intermediate",
bundle: nil
derivedFiles: "/DerivedFiles",
bundle: nil,
skippedRegexes: []
)
private var overrideManager = FingerprintOverrideManagerImpl(
overridingFileExtensions: ["swiftmodule"],
@@ -639,4 +645,79 @@ class PostbuildTests: FileXCTestCase {
XCTAssertEqual(downloadedMeta, expectedMeta)
}
func testDecoratesDerivedSwiftHeaderWithEmptyModulesFolderPath() throws {
let dir = try prepareTempDir()
let derivedSourcesDir = dir
.appendingPathComponent("DerivedSources")
let swiftSwiftHeader = derivedSourcesDir
.appendingPathComponent("MyModule-Swift.h")
let swiftSwiftHeaderOverride = swiftSwiftHeader
.appendingPathExtension("md5")
try fileManager.spt_createEmptyDir(derivedSourcesDir)
try fileManager.spt_createEmptyFile(swiftSwiftHeader)
postbuildContext.moduleName = "MyModule"
postbuildContext.derivedSourcesDir = derivedSourcesDir
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
try postbuild.performBuildCompletion()
XCTAssertTrue(fileManager.fileExists(atPath: swiftSwiftHeaderOverride.path))
}
func testDecoratesPublicSwiftHeaderWithEmptyModulesFolderPath() throws {
let dir = try prepareTempDir()
let productsDir = dir
.appendingPathComponent("MyModule.framework")
.appendingPathComponent("Headers")
let swiftSwiftHeader = productsDir
.appendingPathComponent("MyModule-Swift.h")
let swiftSwiftHeaderOverride = swiftSwiftHeader
.appendingPathExtension("md5")
try fileManager.spt_createEmptyDir(productsDir)
try fileManager.spt_createEmptyFile(swiftSwiftHeader)
postbuildContext.moduleName = "MyModule"
postbuildContext.publicHeadersFolderPath = productsDir
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
try postbuild.performBuildCompletion()
XCTAssertTrue(fileManager.fileExists(atPath: swiftSwiftHeaderOverride.path))
}
}
@@ -63,7 +63,8 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false
)
contextCached = PrebuildContext(
targetTempDir: sampleURL,
@@ -76,7 +77,8 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false
)
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
@@ -241,7 +243,8 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false
)
let prebuild = Prebuild(
@@ -272,7 +275,8 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false
)
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
@@ -335,7 +339,8 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: false,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false
)
try globalCacheSwitcher.enable(sha: "1")
let prebuild = Prebuild(
@@ -353,4 +358,34 @@ class PrebuildTests: FileXCTestCase {
XCTAssertEqual(globalCacheSwitcher.state, .enabled(sha: "1"))
}
func testReturnsDisabledIfXCRCExplicitlyDisabled() throws {
contextNonCached = PrebuildContext(
targetTempDir: sampleURL,
productsDir: sampleURL,
moduleName: nil,
remoteCommit: .unavailable,
remoteCommitLocation: sampleURL,
recommendedCacheAddress: sampleURL,
forceCached: false,
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: "",
disabled: true
)
let prebuild = Prebuild(
context: contextNonCached,
networkClient: remoteNetwork,
remapper: remapper,
fingerprintAccumulator: generator,
artifactsOrganizer: organizer,
globalCacheSwitcher: globalCacheSwitcher,
metaReader: metaReader,
artifactConsumerPrebuildPlugins: []
)
XCTAssertEqual(try prebuild.perform(), .disabled)
}
}
@@ -34,7 +34,9 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
cc: binariesDir.appendingPathComponent("xccc"),
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
libtool: binariesDir.appendingPathComponent("xclibtool"),
lipo: binariesDir.appendingPathComponent("lipo"),
ld: binariesDir.appendingPathComponent("xcld"),
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
)
@@ -43,7 +45,12 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
func testProducerSettingFakeSrcRoot() throws {
let mode: Mode = .producer
let fakeRootURL: URL = "/xxxxxxxxxxP"
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: fakeRootURL,
sdksExclude: []
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
@@ -53,10 +60,90 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
func testConsumerSettingFakeSrcRoot() throws {
let mode: Mode = .consumer
let fakeRootURL: URL = "/xxxxxxxxxxC"
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: fakeRootURL,
sdksExclude: []
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let resultURL: String = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
XCTAssertEqual(resultURL, fakeRootURL.path)
}
func testConsumerSettingLdPlusPlus() throws {
let mode: Mode = .consumer
let fakeRootURL: URL = "/xxxxxxxxxxC"
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: fakeRootURL,
sdksExclude: []
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let ldPlusPlus: String = try XCTUnwrap(result["LDPLUSPLUS"] as? String)
XCTAssertEqual(ldPlusPlus, binaries.ldplusplus.path)
}
func testSinglesdksExcludeIsAppended() throws {
let mode: Mode = .consumer
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: "/",
sdksExclude: ["watchOS*"]
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
XCTAssertEqual(ldPlusPlusWatchOS, "")
}
func testLibtoolIsSetForExcludedSdks() throws {
let mode: Mode = .consumer
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: "/",
sdksExclude: ["watchOS*"]
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let libtoolWatchOS: String = try XCTUnwrap(result["LIBTOOL[sdk=watchOS*]"] as? String)
XCTAssertEqual(libtoolWatchOS, "libtool")
}
func testMultiplesdksExcludeAreAppended() throws {
let mode: Mode = .consumer
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: "/",
sdksExclude: ["watchOS*", "watchsimulator*"]
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
let ldPlusPlusWatchSimulator: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchsimulator*]"] as? String)
XCTAssertEqual(ldPlusPlusWatchOS, "")
XCTAssertEqual(ldPlusPlusWatchSimulator, "")
}
func testAddsDisabledFlagForExcludedSDKs() throws {
let mode: Mode = .consumer
let appender = XcodeProjBuildSettingsIntegrateAppender(
mode: mode,
repoRoot: rootURL,
fakeSrcRoot: "/",
sdksExclude: ["watchOS*", "watchsimulator*"]
)
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
let disabledWatchOS: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchOS*]"] as? String)
let disabledWatchSimulator: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchsimulator*]"] as? String)
XCTAssertEqual(disabledWatchOS, "YES")
XCTAssertEqual(disabledWatchSimulator, "YES")
}
}
@@ -34,7 +34,7 @@ class PrepareMarkContextTests: XCTestCase {
let repoPath = "/AbsolutePath"
config.repoRoot = repoPath
let context = try PrepareMarkContext(config)
let context = try PrepareMarkContext(config, env: [:])
XCTAssertEqual(context.repoRoot.path, repoPath)
}
@@ -43,8 +43,20 @@ class PrepareMarkContextTests: XCTestCase {
let repoPath = "."
config.repoRoot = repoPath
let context = try PrepareMarkContext(config)
let context = try PrepareMarkContext(config, env: [:])
XCTAssertEqual(context.repoRoot.path, "/Root")
}
func testDisabledIsFalseByDefault() throws {
let context = try PrepareMarkContext(config, env: [:])
XCTAssertFalse(context.disabled)
}
func testDisabledIsTrueForYes() throws {
let context = try PrepareMarkContext(config, env: ["XCRC_DISABLED": "YES"])
XCTAssertTrue(context.disabled)
}
}
@@ -89,4 +89,66 @@ class PrepareTests: XCTestCase {
XCTAssertEqual(result, .preparedFor(sha: .init(sha: "2", age: 0), recommendedCacheAddress: remoteURL))
}
func testFailsForMissingCommonShaWhenConfiguredToGrefullyDisable() throws {
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
config.gracefullyHandleMissingCommonSha = true
let prepare = Prepare(
context: try PrepareContext(config, offline: false),
gitClient: git,
networkClients: [],
ccBuilder: CCWrapperBuilderFake(),
fileAccessor: FileManager.default,
globalCacheSwitcher: globalCacheSwitcher,
cacheInvalidator: CacheInvalidatorFake()
)
let result = try prepare.prepare()
XCTAssertEqual(result, .failed)
}
func testDisablesGlobalCacheForMissingCommonShaWhenConfiguredToGrefullyDisable() throws {
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
config.gracefullyHandleMissingCommonSha = true
try globalCacheSwitcher.enable(sha: "starting_state")
let prepare = Prepare(
context: try PrepareContext(config, offline: false),
gitClient: git,
networkClients: [],
ccBuilder: CCWrapperBuilderFake(),
fileAccessor: FileManager.default,
globalCacheSwitcher: globalCacheSwitcher,
cacheInvalidator: CacheInvalidatorFake()
)
_ = try prepare.prepare()
XCTAssertEqual(globalCacheSwitcher.state, .disabled)
}
func testThrowsForMissingCommonSha() throws {
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
config.gracefullyHandleMissingCommonSha = false
let prepare = Prepare(
context: try PrepareContext(config, offline: false),
gitClient: git,
networkClients: [],
ccBuilder: CCWrapperBuilderFake(),
fileAccessor: FileManager.default,
globalCacheSwitcher: globalCacheSwitcher,
cacheInvalidator: CacheInvalidatorFake()
)
XCTAssertThrowsError(try prepare.prepare()) { error in
switch error {
case GitClientError.noCommonShaWithPrimaryRepo: break
default:
XCTFail("Not expected error")
}
}
}
}
@@ -70,7 +70,9 @@ class SwiftcTests: FileXCTestCase {
)
context = try SwiftcContext(config: config, input: input)
markerWriter = MarkerWriterSpy()
productsGenerator = SwiftcProductsGeneratorSpy()
productsGenerator = SwiftcProductsGeneratorSpy(
generatedDestination: SwiftcProductsGeneratorOutput(swiftmoduleDir: "", objcHeaderFile: "")
)
let dependenciesWriterSpy = DependenciesWriterSpy()
self.dependenciesWriterSpy = dependenciesWriterSpy
dependenciesWriterFactory = { [dependenciesWriterSpy] _, _ in dependenciesWriterSpy }
@@ -283,6 +285,12 @@ class SwiftcTests: FileXCTestCase {
let artifactSwiftInterfaceInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftinterface"
)
let artifactPrivateSwiftInterfaceInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.private.swiftinterface"
)
let artifactAbiJsonInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.abi.json"
)
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
let swiftc = Swiftc(
@@ -303,17 +311,14 @@ class SwiftcTests: FileXCTestCase {
_ = try swiftc.mockCompilation()
let swiftModuleFiles = try productsGenerator.generated.first.unwrap()
let swiftModuleURL = swiftModuleFiles.0[.swiftmodule]
let swiftDocURL = swiftModuleFiles.0[.swiftdoc]
let swiftSourceInfoURL = swiftModuleFiles.0[.swiftsourceinfo]
let swiftInterfaceURL = swiftModuleFiles.0[.swiftinterface]
let swiftHeaderURL = swiftModuleFiles.1
XCTAssertEqual(swiftModuleURL, artifactSwiftmodule)
XCTAssertEqual(swiftDocURL, artifactSwiftdoc)
XCTAssertEqual(swiftSourceInfoURL, artifactSwiftSourceInfo)
XCTAssertEqual(swiftHeaderURL, artifactObjCHeader)
XCTAssertEqual(swiftInterfaceURL, artifactSwiftInterfaceInfo)
XCTAssertEqual(swiftModuleFiles.0[.swiftmodule], artifactSwiftmodule)
XCTAssertEqual(swiftModuleFiles.0[.swiftdoc], artifactSwiftdoc)
XCTAssertEqual(swiftModuleFiles.0[.swiftsourceinfo], artifactSwiftSourceInfo)
XCTAssertEqual(swiftModuleFiles.0[.swiftinterface], artifactSwiftInterfaceInfo)
XCTAssertEqual(swiftModuleFiles.0[.privateSwiftinterface], artifactPrivateSwiftInterfaceInfo)
XCTAssertEqual(swiftModuleFiles.0[.abiJson], artifactAbiJsonInfo)
XCTAssertEqual(swiftModuleFiles.1, artifactObjCHeader)
}
@@ -47,7 +47,8 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
let app = appDir.appendingPathComponent("xccc")
try? fileManager.removeItem(at: appDir)
try? FileManager.default.createDirectory(at: appDir, withIntermediateDirectories: true, attributes: nil)
try? builder.compile(to: app, commitSha: commitSha)
// swiftlint:disable:next force_try
try! builder.compile(to: app, commitSha: commitSha)
return app
}()
@@ -332,11 +333,42 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
XCTAssertNotEqual(newFileOutputData, Data())
}
func testPCHObjCCompilationFallbacks() throws {
// Marker is empty to mimic the new file scenario
let pchFile = directory.appendingPathComponent("input.pch")
createValidPCHFile(pchFile)
arguments = ["-x", "objective-c-header", "-MF", dependencyFile.path, "-o", outputFile.path, pchFile.path]
try shellExec(Self.xccc.path, args: arguments, inDir: directory.path)
XCTAssertTrue(fileManager.fileExists(atPath: outputFile.path))
}
func testPCHCCompilationFallbacks() throws {
// Marker is empty to mimic the new file scenario
let pchFile = directory.appendingPathComponent("input.pch")
createValidPCHFile(pchFile)
arguments = ["-x", "c-header", "-MF", dependencyFile.path, "-o", outputFile.path, pchFile.path]
try shellExec(Self.xccc.path, args: arguments, inDir: directory.path)
XCTAssertTrue(fileManager.fileExists(atPath: outputFile.path))
}
/// Creates a simple C code in the location
private func createValidCFile(_ location: URL) {
fileManager.createFile(atPath: location.path, contents: "int main(){}".data(using: .utf8), attributes: nil)
}
/// Creates a simple PCH code in the location
private func createValidPCHFile(_ location: URL) {
fileManager.createFile(
atPath: location.path,
contents: "#import <Availability.h>".data(using: .utf8),
attributes: nil
)
}
/// Creates a C code that requires extra CUSTOM_STR clang macro to compile
private func createInvalidCFile(_ location: URL) {
fileManager.createFile(
@@ -0,0 +1,95 @@
// 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.
@testable import XCRemoteCache
import XCTest
class XCRemoteCacheConfigReaderTests: XCTestCase {
private var fileReader: FileAccessorFake!
private var reader: XCRemoteCacheConfigReader!
override func setUp() {
super.setUp()
fileReader = FileAccessorFake(mode: .normal)
reader = XCRemoteCacheConfigReader(srcRootPath: "/", fileReader: fileReader)
}
func testReadsFromExtraConfig() throws {
let contents = [
"cache_addresses: [test]",
"retry_delay: 30",
"upload_batch_size: 5",
].joined(separator: "\n").data(using: .utf8)
try fileReader.write(toPath: "/.rcinfo", contents: contents)
let config = try reader.readConfiguration()
XCTAssertEqual(config.cacheAddresses, ["test"])
XCTAssertEqual(config.retryDelay, 30)
XCTAssertEqual(config.uploadBatchSize, 5)
}
func testOverridesExtraConfigFromExtraFile() throws {
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
try fileReader.write(toPath: "/user.rcinfo", contents: "cache_addresses: [user]")
let config = try reader.readConfiguration()
XCTAssertEqual(config.cacheAddresses, ["user"])
}
func testReadsExtraConfigMultipleTimes() throws {
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
try fileReader.write(toPath: "/user.rcinfo", contents: """
cache_addresses: [user]
extra_configuration_file: user2.rcinfo
""")
try fileReader.write(toPath: "/user2.rcinfo", contents: "cache_addresses: [user2]")
let config = try reader.readConfiguration()
XCTAssertEqual(config.cacheAddresses, ["user2"])
}
func testBreaksImportingExtraConfigIfReachingALoop() throws {
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
try fileReader.write(toPath: "/user.rcinfo", contents: """
cache_addresses: [user]
extra_configuration_file: .rcinfo
""")
let config = try reader.readConfiguration()
XCTAssertEqual(config.cacheAddresses, ["user"])
}
func testBreaksImportingExtraConfigIfFileDoesntExist() throws {
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
try fileReader.write(toPath: "/user.rcinfo", contents: """
cache_addresses: [user]
extra_configuration_file: nonexisting.rcinfo
""")
let config = try reader.readConfiguration()
XCTAssertEqual(config.cacheAddresses, ["user"])
XCTAssertEqual(config.extraConfigurationFile, "nonexisting.rcinfo")
}
}
@@ -0,0 +1,139 @@
// 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.
@testable import XCRemoteCache
import XCTest
class DependenciesReaderPerformanceTest: XCTestCase {
private static let resourcesSubdirectory = "TestData/Dependencies/DependenciesReaderPerformanceTest"
private func pathForTestData(name: String) throws -> URL {
return try XCTUnwrap(Bundle.module.url(
forResource: name,
withExtension: "d",
subdirectory: DependenciesReaderPerformanceTest.resourcesSubdirectory
))
}
func testFindDependenciesPerformance() throws {
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
self.measure { // 0.005
do {
_ = try reader.findDependencies()
} catch {
print("Error reading dependencies")
}
}
}
func testReadRawFilePerformance() throws {
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
self.measure { // 0.002
do {
_ = try reader.readRaw()
} catch {
print("Error reading dependencies")
}
}
}
func testGetFileDataPerformance() throws {
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
self.measure { // 0.00008
do {
_ = try reader.getFileData()
} catch {
print("Error reading dependencies")
}
}
}
func testGetFileStringPerformance() throws {
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
let fileData = try reader.getFileData()
self.measure { // 0.00002
do {
_ = try reader.getFileStringFromData(fileData: fileData)
} catch {
print("Error reading dependencies")
}
}
}
func testGetYamlPerformance() throws { // 0.222
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
let fileData = try reader.getFileData()
let fileString = try reader.getFileStringFromData(fileData: fileData)
self.measure { // 0.0022
do {
_ = try reader.getYaml(fileString: fileString)
} catch {
print("Error reading dependencies")
}
}
}
func testParseDependencyFileListUsingUTF8View() throws {
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
let fileData = try reader.getFileData()
let fileString = try reader.getFileStringFromData(fileData: fileData)
let yaml = try reader.getYaml(fileString: fileString)
guard let dependencies = yaml["dependencies"] else {
XCTAssertTrue(false)
return
}
self.measure { // 0.004
let deps = reader.parseDependencyFileList(dependencies)
XCTAssertTrue(deps.count == 1000)
}
}
func testDeprecatedParseDependenciesFilesListOfAnObjectUsingUTF8View() throws {
let file = try pathForTestData(name: "dependencies")
let reader = FileDependenciesReader(file, accessor: FileManager.default)
let fileData = try reader.getFileData()
let fileString = try reader.getFileStringFromData(fileData: fileData)
let yaml = try reader.getYaml(fileString: fileString)
guard let dependencies = yaml["/This/Is/A/Path/To/Some/Object/objectfile.o"] else {
XCTAssertTrue(false)
return
}
self.measure { // 0.00048
let deps = reader.parseDependencyFileList(dependencies)
XCTAssertTrue(deps.count == 100)
}
}
}
@@ -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.
@testable import XCRemoteCache
import XCTest
class FileDependenciesWriterTests: XCTestCase {
private func generateTempFileURL(name: String = #function) throws -> URL {
let directory = NSTemporaryDirectory()
return try NSURL.fileURL(withPathComponents: [directory, name]).unwrap()
}
func testWriteDependencyWithSpace() throws {
let url = try generateTempFileURL()
let writer = FileDependenciesWriter(url, accessor: FileManager.default)
try writer.writeGeneric(dependencies: [
"/SomePath/Pods/Target Support Files/lottie-ios/lottie-ios-dummy.m",
])
let expectedContent = """
dependencies: /SomePath/Pods/Target\\ Support\\ Files/lottie-ios/lottie-ios-dummy.m
"""
let content = String(data: try Data(contentsOf: url), encoding: .utf8)
XCTAssertEqual(content, expectedContent)
}
}
@@ -27,17 +27,21 @@ class DependencyProcessorImplTests: FileXCTestCase {
product: "/Product",
source: "/Source",
intermediate: "/Intermediate",
bundle: "/Bundle"
derivedFiles: "/DerivedFiles",
bundle: "/Bundle",
skippedRegexes: []
)
func testIntermediateFileIsSkippedForProductAndSourceSubdirectory() {
func testIntermediateFileIsskippedRegexesForProductAndSourceSubdirectory() {
let intermediateFile: URL = "/Intermediate/some"
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
product: "/",
source: "/",
intermediate: "/Intermediate",
bundle: nil
derivedFiles: "/DerivedFiles",
bundle: nil,
skippedRegexes: []
)
XCTAssertEqual(
@@ -46,14 +50,16 @@ class DependencyProcessorImplTests: FileXCTestCase {
)
}
func testBundleFileIsSkippedForProductAndSourceSubdirectory() {
func testBundleFileIsskippedRegexesForProductAndSourceSubdirectory() {
let bundleFile: URL = "/Bundle/some"
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
product: "/",
source: "/",
intermediate: "/Intermediate",
bundle: "/Bundle"
derivedFiles: "/DerivedFiles",
bundle: "/Bundle",
skippedRegexes: []
)
XCTAssertEqual(
@@ -62,6 +68,22 @@ class DependencyProcessorImplTests: FileXCTestCase {
)
}
func testFiltersOutGeneratedSwiftHeaders() throws {
let dependencies = processor.process([
"/DerivedFiles/ModuleName-Swift.h",
])
XCTAssertEqual(dependencies, [])
}
func testFiltersOutDerivedFile() throws {
let dependencies = processor.process([
"/DerivedFiles/output.h",
])
XCTAssertEqual(dependencies, [])
}
func testFiltersOutProductModulemap() throws {
let dependencies = processor.process([
"/Product/some.modulemap",
@@ -130,7 +152,9 @@ class DependencyProcessorImplTests: FileXCTestCase {
product: "/Product",
source: "/Source",
intermediate: intermediateDirReal,
bundle: "/Bundle"
derivedFiles: "/DerivedFiles",
bundle: "/Bundle",
skippedRegexes: []
)
let intermediateFileSymlink = createSymlink(
@@ -158,7 +182,9 @@ class DependencyProcessorImplTests: FileXCTestCase {
product: "/Product",
source: sourceDirReal,
intermediate: "/Intermediate",
bundle: "/Bundle"
derivedFiles: "/DerivedFiles",
bundle: "/Bundle",
skippedRegexes: []
)
let sourceFileSymlink = createSymlink(
@@ -189,4 +215,76 @@ class DependencyProcessorImplTests: FileXCTestCase {
return sourceDir.appendingPathComponent(filename)
}
func testSkipsCustomizedDerivedDirFileUnderSources() {
let derivedFile: URL = "/DerivedFiles/Module-Swift.h"
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
product: "/",
source: "/",
intermediate: "/Intermediate",
derivedFiles: "/DerivedFiles",
bundle: nil,
skippedRegexes: []
)
XCTAssertEqual(
processor.process([derivedFile]),
[]
)
}
func testSkippsFilesWithFullMatch() {
let source: URL = "/someFile.m"
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
product: "/",
source: "/",
intermediate: "/Intermediate",
derivedFiles: "/DerivedFiles",
bundle: nil,
skippedRegexes: ["/someFile\\.m"]
)
XCTAssertEqual(
processor.process([source]),
[]
)
}
func testSkippsFilesWithPartialMatch() {
let derivedModulemap: URL = "/module.modulemap"
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
product: "/product",
source: "/",
intermediate: "/Intermediate",
derivedFiles: "/DerivedFiles",
bundle: nil,
skippedRegexes: ["\\.modulemap$"]
)
XCTAssertEqual(
processor.process([derivedModulemap]),
[]
)
}
func testDoesntSkipFileIfInvalidRegex() {
let source: URL = "/someFile.m"
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
product: "/product",
source: "/",
intermediate: "/Intermediate",
derivedFiles: "/DerivedFiles",
bundle: nil,
skippedRegexes: ["\\"]
)
XCTAssertEqual(
processor.process([source]),
[.init(url: source, type: .source)]
)
}
}
@@ -74,4 +74,51 @@ class FileFingerprintSyncerTests: FileXCTestCase {
XCTAssertTrue(fileManager.fileExists(atPath: nonOverrideFile.path))
}
func testDecoratesFile() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
try fileManager.spt_createEmptyFile(header)
try syncer.decorate(file: header, fingerprint: "1")
XCTAssertEqual(try String(contentsOf: headerOverride), "1")
}
func testFileDecorateOverridesPreviousOverlay() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
try fileManager.spt_createEmptyFile(header)
try "1".write(to: headerOverride, atomically: true, encoding: .utf8)
try syncer.decorate(file: header, fingerprint: "2")
XCTAssertEqual(try String(contentsOf: headerOverride), "2")
}
func testDeletesFileOverride() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
try fileManager.spt_createEmptyFile(header)
try fileManager.spt_createEmptyFile(headerOverride)
try syncer.delete(file: header)
XCTAssertFalse(fileManager.fileExists(atPath: headerOverride.path))
}
func testDeletesDoesntDeleteWhenFileIsMissing() throws {
let nonExistingFile = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
XCTAssertNoThrow(try syncer.delete(file: nonExistingFile))
}
func testDeletesDoesntDeleteWhenOverrideIsMissing() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
try fileManager.spt_createEmptyFile(header)
XCTAssertNoThrow(try syncer.delete(file: header))
}
}
@@ -71,4 +71,17 @@ class FileManagerDirScannerTests: FileXCTestCase {
try XCTAssertThrowsError(dirScanner.items(at: dir))
}
func testFindsAllFilesRecursively() throws {
let dir = workingDirectory!.appendingPathComponent("dir")
let nestedDir = dir.appendingPathComponent("nested")
let nestedFile = nestedDir.appendingPathComponent("file")
try fileManager.spt_createEmptyFile(nestedFile)
let allFiles = try dirScanner.recursiveItems(at: dir)
// returned items may contain symbolic links in a path
let allFilesResolve = allFiles.map { $0.resolvingSymlinksInPath() }
XCTAssertEqual(allFilesResolve, [nestedFile])
}
}
@@ -39,13 +39,17 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
private static let currentVersion = "5"
private var config: XCRemoteCacheConfig!
private var generator: FingerprintAccumulator!
private var generator: FingerprintAccumulator! {
return generatorFake
}
private var generatorFake: FingerprintAccumulatorFake!
private var fingerprintGenerator: EnvironmentFingerprintGenerator!
override func setUp() {
super.setUp()
config = XCRemoteCacheConfig(sourceRoot: "")
generator = FingerprintAccumulatorFake()
generatorFake = FingerprintAccumulatorFake()
fingerprintGenerator = EnvironmentFingerprintGenerator(
configuration: config,
env: Self.defaultENV,
@@ -92,4 +96,11 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
XCTAssertEqual(fingerprint, "GCC,YES,TARGET,CONG,PLAT,XC,1,2,3,4,AR,CUSTOM_VALUE,\(Self.currentVersion)")
}
func testFingerprintIsGeneratedOnce() throws {
let fingerprint1 = try fingerprintGenerator.generateFingerprint()
let fingerprint2 = try fingerprintGenerator.generateFingerprint()
XCTAssertEqual(fingerprint1, fingerprint2)
XCTAssertEqual(generatorFake.generateCallsCount, 1)
}
}
@@ -35,10 +35,12 @@ class AWSV4SignatureTest: XCTestCase {
let key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
let accessKey = "AKIDEXAMPLE"
let securityToken = "IQoJb3JpZ2luX2VjENv//////////wEaCXVzLWVhc3Q+bsHwqnovXtl/1JVe61XHMnAw3AIXwOAOxqMvhw=="
AWSV4Signature(
secretKey: key,
accessKey: accessKey,
securityToken: securityToken,
region: "us-east-1",
service: "iam",
date: Date(timeIntervalSince1970: 1_440_938_160)
@@ -49,8 +51,8 @@ class AWSV4SignatureTest: XCTestCase {
func testAuthHeaderContainsCorrectSignature() throws {
XCTAssertEqual(
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, " +
"SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, " +
"Signature=dd479fa8a80364edf2119ec24bebde66712ee9c9cb2b0d92eb3ab9ccdc0c3947",
"SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, " +
"Signature=d26ab974bba1b248f041ea1120064e1fa672d6f06cac2cff42b38acea87b76e5",
request.allHTTPHeaderFields?["Authorization"]
)
}
@@ -37,6 +37,27 @@ class CanonicalRequestTest: XCTestCase {
}
func testCanonicalRequest() {
request.url = URL(
string: "https://region.amazonaws.com/bucket/with%20space?param=value&hej=hej&abd=cde&test=-_.~"
)!
let canonicalRequest = CanonicalRequest(
request: request
)
XCTAssertEqual(
canonicalRequest.value,
"GET\n" +
"/bucket/with%20space\n" +
"abd=cde&hej=hej&param=value&test=-_.~\n" +
"a-header2key:A-Header2Value\n" +
"b-header3key:B-Header3Value\n" +
"c-header4key:C Header 4 Value\n" +
"x-header1key:X-Header1Value\n\n" +
"a-header2key;b-header3key;c-header4key;x-header1key\n" +
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
}
func testCanonicalRequestWithEmptySpaceInPath() {
let canonicalRequest = CanonicalRequest(
request: request
)
@@ -72,7 +72,13 @@ class NetworkClientImplTests: XCTestCase {
configuration.protocolClasses = [URLProtocolStub.self]
session = URLSession(configuration: configuration)
fileManager = FileManager.default
client = NetworkClientImpl(session: session, retries: 0, fileManager: fileManager, awsV4Signature: nil)
client = NetworkClientImpl(
session: session,
retries: 0,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
}
override func tearDown() {
@@ -87,7 +93,7 @@ class NetworkClientImplTests: XCTestCase {
super.tearDown()
}
func waitForResponse<R>(_ action: (@escaping Completion<R>) -> Void) throws -> Result<R, NetworkClientError> {
func waitForResponse<R>(_ action: (@escaping Completion<R>) -> Void, timeout: TimeInterval = 0.1) throws -> Result<R, NetworkClientError> {
let responseExpectation = expectation(description: "RequestResponse")
var receivedResponse: Result<R, NetworkClientError>?
@@ -95,7 +101,7 @@ class NetworkClientImplTests: XCTestCase {
receivedResponse = response
responseExpectation.fulfill()
}
waitForExpectations(timeout: 0.1)
waitForExpectations(timeout: timeout)
return try receivedResponse.unwrap()
}
@@ -141,9 +147,15 @@ class NetworkClientImplTests: XCTestCase {
}
func testUploadFilureWith400Retries() throws {
client = NetworkClientImpl(session: session, retries: 2, fileManager: fileManager, awsV4Signature: nil)
client = NetworkClientImpl(
session: session,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
responses[url] = .success(failureResponse, Data())
_ = try waitForResponse { client.upload(fileURL, as: url, completion: $0) }
_ = try waitForResponse({ client.upload(fileURL, as: url, completion: $0) }, timeout: 0.5)
XCTAssertEqual(
requests.map(\.url),
@@ -153,13 +165,51 @@ class NetworkClientImplTests: XCTestCase {
}
func testUploadSuccessDoesntRetry() throws {
client = NetworkClientImpl(session: session, retries: 0, fileManager: fileManager, awsV4Signature: nil)
client = NetworkClientImpl(
session: session,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
responses[url] = .success(successResponse, Data())
_ = try waitForResponse { client.upload(fileURL, as: url, completion: $0) }
XCTAssertEqual(requests.map(\.url), [url], "Expected 1 request - original only")
}
func testDownloadFilureWith400Retries() throws {
client = NetworkClientImpl(
session: session,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
responses[url] = nil
_ = try waitForResponse({ client.download(url, to: fileURL, completion: $0) }, timeout: 0.5)
XCTAssertEqual(
requests.map(\.url),
Array(repeating: url, count: 3),
"Expected 3 requests (original + 2 retries)"
)
}
func testDownloadSuccessDoesntRetry() throws {
client = NetworkClientImpl(
session: session,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
responses[url] = .success(successResponse, Data())
_ = try waitForResponse { client.download(url, to: fileURL, completion: $0) }
XCTAssertEqual(requests.map(\.url), [url], "Expected 1 request - original only")
}
func testFileExits400CompletesWithFalse() throws {
responses[url] = .success(failureResponse, Data())
let response = try waitForResponse { client.fileExists(url, completion: $0) }
@@ -203,18 +253,25 @@ class NetworkClientImplTests: XCTestCase {
let signature = AWSV4Signature(
secretKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
accessKey: "AKIDEXAMPLE",
securityToken: "IQoJb3JpZ2luX2VjENv//////////wEaCXVzLWVhc3Q+bsHwqnovXtl/1JVe61XHMnAw3AIXwOAOxqMvhw==",
region: "us-east-1",
service: "iam",
date: Date(timeIntervalSince1970: 1_440_938_160)
)
client = NetworkClientImpl(session: session, retries: 0, fileManager: fileManager, awsV4Signature: signature)
client = NetworkClientImpl(
session: session,
retries: 0,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: signature
)
responses[url] = .success(successResponse, Data())
_ = try waitForResponse { client.fetch(url, completion: $0) }
XCTAssertEqual(
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, " +
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, " +
"Signature=acdb223475463b6ce711b063f199387a7a12bd638e4dccaaeb5efcbf159b6454",
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, " +
"Signature=e5578464567fb97fd26e871702e4ec4ff7d61cb87eb72a40d22b80e12da30c34",
try requests[0].allHTTPHeaderFields?["Authorization"].unwrap()
)
}
@@ -39,7 +39,12 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
uploadURLs = try [URL(string: "http://upload1.com").unwrap(), URL(string: "http://upload2.com").unwrap()]
download = URLBuilderFake(downloadURL)
uploads = uploadURLs.map(URLBuilderFake.init)
client = ReplicatedRemotesNetworkClient(networkClient, download: download, uploads: uploads)
client = ReplicatedRemotesNetworkClient(
networkClient,
download: download,
uploads: uploads,
uploadBatchSize: 1
)
}
private func prepareLocalEmptyFile() throws -> URL {
@@ -62,6 +67,30 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedArtifact2))
}
func testUploadsWithLimit() throws {
var expectedArtifacts = [URL]()
var uploadURLs = [URL]()
for index in 0...99 {
let expectedArtifact = try URL(string: "http://upload\(index).com/file/id1").unwrap()
expectedArtifacts.append(expectedArtifact)
let uploadURL = try URL(string: "http://upload\(index).com").unwrap()
uploadURLs.append(uploadURL)
}
uploads = uploadURLs.map(URLBuilderFake.init)
client = ReplicatedRemotesNetworkClient(
networkClient,
download: download,
uploads: uploads,
uploadBatchSize: 10
)
try client.uploadSynchronously(localSampleFile, as: .artifact(id: "id1"))
for expectedArtifact in expectedArtifacts {
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedArtifact))
}
}
func testCreatesInAllStreams() throws {
let expectedMeta1 = try URL(string: "http://upload1.com/meta/commit_id").unwrap()
let expectedMeta2 = try URL(string: "http://upload2.com/meta/commit_id").unwrap()
@@ -71,4 +100,5 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedMeta1))
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedMeta2))
}
}
File diff suppressed because one or more lines are too long
@@ -47,6 +47,16 @@ class DirAccessorFake: DirAccessor {
}
}
func recursiveItems(at dir: URL) throws -> [URL] {
memory.compactMap { url, _ in
// comparing paths to ignore dir or url's "isDir" property
if url.deletingLastPathComponent().path.starts(with: dir.path) {
return url
}
return nil
}
}
func contents(atPath path: String) throws -> Data? {
memory[URL(fileURLWithPath: path)]
}
@@ -60,3 +60,28 @@ class FileAccessorFake: FileAccessor {
return storage[path]?.mdate
}
}
extension FileAccessorFake: DirScanner {
func itemType(atPath path: String) throws -> ItemType {
if storage[path] != nil {
return .file
}
if try !recursiveItems(at: URL(fileURLWithPath: path)).isEmpty {
return .dir
}
return .nonExisting
}
func items(at dir: URL) throws -> [URL] {
storage.keys.map(URL.init(fileURLWithPath:)).filter {
$0.deletingLastPathComponent() == dir
}
}
func recursiveItems(at dir: URL) throws -> [URL] {
let paths = storage.keys.filter {
$0.hasPrefix(dir.path)
}
return paths.map(URL.init(fileURLWithPath:))
}
}
@@ -34,5 +34,11 @@ class FingerprintAccumulatorFake: FingerprintAccumulator {
appendedStrings.append("FILE{\(file.path)}")
}
func generate() throws -> RawFingerprint { return appendedStrings.joined(separator: ",") }
private(set) var generateCallsCount = 0
func generate() throws -> RawFingerprint {
defer {
generateCallsCount += 1
}
return appendedStrings.joined(separator: ",")
}
}
@@ -34,7 +34,10 @@ class GitClientFake: GitClient {
}
func getCommonPrimarySha() throws -> String {
shaHistory[primaryBranchIndex].sha
guard shaHistory.count > primaryBranchIndex else {
throw GitClientError.noCommonShaWithPrimaryRepo(remoteName: "testing", error: "SampleError")
}
return shaHistory[primaryBranchIndex].sha
}
func getShaDate(sha: String) throws -> Date {
@@ -0,0 +1,28 @@
// 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
@testable import XCRemoteCache
/// No-operation processor
class NoopArtifactProcessor: ArtifactProcessor {
func process(rawArtifact url: URL) throws {}
func process(localArtifact url: URL) throws {}
}

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