Compare commits

...

206 Commits

Author SHA1 Message Date
Bartosz Polaczyk 75bac3293a Merge pull request #227 from CharlieSuP1/master
prebuild should run artifactsOrganizer.prepare(artifact:) method even  if artifacts exists locally
2024-07-09 19:43:02 -07:00
Bartosz Polaczyk 8c89e88716 Merge pull request #234 from BalestraPatrick/patch-1
Remove references to Spotify FOSS Slack
2024-07-09 19:42:11 -07:00
Bartosz Polaczyk 0f97aa120f Merge pull request #238 from Coder-Star/bug/action
fix: archive build, the ACTION value is install
2024-07-09 19:41:39 -07:00
CoderStar 9f5c455ea6 fix: archive build, the ACTION value is install 2024-07-09 14:53:53 +08:00
Bartosz Polaczyk 86e64d3eab Merge pull request #239 from polac24/bump-xcode-143
Bump CI to Xcode 14.3.1/macOS14
2024-07-03 07:19:45 -07:00
Bartosz Polaczyk 832e0ffeb0 Install nginx on CI 2024-07-02 23:23:16 -07:00
Bartosz Polaczyk 4db65a9bc5 Bump macOS to 14 2024-07-02 23:05:18 -07:00
Bartosz Polaczyk 34e8c0b911 Update release.yaml 2024-07-02 22:57:14 -07:00
Bartosz Polaczyk acd866c242 Update docs.yaml 2024-07-02 22:57:02 -07:00
Bartosz Polaczyk 2e6729ecaa Bump ci.yaml 2024-07-02 22:56:22 -07:00
Patrick Balestra a05fa9cab5 Remove references to Spotify FOSS Slack
The FOSS Slack is being shut down. We will continue use GitHub issues, discussions, etc. for receiving feedback and bug reports.
2024-01-10 13:28:41 +01:00
Bartosz Polaczyk 487a58aba0 Merge pull request #230 from grigorye/bug/shell-hangup-due-to-unread-pipe
Fixed task hang up in shellInternal due to unread error pipe.
2023-11-22 09:22:27 -08:00
Grigory Entin 62ace6a24f Fixed errorData generation.
Co-authored-by: Bartosz Polaczyk <polac24@gmail.com>
2023-11-21 09:54:22 +01:00
Grigory Entin 0290557197 Fixed task hang up in shellInternal due to unread error pipe. 2023-11-20 15:42:12 +01:00
supeng.charlie 68b1f76cd4 prebuild should run artifactsOrganizer.prepare(artifact:) method even if artifacts exists locally 2023-09-19 17:55:44 +08:00
Bartosz Polaczyk 64472d58bf Merge pull request #225 from polac24/fix-spaces-libtool
Support spaces in libtools paths
2023-08-17 08:54:47 +02:00
Bartosz Polaczyk 3d96dddc91 Fix linting 2023-08-16 14:23:13 +02:00
Bartosz Polaczyk deb6adcb70 Support spaces in libtools paths 2023-08-16 13:13:14 +02:00
Bartosz Polaczyk f8c854d007 Merge pull request #214 from polac24/up-to-date-meta
Ensure up-to-date meta json in the unzipped artifact
2023-08-03 15:48:53 +02:00
Bartosz Polaczyk 7fe04517e7 Merge pull request #222 from polac24/libtool-mode-fix
Improve XCLibtool's argument parsing
2023-08-03 12:51:29 +02:00
Bartosz Polaczyk 5029f9c73b Improve Libtool libraries argument parsing 2023-08-02 22:10:03 +02:00
Bartosz Polaczyk ed256234f1 Merge pull request #221 from grigorye/fix/xclibtool-static-broken-markers
Fixed xclibtool not accounting the artifacts retrieved from cache in certain scenarios.
2023-08-02 21:00:45 +02:00
Grigory Entin fd6e1da054 Fixed -static flag treated as an libtool input (library). 2023-08-02 04:26:23 +02:00
Grigory Entin 574129788c Fixed libtool executable accounted as its own argument. 2023-08-02 04:26:23 +02:00
Bartosz Polaczyk 6a1335ea97 Merge pull request #218 from polac24/support-
[Cocoapods] Ensure the source build phase exist before enabling xcrc
2023-08-01 10:50:27 +02:00
Bartosz Polaczyk be87c3779e Bump version 2023-07-29 23:11:58 -07:00
Bartosz Polaczyk 2c23632732 fix module 2023-07-29 22:55:23 -07:00
Bartosz Polaczyk b048393eb0 Define module of the source phase 2023-07-29 22:51:52 -07:00
Bartosz Polaczyk 8955cb2750 Bail out if source build phase phase doesnt exist 2023-07-29 22:46:54 -07:00
Bartosz Polaczyk afb1f9e531 Merge pull request #215 from polac24/xcode-15-lightweight-support
Add support for Xcode 15
2023-06-14 02:56:02 -04:00
Bartosz Polaczyk 53e7ddd34c Fix linters 2023-06-13 21:57:31 -07:00
Bartosz Polaczyk 344b1f13ca Cleanup 2023-06-13 21:25:34 -07:00
Bartosz Polaczyk bc13f58e73 Add unit tests 2023-06-13 21:08:56 -07:00
Bartosz Polaczyk 47ad8a890b Fix swiftc allowed paths 2023-06-12 23:33:09 -07:00
Bartosz Polaczyk 801920dc87 Fix reference path 2023-06-12 23:25:37 -07:00
Bartosz Polaczyk 37e144c36a Add unit tests 2023-06-12 22:58:05 -07:00
Bartosz Polaczyk f389887e37 Read also dependencies from assetcatalog_dependencies output 2023-06-12 22:22:59 -07:00
Bartosz Polaczyk 587840ddbc Fix linter 2023-06-12 19:44:08 -07:00
Bartosz Polaczyk 42a56b4f5b Add tests for ZipArtifactOrganizer 2023-06-12 19:23:36 -07:00
Bartosz Polaczyk 6a98cb4127 Add unit tests for updater 2023-06-12 18:59:35 -07:00
Bartosz Polaczyk d5e95962b5 Keep fresh meta in active artifact dir 2023-06-12 17:59:05 -07:00
Bartosz Polaczyk b74e002415 Merge pull request #212 from polac24/add-driver-tests
[SwiftDriverIntegraiton] Part IV: Add E2E tests for the swift driver integration #5
2023-06-07 10:07:04 -04:00
Bartosz Polaczyk 9a34a99f0b Merge pull request #210 from polac24/add-driver-sync
[SwiftDriver] Part III - Introduce synchronization between swift-frontend invocations
2023-06-06 02:23:21 -04:00
Bartosz Polaczyk f4eed9e8aa Apply review suggestions 2023-06-05 22:45:41 -07:00
Bartosz Polaczyk 811cc00f0c Add E2E tests for swift driver integration 2023-06-04 08:56:38 -07:00
Bartosz Polaczyk 56d9c208a1 Fix typos 2023-06-03 21:25:31 -07:00
Bartosz Polaczyk 8dffbd4162 Add missing unit tests 2023-06-03 17:50:50 -07:00
Bartosz Polaczyk 3853ce2bc2 Write to the shared frontend lock early 2023-06-03 09:32:53 -07:00
Bartosz Polaczyk 148c99d2f5 Add docs 2023-06-01 20:32:03 -07:00
Bartosz Polaczyk 65bf9156ec Merge pull request #209 from polac24/add-driver-parsing
Swift-driver integration, Part II: add Swift front-end parsing stage
2023-06-01 10:12:38 -04:00
Bartosz Polaczyk 22484b4daf Add syncing implementation 2023-05-31 19:55:30 -07:00
Bartosz Polaczyk 867bbb6265 Fix typos 2023-05-31 10:22:34 -04:00
Bartosz Polaczyk 532484c3ab Add comment 2023-05-30 21:38:22 -07:00
Bartosz Polaczyk 338cbd141a Fix linters 2023-05-30 21:08:05 -07:00
Bartosz Polaczyk f15dd8f98d Add Swift front-end mocking parsing stage 2023-05-30 20:53:48 -07:00
Bartosz Polaczyk ee31a3815f Merge pull request #208 from polac24/support-xcode14
Refactor existing implementation for swift-driver change
2023-05-30 09:58:36 -04:00
Bartosz Polaczyk 5749762b86 Fix compilation 2023-05-29 21:25:46 -07:00
Bartosz Polaczyk f8757b6ee7 Add unit tests 2023-05-29 21:01:53 -07:00
Bartosz Polaczyk c26aaf7d42 Refactor existing implementation for swift-driver change 2023-05-29 20:25:50 -07:00
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
187 changed files with 8169 additions and 505 deletions
+4 -2
View File
@@ -13,14 +13,16 @@ jobs:
args: --strict
macOS:
runs-on: macOS-latest
runs-on: macos-14
env:
XCODE_VERSION: ${{ '13.1' }}
XCODE_VERSION: ${{ '14.3.1' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
- name: Checkout
uses: actions/checkout@v1
- name: Install nginx
run: brew install nginx
- name: Build and Run
run: rake build[release]
- name: Test
+24
View File
@@ -0,0 +1,24 @@
name: Docs
on:
push:
branches:
- master
jobs:
docs:
runs-on: macos-14
env:
XCODE_VERSION: ${{ '14.3.1' }}
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-14
env:
XCODE_VERSION: ${{ '13.1' }}
XCODE_VERSION: ${{ '14.3.1' }}
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"
}
},
{
+33 -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,20 @@ let package = Package(
name: "xcswiftc",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcswift-frontend",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtoolSupport",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtool",
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
),
.target(
name: "xclipo",
dependencies: ["XCRemoteCache"]
),
.target(
@@ -50,15 +63,33 @@ 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",
"xcswift-frontend",
"xclibtool",
"xcpostbuild",
"xcprepare",
"xcld",
"xcldplusplus",
"xclipo",
]
),
.testTarget(
name: "XCRemoteCacheTests",
dependencies: ["XCRemoteCache"],
resources: [.copy("TestData")]
),
.testTarget(
name: "xclibtoolSupportTests",
dependencies: ["xclibtoolSupport"]
),
]
)
+52 -4
View File
@@ -7,7 +7,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)
@@ -44,6 +44,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
- [Limitations](#limitations)
- [FAQ](#faq)
- [Development](#development)
- [Architectural Designs](#architectural-designs)
- [Release](#release)
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
* [Building release package](#building-release-package)
@@ -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 -f "${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:
@@ -295,6 +335,8 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `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 | `[]` | ⬜️ |
@@ -315,6 +357,9 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `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` | ⬜️ |
| `enable_swift_driver_integration` | Enable experimental integration with swift driver, added in Xcode 14 | `false` | ⬜️ |
## Backend cache server
@@ -414,6 +459,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.
* The new Swift driver (introduced by default in Xcode 14.0) is not supported and has to be disabled when using XCRemoteCache
## FAQ
@@ -423,6 +469,10 @@ Follow the [FAQ](docs/FAQ.md) page.
Follow the [Development](docs/Development.md) guide. It has all the information on how to get started.
## Architectural designs
Follow the [Architectural designs](docs/design/ArchitecturalDesigns.md) document that describes and documents XCRemoteCache designs and implementation details.
## Release
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
@@ -448,8 +498,6 @@ The zip package will be generated at `releases/XCRemoteCache.zip`.
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
## Contributing
We feel that a welcoming community is important and we ask that you follow Spotify's
+26 -3
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', 'swiftc', 'xcswift-frontend', 'swift-frontend', 'xcld', 'xcldplusplus', 'xclipo']
PROJECT_NAME = 'XCRemoteCache'
SWIFTLINT_ENABLED = true
@@ -59,6 +59,10 @@ task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args|
# Path of the executable looks like: `.build/(debug|release)/XCRemoteCache`
build_path_base = File.join(DERIVED_DATA_DIR, args.configuration)
# swift-frontent integration requires that the SWIFT_EXEC is `swiftc` so create
# a symbolic link between swiftc->xcswiftc and swift-frontend->xcswift-frontend
system("cd #{build_path_base} && ln -s xcswiftc swiftc")
system("cd #{build_path_base} && ln -s xcswift-frontend swift-frontend")
sdk_build_paths = EXECUTABLE_NAMES.map {|e| File.join(build_path_base, e)}
build_paths.push(sdk_build_paths)
@@ -72,6 +76,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
@@ -114,7 +134,9 @@ def create_release_zip(build_paths)
# Create and move files into the release directory
mkdir_p release_dir
build_paths.each {|p|
cp_r p, release_dir
# -r for recursive
# -P for copying symbolic link as is
system("cp -rP #{p} #{release_dir}")
}
output_artifact_basename = "#{PROJECT_NAME}.zip"
@@ -123,7 +145,8 @@ def create_release_zip(build_paths)
# -X: no extras (uid, gid, file times, ...)
# -x: exclude .DS_Store
# -r: recursive
system("zip -X -x '*.DS_Store' -r #{output_artifact_basename} .") or abort "zip failure"
# -y: to store symbolic links (used for swiftc -> xcswiftc)
system("zip -X -x '*.DS_Store' -r -y #{output_artifact_basename} .") or abort "zip failure"
# List contents of zip file
system("unzip -l #{output_artifact_basename}") or abort "unzip failure"
end
@@ -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
@@ -0,0 +1,72 @@
// 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
enum ArtifactMetaUpdaterError: Error {
/// The prebuild plugin execution was called but the local
/// path to the artifact directory is still unknown
/// Might happen that the artifact processor didn't invoke the updater's
/// .process() after downloading/activating an artifact
case artifactLocationIsUnknown
}
/// Updates the meta file in an unzipped artifact directory, by placing an up-to-date
/// and remapped meta file. Updating the meta in the artifact allows reusing existing
/// artifacts it a new meta.json schema has been released to the meta format, while
/// artifacts are still backward-compatible
class ArtifactMetaUpdater: ArtifactProcessor {
private var artifactLocation: URL?
private let metaWriter: MetaWriter
private let fileRemapper: FileDependenciesRemapper
init(
fileRemapper: FileDependenciesRemapper,
metaWriter: MetaWriter
) {
self.metaWriter = metaWriter
self.fileRemapper = fileRemapper
}
/// Remembers the artifact location, used later in the plugin
/// - Parameter url: artifact's root directory
func process(rawArtifact url: URL) throws {
// Storing the location of the just downloaded/activated artifact
// Note, the `url` location already includes a meta (generated by producer
// while compiling and building an artifact)
artifactLocation = url
}
func process(localArtifact url: URL) throws {
// No need to do anything in the postbuild
}
}
extension ArtifactMetaUpdater: ArtifactConsumerPrebuildPlugin {
/// Updates the meta json file in a local, unzipped, artifact location. It also remaps
/// all paths so other steps (like actool or postbuild) don't have to do it again
func run(meta: MainArtifactMeta) throws {
guard let artifactLocation = artifactLocation else {
throw ArtifactMetaUpdaterError.artifactLocationIsUnknown
}
let metaURL = try metaWriter.write(meta, locationDir: artifactLocation)
try fileRemapper.remap(fromGeneric: metaURL)
}
}
@@ -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
@@ -47,11 +47,16 @@ protocol ArtifactOrganizer {
}
class ZipArtifactOrganizer: ArtifactOrganizer {
static let activeArtifactLocation = "active"
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
}
@@ -60,7 +65,7 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
}
func getActiveArtifactLocation() -> URL {
return cacheDir.appendingPathComponent("active")
return cacheDir.appendingPathComponent(Self.self.activeArtifactLocation)
}
func getActiveArtifactFilekey() throws -> String {
@@ -87,16 +92,27 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
let destinationURL = artifact.deletingPathExtension()
guard fileManager.fileExists(atPath: destinationURL.path) == false else {
infoLog("Skipping artifact, already existing at \(destinationURL)")
try runArtifactProcessors(artifactLocation: destinationURL)
return destinationURL
}
// Uzipping to a temp file first to never leave the uncompleted zip in the final location
// Unzipping to a temp file first to never leave the uncompleted zip in the final location
// when the command was interrupted (internal crash or `kill -9` signal)
let tempDestination = destinationURL.appendingPathExtension("tmp")
try Zip.unzipFile(artifact, destination: tempDestination, overwrite: true, password: nil)
try fileManager.moveItem(at: tempDestination, to: destinationURL)
try runArtifactProcessors(artifactLocation: destinationURL)
return destinationURL
}
/// Iterates all processor when an artifact has been just downloaded or reused from already downloaded
/// and previously processed location
private func runArtifactProcessors(artifactLocation: URL) throws {
try artifactProcessors.forEach { processor in
try processor.process(rawArtifact: artifactLocation)
}
}
func activate(extractedArtifact: URL) throws {
let activeLocationURL = getActiveArtifactLocation()
try fileManager.spt_forceSymbolicLink(at: activeLocationURL, withDestinationURL: extractedArtifact)
@@ -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() {
@@ -55,7 +66,7 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
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,15 @@ 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
/// The LLBUILD_BUILD_ID ENV that describes the compilation identifier
/// it is used in the swift-frontend flow
let llbuildIdLockFile: URL
}
extension PostbuildContext {
@@ -91,6 +101,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 +144,18 @@ 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
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
llbuildIdLockFile = SwiftFrontendContext.buildLlbuildIdSharedLockUrl(
llbuildId: llbuildId,
tmpDir: targetTempDir
)
}
}
@@ -60,6 +60,7 @@ public class XCPostbuild {
dependenciesWriter: FileDependenciesWriter.init,
dependenciesReader: FileDependenciesReader.init,
markerWriter: NoopMarkerWriter.init,
llbuildLockFile: context.llbuildIdLockFile,
fileManager: fileManager
)
@@ -87,8 +88,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 +104,7 @@ public class XCPostbuild {
modulesFolderPath: context.modulesFolderPath,
dSYMPath: context.dSYMPath,
metaWriter: metaWriter,
artifactProcessor: UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager),
fileManager: fileManager
)
let dirAccessor = DirAccessorComposer(
@@ -123,6 +131,7 @@ public class XCPostbuild {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.uploadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -130,6 +139,7 @@ public class XCPostbuild {
mode: context.mode,
downloadStreamURL: context.recommendedCacheAddress,
upstreamStreamURL: context.cacheAddresses,
uploadBatchSize: config.uploadBatchSize,
networkClient: networkClient,
urlBuilderFactory: {
try URLBuilderImpl(
@@ -143,9 +153,14 @@ public class XCPostbuild {
let fileReaderFactory: (URL) -> DependenciesReader = {
FileDependenciesReader($0, accessor: fileManager)
}
let assetsFileDependenciesFactory: (URL) -> DependenciesReader = {
AssetsFileDependenciesReader($0, dirAccessor: fileManager)
}
let dependenciesReader = TargetDependenciesReader(
context.compilationTempDir,
fileDependeciesReaderFactory: fileReaderFactory,
compilationOutputDir: context.compilationTempDir,
assetsCatalogOutputDir: context.targetTempDir,
fileDependenciesReaderFactory: fileReaderFactory,
assetsDependenciesReaderFactory: assetsFileDependenciesFactory,
dirScanner: fileManager
)
var remappers: [DependenciesRemapper] = []
@@ -171,7 +186,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(
@@ -198,7 +215,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 =
@@ -260,7 +280,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
}
@@ -87,6 +91,7 @@ class Prebuild {
switch artifactPreparationResult {
case .artifactExists(let artifactDir):
infoLog("Artifact exists locally at \(artifactDir)")
_ = try artifactsOrganizer.prepare(artifact: artifactDir)
try artifactsOrganizer.activate(extractedArtifact: artifactDir)
case .preparedForArtifact(let artifactPackage):
infoLog("Downloading artifact to \(artifactPackage)")
@@ -46,6 +46,11 @@ 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
/// The LLBUILD_BUILD_ID ENV that describes the compilation identifier
/// it is used in the swift-frontend flow
let llbuildIdLockFile: URL
}
extension PrebuildContext {
@@ -69,5 +74,11 @@ 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
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
llbuildIdLockFile = SwiftFrontendContext.buildLlbuildIdSharedLockUrl(
llbuildId: llbuildId,
tmpDir: targetTempDir
)
}
}
@@ -55,6 +55,7 @@ public class XCPrebuild {
dependenciesWriter: FileDependenciesWriter.init,
dependenciesReader: FileDependenciesReader.init,
markerWriter: lazyMarkerWriterFactory,
llbuildLockFile: context.llbuildIdLockFile,
fileManager: fileManager
)
@@ -78,6 +79,10 @@ public class XCPrebuild {
exit(0)
}
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
context.compilationHistoryFile,
fileManager: fileManager
)
do {
let envFingerprint = try EnvironmentFingerprintGenerator(
configuration: config,
@@ -105,6 +110,7 @@ public class XCPrebuild {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.downloadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -147,17 +153,29 @@ 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 metaUpdater = ArtifactMetaUpdater(
fileRemapper: fileRemapper,
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: true)
)
let organizer = ZipArtifactOrganizer(
targetTempDir: context.targetTempDir,
artifactProcessors: [artifactProcessor, metaUpdater],
fileManager: fileManager
)
let metaReader = JsonMetaReader(fileAccessor: fileManager)
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = [metaUpdater]
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,
@@ -189,7 +207,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(
@@ -197,6 +217,7 @@ public class XCPrebuild {
errorMessage: "Prebuild step failed with error: \(error)"
)
}
compilationHistoryOrganizer.reset()
}
private func disableRemoteCache(modeController: PhaseCacheModeController, errorMessage: String?) {
@@ -396,7 +396,8 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
const char *output_arg_name = "-o";
const char *serialize_diagnostics_arg_name = "--serialize-diagnostics";
const char *language_mode_arg_name = "-x";
const char *precompile_header_arg_value = "objective-c-header";
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)";
@@ -464,7 +465,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
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_header_arg_value) == 0) {
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);
}
@@ -21,6 +21,11 @@ import Foundation
typealias BuildSettings = [String: Any]
struct BuildSettingsIntegrateAppenderOption: OptionSet {
let rawValue: Int
static let disableSwiftDriverIntegration = BuildSettingsIntegrateAppenderOption(rawValue: 1 << 0)
}
// Manages Xcode build settings
protocol BuildSettingsIntegrateAppender {
/// Appends XCRemoteCache-specific build settings
@@ -34,21 +39,43 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
private let mode: Mode
private let repoRoot: URL
private let fakeSrcRoot: URL
private let sdksExclude: [String]
private let options: BuildSettingsIntegrateAppenderOption
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL) {
init(
mode: Mode,
repoRoot: URL,
fakeSrcRoot: URL,
sdksExclude: [String],
options: BuildSettingsIntegrateAppenderOption
) {
self.mode = mode
self.repoRoot = repoRoot
self.fakeSrcRoot = fakeSrcRoot
self.sdksExclude = sdksExclude
self.options = options
}
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 )
if options.contains(.disableSwiftDriverIntegration) {
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 +87,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"
}
}
}
@@ -27,14 +27,14 @@ struct IntegrateContext {
let configOverride: URL
let fakeSrcRoot: URL
let output: URL?
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption
}
extension IntegrateContext {
init(
input: String,
repoRootPath: String,
config: XCRemoteCacheConfig,
mode: Mode,
configOverridePath: String,
env: [String: String],
binariesDir: URL,
fakeSrcRoot: String,
@@ -42,19 +42,29 @@ extension IntegrateContext {
) throws {
projectPath = URL(fileURLWithPath: input)
let srcRoot = projectPath.deletingLastPathComponent()
repoRoot = URL(fileURLWithPath: repoRootPath, relativeTo: srcRoot)
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: srcRoot)
self.mode = mode
configOverride = URL(fileURLWithPath: configOverridePath, relativeTo: srcRoot)
configOverride = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: srcRoot)
output = outputPath.flatMap(URL.init(fileURLWithPath:))
self.fakeSrcRoot = URL(fileURLWithPath: fakeSrcRoot)
var swiftcBinaryName = "swiftc"
var buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = []
// Keep the legacy behaviour (supported in Xcode 14 and lower)
if !config.enableSwiftDriverIntegration {
buildSettingsAppenderOptions.insert(.disableSwiftDriverIntegration)
swiftcBinaryName = "xcswiftc"
}
binaries = XCRCBinariesPaths(
prepare: binariesDir.appendingPathComponent("xcprepare"),
cc: binariesDir.appendingPathComponent("xccc"),
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
swiftc: binariesDir.appendingPathComponent(swiftcBinaryName),
libtool: binariesDir.appendingPathComponent("xclibtool"),
lipo: binariesDir.appendingPathComponent("xclipo"),
ld: binariesDir.appendingPathComponent("xcld"),
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
)
self.buildSettingsAppenderOptions = buildSettingsAppenderOptions
}
}
@@ -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
}
@@ -79,9 +82,8 @@ public class XCIntegrate {
let context = try IntegrateContext(
input: projectPath,
repoRootPath: config.repoRoot,
config: config,
mode: mode,
configOverridePath: config.extraConfigurationFile,
env: env,
binariesDir: binariesDir,
fakeSrcRoot: fakeSrcRoot,
@@ -98,7 +100,9 @@ public class XCIntegrate {
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
mode: context.mode,
repoRoot: context.repoRoot,
fakeSrcRoot: context.fakeSrcRoot
fakeSrcRoot: context.fakeSrcRoot,
sdksExclude: sdksExclude.integrateArrayArguments,
options: context.buildSettingsAppenderOptions
)
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
}
}
@@ -87,6 +87,7 @@ public class XCPrepare {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.downloadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -47,12 +47,17 @@ public class XCPrepareMark {
let xcodeVersion: String
do {
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
context = try PrepareMarkContext(config)
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?
@@ -69,6 +74,7 @@ public class XCPrepareMark {
let networkClient = NetworkClientImpl(
session: sessionFactory.build(),
retries: config.uploadRetries,
retryDelay: config.retryDelay,
fileManager: fileManager,
awsV4Signature: awsV4Signature
)
@@ -76,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
@@ -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 {
@@ -0,0 +1,244 @@
// 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
enum SwiftFrontendArgInputError: Error, Equatable {
// swift-frontend should either be compling or emiting a module
case bothCompilationAndEmitAction
// no .swift files have been passed as input files
case noCompilationInputs
// no -primary-file .swift files have been passed as input files
case noPrimaryFileCompilationInputs
// number of -emit-dependencies-path doesn't match compilation inputs
case dependenciesOuputCountDoesntMatch(expected: Int, parsed: Int)
// number of -serialize-diagnostics-path doesn't match compilation inputs
case diagnosticsOuputCountDoesntMatch(expected: Int, parsed: Int)
// number of -o doesn't match compilation inputs
case outputsOuputCountDoesntMatch(expected: Int, parsed: Int)
// number of -o for emit-module can be only 1
case emitModulOuputCountIsNot1(parsed: Int)
// number of -emit-dependencies-path for emit-module can be 0 or 1 (generate or not)
case emitModuleDependenciesOuputCountIsHigherThan1(parsed: Int)
// number of -serialize-diagnostics-path for emit-module can be 0 or 1 (generate or not)
case emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: Int)
// emit-module requires -emit-objc-header-path
case emitModuleMissingObjcHeaderPath
// -target is required
case emitMissingTarget
// -moduleName is required
case emitMissingModuleName
}
public struct SwiftFrontendArgInput {
let compile: Bool
let emitModule: Bool
let objcHeaderOutput: String?
let moduleName: String?
let target: String?
let primaryInputPaths: [String]
let inputPaths: [String]
var outputPaths: [String]
var dependenciesPaths: [String]
// Extra params
// Diagnostics are not supported yet in the XCRemoteCache (cached artifacts assumes no warnings)
var diagnosticsPaths: [String]
// Unsed for now:
// .swiftsourceinfo and .swiftdoc will be placed next to the .swiftmodule
let sourceInfoPath: String?
let docPath: String?
// Passed as -supplementary-output-file-map
let supplementaryOutputFileMap: String?
/// Manual initializer implementation required to be public
public init(
compile: Bool,
emitModule: Bool,
objcHeaderOutput: String?,
moduleName: String?,
target: String?,
primaryInputPaths: [String],
inputPaths: [String],
outputPaths: [String],
dependenciesPaths: [String],
diagnosticsPaths: [String],
sourceInfoPath: String?,
docPath: String?,
supplementaryOutputFileMap: String?
) {
self.compile = compile
self.emitModule = emitModule
self.objcHeaderOutput = objcHeaderOutput
self.moduleName = moduleName
self.target = target
self.primaryInputPaths = primaryInputPaths
self.inputPaths = inputPaths
self.outputPaths = outputPaths
self.dependenciesPaths = dependenciesPaths
self.diagnosticsPaths = diagnosticsPaths
self.sourceInfoPath = sourceInfoPath
self.docPath = docPath
self.supplementaryOutputFileMap = supplementaryOutputFileMap
}
private func generateForCompilation(
config: XCRemoteCacheConfig,
target: String,
moduleName: String
) throws -> SwiftcContext {
let primaryInputsCount = primaryInputPaths.count
guard primaryInputsCount > 0 else {
throw SwiftFrontendArgInputError.noPrimaryFileCompilationInputs
}
guard [primaryInputsCount, 0].contains(dependenciesPaths.count) else {
throw SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(
expected: primaryInputsCount,
parsed: dependenciesPaths.count
)
}
guard [primaryInputsCount, 0].contains(diagnosticsPaths.count) else {
throw SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(
expected: primaryInputsCount,
parsed: diagnosticsPaths.count
)
}
guard outputPaths.count == primaryInputsCount else {
throw SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(
expected: primaryInputsCount,
parsed: outputPaths.count
)
}
let primaryInputFilesURLs: [URL] = primaryInputPaths.map(URL.init(fileURLWithPath:))
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
compileFilesScope: .subset(primaryInputFilesURLs),
emitModule: nil
)
let compilationFilesInputs = buildCompilationFilesInputs(
primaryInputsCount: primaryInputsCount,
primaryInputFilesURLs: primaryInputFilesURLs
)
return try .init(
config: config,
moduleName: moduleName,
steps: steps,
inputs: compilationFilesInputs,
target: target,
compilationFiles: .list(inputPaths),
exampleWorkspaceFilePath: outputPaths[0]
)
}
private func buildCompilationFilesInputs(
primaryInputsCount: Int,
primaryInputFilesURLs: [URL]
) -> SwiftcContext.CompilationFilesInputs {
if let compimentaryFileMa = supplementaryOutputFileMap {
return .supplementaryFileMap(compimentaryFileMa)
} else {
return .map((0..<primaryInputsCount).reduce(
[String: SwiftFileCompilationInfo]()
) { prev, i in
var new = prev
new[primaryInputPaths[i]] = SwiftFileCompilationInfo(
file: primaryInputFilesURLs[i],
dependencies: dependenciesPaths.get(i).map(URL.init(fileURLWithPath:)),
object: outputPaths.get(i).map(URL.init(fileURLWithPath:)),
// for now - swift-dependencies are not requested in the driver compilation mode
swiftDependencies: nil
)
return new
})
}
}
private func generateForEmitModule(
config: XCRemoteCacheConfig,
target: String,
moduleName: String
) throws -> SwiftcContext {
guard outputPaths.count == 1 else {
throw SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: outputPaths.count)
}
guard let objcHeaderOutput = objcHeaderOutput else {
throw SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath
}
guard diagnosticsPaths.count <= 1 else {
throw SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(
parsed: diagnosticsPaths.count
)
}
guard dependenciesPaths.count <= 1 else {
throw SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(
parsed: dependenciesPaths.count
)
}
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
compileFilesScope: .none,
emitModule: SwiftcContext.SwiftcStepEmitModule(
objcHeaderOutput: URL(fileURLWithPath: objcHeaderOutput),
modulePathOutput: URL(fileURLWithPath: outputPaths[0]),
dependencies: dependenciesPaths.first.map(URL.init(fileURLWithPath:))
)
)
return try .init(
config: config,
moduleName: moduleName,
steps: steps,
inputs: .map([:]),
target: target,
compilationFiles: .list(inputPaths),
exampleWorkspaceFilePath: objcHeaderOutput
)
}
func generateSwiftcContext(config: XCRemoteCacheConfig) throws -> SwiftcContext {
guard compile != emitModule else {
throw SwiftFrontendArgInputError.bothCompilationAndEmitAction
}
let inputPathsCount = inputPaths.count
guard inputPathsCount > 0 else {
throw SwiftFrontendArgInputError.noCompilationInputs
}
guard let target = target else {
throw SwiftFrontendArgInputError.emitMissingTarget
}
guard let moduleName = moduleName else {
throw SwiftFrontendArgInputError.emitMissingModuleName
}
if compile {
return try generateForCompilation(
config: config,
target: target,
moduleName: moduleName
)
} else {
return try generateForEmitModule(
config: config,
target: target,
moduleName: moduleName
)
}
}
}
@@ -0,0 +1,42 @@
// 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
struct SwiftFrontendContext {
/// File lock used for synchronizing multiple invocations
let invocationLockFile: URL
}
extension SwiftFrontendContext {
init(_ swiftcContext: SwiftcContext, env: [String: String]) throws {
/// The LLBUILD_BUILD_ID ENV that describes the entire (incl. parent's swiftc) bui;d
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
invocationLockFile = Self.self.buildLlbuildIdSharedLockUrl(
llbuildId: llbuildId,
tmpDir: swiftcContext.tempDir
)
}
/// Generate the filename to be used to synchronize multiple swift-frontend invocations
/// The same file is used in prebuild, xcswift-frontend and postbuild (to clean it up)
static func buildLlbuildIdSharedLockUrl(llbuildId: String, tmpDir: URL) -> URL {
return tmpDir.appendingPathComponent(llbuildId).appendingPathExtension("lock")
}
}
@@ -0,0 +1,136 @@
// 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
/// Manages the `swift-frontend` logic
protocol SwiftFrontendOrchestrator {
/// Executes the critical section according to the required order
/// - Parameter criticalSection: the block that should be synchronized
func run(criticalSection: () -> Void ) throws
}
/// The default orchestrator that manages the order or swift-frontend invocations
/// For emit-module (the "first" process) action, it locks a shared file between all swift-frontend invocations,
/// verifies that the mocking can be done and continues the mocking/fall-backing along the lock release
/// For the compilation action, tries to acquire a lock and waits until the "emit-module" makes a decision
/// if the compilation should be skipped and a "mocking" should used instead
class CommonSwiftFrontendOrchestrator {
/// Content saved to the shared file
/// Safe to use forced unwrapping
private static let emitModuleContent = "done".data(using: .utf8)!
enum Action {
case emitModule
case compile
}
private let mode: SwiftcContext.SwiftcMode
private let action: Action
private let lockAccessor: ExclusiveFileAccessor
private let maxLockTimeout: TimeInterval
init(
mode: SwiftcContext.SwiftcMode,
action: Action,
lockAccessor: ExclusiveFileAccessor,
maxLockTimeout: TimeInterval
) {
self.mode = mode
self.action = action
self.lockAccessor = lockAccessor
self.maxLockTimeout = maxLockTimeout
}
func run(criticalSection: () throws -> Void) throws {
guard case .consumer(commit: .available) = mode else {
// no need to lock anything - just allow fallbacking to the `swiftc or swift-frontend`
// for a producer or a consumer where RC is disabled (we have already caught the
// cache miss)
try criticalSection()
return
}
try executeMockAttemp(criticalSection: criticalSection)
}
private func executeMockAttemp(criticalSection: () throws -> Void) throws {
switch action {
case .emitModule:
try validateEmitModuleStep(criticalSection: criticalSection)
case .compile:
try waitForEmitModuleLock(criticalSection: criticalSection)
}
}
/// For emit-module, wrap the critical section with the shared lock so other processes (compilation)
/// have to wait until emit-module finishes
/// Once the emit-module is done, the "magical" string is saved to the file and the lock is released
///
/// Note: The design of wrapping the entire "emit-module" has a small performance downside if inside
/// the critical section, the code realizes that remote cache cannot be used
/// (in practice - a new file has been added)
/// None of compilation process (so with '-c' args) can continue until the entire emit-module logic finishes
/// Because it is expected to happen not that often and emit-module is usually quite fast, this makes the
/// implementation way simpler. If we ever want to optimize it, we should release the lock as early
/// as we know, the remote cache cannot be used. Then all other compilation process (-c) can run
/// in parallel with emit-module
private func validateEmitModuleStep(criticalSection: () throws -> Void) throws {
debugLog("starting the emit-module step: locking")
try lockAccessor.exclusiveAccess { handle in
debugLog("starting the emit-module step: locked")
// writing to the file content proactively - incase the critical section never returns
// (in case of a fallback to the local compilation), all awaiting swift-frontend processes
// will be immediately unblocked
handle.write(Self.self.emitModuleContent)
try criticalSection()
debugLog("lock file emit-module criticial end")
}
}
/// Locks a shared file in a loop until its content is non-empty - meaning the "parent" emit-module
/// has already finished
private func waitForEmitModuleLock(criticalSection: () throws -> Void) throws {
// emit-module process should really quickly obtain a lock (it is always invoked
// by Xcode as a first process)
var executed = false
let startingDate = Date()
while !executed {
debugLog("lock file compilation trying to acquire a lock ....")
try lockAccessor.exclusiveAccess { handle in
if !handle.availableData.isEmpty {
// the file is not empty so the emit-module process is done with the "check"
debugLog("swift-frontend lock file is unlocked for compilation")
try criticalSection()
executed = true
} else {
debugLog("swift-frontend lock file is not ready for compilation")
}
}
// When a max locking time is achieved, execute anyway
if !executed && Date().timeIntervalSince(startingDate) > self.maxLockTimeout {
errorLog("""
Executing command \(action) without lock synchronization. That may be cause by the\
crashed or extremely long emit-module. Contact XCRemoteCache authors about this error.
""")
try criticalSection()
executed = true
}
}
}
}
@@ -0,0 +1,80 @@
// 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
public class XCSwiftFrontend: XCSwiftAbstract<SwiftFrontendArgInput> {
// don't lock individual compilation invocations for more than 10s
private static let MaxLockingTimeout: TimeInterval = 10
private let env: [String: String]
public init(
command: String,
inputArgs: SwiftFrontendArgInput,
env: [String: String],
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
touchFactory: @escaping (URL, FileManager) -> Touch
) throws {
self.env = env
super.init(
command: command,
inputArgs: inputArgs,
dependenciesWriter: dependenciesWriter,
touchFactory: touchFactory
)
}
override func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: SwiftcContext
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
context = try SwiftcContext(config: config, input: inputArgs)
// do not cache this context, as it is subject to change when
// the emit-module finds that the cached artifact cannot be used
return (config, context)
}
override public func run() throws {
do {
let (_, context) = try buildContext()
let frontendContext = try SwiftFrontendContext(context, env: env)
let sharedLock = ExclusiveFile(frontendContext.invocationLockFile, mode: .override)
let action: CommonSwiftFrontendOrchestrator.Action = inputArgs.emitModule ? .emitModule : .compile
let swiftFrontendOrchestrator = CommonSwiftFrontendOrchestrator(
mode: context.mode,
action: action,
lockAccessor: sharedLock,
maxLockTimeout: Self.self.MaxLockingTimeout
)
try swiftFrontendOrchestrator.run(criticalSection: super.run)
} catch {
// Splitting into 2 invocations as os_log truncates a massage
defaultLog("Cannot correctly orchestrate the \(command) with params \(inputArgs)")
defaultLog("Cannot correctly orchestrate error: \(error)")
throw error
}
}
}
@@ -0,0 +1,38 @@
// 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
/// Decides if an input to the compilation step should allow reusing the cached artifact
protocol AllowedInputDeterminer {
/// Decides if the input file is allowed to be compiled, even not specified in the dependency list
func allowedNonDependencyInput(file: URL) -> Bool
}
class FilenameBasedAllowedInputDeterminer: AllowedInputDeterminer {
private let filenames: [String]
init(_ filenames: [String]) {
self.filenames = filenames
}
func allowedNonDependencyInput(file: URL) -> Bool {
return filenames.contains(file.lastPathComponent)
}
}
@@ -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 {
@@ -0,0 +1,38 @@
// 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
/// Products generator that doesn't create any swiftmodule. It is used in the compilation swift-frontend mocking, where
/// only individual .o files are created and not .swiftmodule of -Swift.h
/// (which is part of swift-frontend -emit-module invocation)
class NoopSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> SwiftcProductsGeneratorOutput {
infoLog("""
Invoking module generation from NoopSwiftcProductsGenerator does nothing. \
It might be a side-effect of a plugin asking to generate a module.
""")
// NoopSwiftcProductsGenerator is intended only for the swift-frontend
let trivialURL = URL(fileURLWithPath: "/non-existing")
return SwiftcProductsGeneratorOutput(swiftmoduleDir: trivialURL, objcHeaderFile: trivialURL)
}
}
@@ -0,0 +1,46 @@
// 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 StaticSwiftcInputReader: SwiftcInputReader {
private let moduleDependencies: URL?
private let swiftDependencies: URL?
private let compilationFiles: [SwiftFileCompilationInfo]
init(
moduleDependencies: URL?,
swiftDependencies: URL?,
compilationFiles: [SwiftFileCompilationInfo]
) {
self.moduleDependencies = moduleDependencies
self.swiftDependencies = swiftDependencies
self.compilationFiles = compilationFiles
}
func read() throws -> SwiftCompilationInfo {
return .init(
info: .init(
dependencies: moduleDependencies,
swiftDependencies: swiftDependencies
),
files: compilationFiles
)
}
}
@@ -57,6 +57,7 @@ class Swiftc: SwiftcProtocol {
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
private let touchFactory: (URL, FileManager) -> Touch
private let plugins: [SwiftcProductGenerationPlugin]
private let allowedInputDeterminer: AllowedInputDeterminer
init(
inputFileListReader: ListReader,
@@ -70,7 +71,8 @@ class Swiftc: SwiftcProtocol {
fileManager: FileManager,
dependenciesWriterFactory: @escaping (URL, FileManager) -> DependenciesWriter,
touchFactory: @escaping (URL, FileManager) -> Touch,
plugins: [SwiftcProductGenerationPlugin]
plugins: [SwiftcProductGenerationPlugin],
allowedInputDeterminer: AllowedInputDeterminer
) {
self.inputFileListReader = inputFileListReader
self.markerReader = markerReader
@@ -84,6 +86,7 @@ class Swiftc: SwiftcProtocol {
self.dependenciesWriterFactory = dependenciesWriterFactory
self.touchFactory = touchFactory
self.plugins = plugins
self.allowedInputDeterminer = allowedInputDeterminer
}
// swiftlint:disable:next function_body_length
@@ -96,13 +99,17 @@ class Swiftc: SwiftcProtocol {
let inputFilesInputs = try inputFileListReader.listFilesURLs()
let markerAllowedFiles = try markerReader.listFilesURLs()
let allDependencies = Set(markerAllowedFiles + inputFilesInputs)
let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory(
dependencies: markerAllowedFiles,
dependencies: Array(allDependencies),
fileManager: fileManager,
writerFactory: dependenciesWriterFactory
)
// Verify all input files to be present in a marker fileList
let disallowedInputs = try inputFilesInputs.filter { try !allowedFilesListScanner.contains($0) }
let disallowedInputs = try inputFilesInputs.filter { file in
try !allowedFilesListScanner.contains(file) &&
!allowedInputDeterminer.allowedNonDependencyInput(file: file)
}
if !disallowedInputs.isEmpty {
// New file (disallowedFile) added without modifying the rest of the feature. Fallback to swiftc and
@@ -132,7 +139,7 @@ class Swiftc: SwiftcProtocol {
// Read swiftmodule location from XCRemoteCache
// arbitrary format swiftmodule/${arch}/${moduleName}.swift{module|doc|sourceinfo}
let moduleName = context.modulePathOutput.deletingPathExtension().lastPathComponent
let moduleName = context.moduleName
let allCompilations = try inputFilesReader.read()
let artifactSwiftmoduleDir = artifactLocation
.appendingPathComponent("swiftmodule")
@@ -145,20 +152,24 @@ class Swiftc: SwiftcProtocol {
}
)
// Build -Swift.h location from XCRemoteCache arbitrary format include/${arch}/${target}-Swift.h
let artifactSwiftModuleObjCDir = artifactLocation
.appendingPathComponent("include")
.appendingPathComponent(context.arch)
.appendingPathComponent(context.moduleName)
// Move cached xxxx-Swift.h to the location passed in arglist
// Alternatively, artifactSwiftModuleObjCFile could be built as a first .h file in artifactSwiftModuleObjCDir
let artifactSwiftModuleObjCFile = artifactSwiftModuleObjCDir
.appendingPathComponent(context.objcHeaderOutput.lastPathComponent)
// emit module (if requested)
if let emitModule = context.steps.emitModule {
// Build -Swift.h location from XCRemoteCache arbitrary format include/${arch}/${target}-Swift.h
let artifactSwiftModuleObjCDir = artifactLocation
.appendingPathComponent("include")
.appendingPathComponent(context.arch)
.appendingPathComponent(context.moduleName)
// Move cached xxxx-Swift.h to the location passed in arglist
// Alternatively, artifactSwiftModuleObjCFile could be built as a first .h
// file in artifactSwiftModuleObjCDir
let artifactSwiftModuleObjCFile = artifactSwiftModuleObjCDir
.appendingPathComponent(emitModule.objcHeaderOutput.lastPathComponent)
_ = try productsGenerator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
_ = try productsGenerator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
}
try plugins.forEach {
try $0.generate(for: allCompilations)
@@ -176,8 +187,10 @@ class Swiftc: SwiftcProtocol {
try cachedDependenciesWriterFactory.generate(output: individualDeps)
}
}
// Save .d for the entire module
try cachedDependenciesWriterFactory.generate(output: allCompilations.info.swiftDependencies)
// Save .d for the entire module (might not be required in the `swift-frontend -c` mode)
if let swiftDependencies = allCompilations.info.swiftDependencies {
try cachedDependenciesWriterFactory.generate(output: swiftDependencies)
}
// Generate .d file with all deps in the "-master.d" (e.g. for WMO)
if let wmoDeps = allCompilations.info.dependencies {
try cachedDependenciesWriterFactory.generate(output: wmoDeps)
@@ -20,6 +20,53 @@
import Foundation
public struct SwiftcContext {
/// Describes the action if the module emit should happen
/// that generates .swiftmodule and/or -Swift.h
public struct SwiftcStepEmitModule: Equatable {
// where the -Swift.h should be placed
let objcHeaderOutput: URL
// where should the .swiftmodule be placed
let modulePathOutput: URL
// might be passed as an explicit argument in the swiftc
// -emit-dependencies-path
let dependencies: URL?
}
/// Which files (from the list of all files in the module)
/// should be compiled in this process
public enum SwiftcStepCompileFilesScope: Equatable {
/// used if only emit module should be done
case none
case all
case subset([URL])
}
/// Describes which steps should be done as a part of this process
public struct SwiftcSteps: Equatable {
/// which files should be compiled
let compileFilesScope: SwiftcStepCompileFilesScope
/// if a module should be generated
let emitModule: SwiftcStepEmitModule?
}
/// Defines how a list of input files (*.swift) is passed to the invocation
public enum CompilationFilesSource: Equatable {
/// defined in a separate file (via @/.../*.SwiftFileList)
case fileList(String)
/// explicitly passed a list of files
case list([String])
}
/// Defines how a list of output files (*.d, *.o etc.) is passed to the invocation
public enum CompilationFilesInputs: Equatable {
/// defined in a separate file (via -output-file-map)
case fileMap(String)
/// defined in a separate file (via -supplementary-output-file-map)
case supplementaryFileMap(String)
/// explicitly passed in the invocation
case map([String: SwiftFileCompilationInfo])
}
enum SwiftcMode: Equatable {
case producer
/// Commit sha of the commit to use during remote cache
@@ -28,14 +75,13 @@ public struct SwiftcContext {
case producerFast
}
let objcHeaderOutput: URL
let steps: SwiftcSteps
let moduleName: String
let modulePathOutput: URL
/// File that defines output files locations (.d, .swiftmodule etc.)
let filemap: URL
/// A source that defines output files locations (.d, .swiftmodule etc.)
let inputs: CompilationFilesInputs
let target: String
/// File that contains input files for the swift module compilation
let fileList: URL
/// A source that contains all input files for the swift module compilation
let compilationFiles: CompilationFilesSource
let tempDir: URL
let arch: String
let prebuildDependenciesPath: String
@@ -43,29 +89,29 @@ public struct SwiftcContext {
/// File that stores all compilation invocation arguments
let invocationHistoryFile: URL
public init(
config: XCRemoteCacheConfig,
objcHeaderOutput: String,
moduleName: String,
modulePathOutput: String,
filemap: String,
steps: SwiftcSteps,
inputs: CompilationFilesInputs,
target: String,
fileList: String
compilationFiles: CompilationFilesSource,
/// any workspace file path - all other intermediate files for this compilation
/// are placed next to it. This path is used to infer the arch and TARGET_TEMP_DIR
exampleWorkspaceFilePath: String
) throws {
self.objcHeaderOutput = URL(fileURLWithPath: objcHeaderOutput)
self.moduleName = moduleName
self.modulePathOutput = URL(fileURLWithPath: modulePathOutput)
self.filemap = URL(fileURLWithPath: filemap)
self.steps = steps
self.inputs = inputs
self.target = target
self.fileList = URL(fileURLWithPath: fileList)
// modulePathOutput is place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/$TARGET_NAME.swiftmodule
self.compilationFiles = compilationFiles
// exampleWorkspaceFilePath has a format $TARGET_TEMP_DIR/Objects-normal/$ARCH/some.file
// That may be subject to change for other Xcode versions
tempDir = URL(fileURLWithPath: modulePathOutput)
tempDir = URL(fileURLWithPath: exampleWorkspaceFilePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
arch = URL(fileURLWithPath: modulePathOutput).deletingLastPathComponent().lastPathComponent
arch = URL(fileURLWithPath: exampleWorkspaceFilePath).deletingLastPathComponent().lastPathComponent
let srcRoot: URL = URL(fileURLWithPath: config.sourceRoot)
let remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: srcRoot)
@@ -92,14 +138,32 @@ public struct SwiftcContext {
config: XCRemoteCacheConfig,
input: SwiftcArgInput
) throws {
let steps = SwiftcSteps(
compileFilesScope: .all,
emitModule: SwiftcStepEmitModule(
objcHeaderOutput: URL(fileURLWithPath: (input.objcHeaderOutput)),
modulePathOutput: URL(fileURLWithPath: input.modulePathOutput),
// in `swiftc`, .d dependencies are pass in the output filemap
dependencies: nil
)
)
let inputs = CompilationFilesInputs.fileMap(input.filemap)
let compilationFiles = CompilationFilesSource.fileList(input.fileList)
try self.init(
config: config,
objcHeaderOutput: input.objcHeaderOutput,
moduleName: input.moduleName,
modulePathOutput: input.modulePathOutput,
filemap: input.filemap,
steps: steps,
inputs: inputs,
target: input.target,
fileList: input.fileList
compilationFiles: compilationFiles,
exampleWorkspaceFilePath: input.modulePathOutput
)
}
init(
config: XCRemoteCacheConfig,
input: SwiftFrontendArgInput
) throws {
self = try input.generateSwiftcContext(config: config)
}
}
@@ -18,11 +18,16 @@
// under the License.
import Foundation
import Yams
/// Errors with reading swiftc inputs
enum SwiftcInputReaderError: Error {
case readingFailed
case invalidFormat
/// The file is not in the yaml format
case invalidYamlFormat
/// The yaml string contains illegal characters
case invalidYamlString
case missingField(String)
}
@@ -45,10 +50,11 @@ struct SwiftCompilationInfo: Encodable, Equatable {
struct SwiftModuleCompilationInfo: Encodable, Equatable {
// not present for incremental builds
let dependencies: URL?
let swiftDependencies: URL
// might be nil for the swift-frontend '-c' invocation
let swiftDependencies: URL?
}
struct SwiftFileCompilationInfo: Encodable, Equatable {
public struct SwiftFileCompilationInfo: Encodable, Hashable {
let file: URL
// not present for WMO builds
let dependencies: URL?
@@ -60,11 +66,18 @@ struct SwiftFileCompilationInfo: Encodable, Equatable {
class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
enum Format {
case json
case yaml
}
private let file: URL
private let fileFormat: Format
private let fileManager: FileManager
init(_ file: URL, fileManager: FileManager) {
init(_ file: URL, fileFormat: Format, fileManager: FileManager) {
self.file = file
self.fileFormat = fileFormat
self.fileManager = fileManager
}
@@ -72,7 +85,7 @@ class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
guard let content = fileManager.contents(atPath: file.path) else {
throw SwiftcInputReaderError.readingFailed
}
guard let representation = try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any] else {
guard let representation = try decodeFile(content: content) else {
throw SwiftcInputReaderError.invalidFormat
}
return try SwiftCompilationInfo(from: representation)
@@ -82,11 +95,23 @@ class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
let data = try JSONSerialization.data(withJSONObject: info.dump(), options: [.prettyPrinted])
fileManager.createFile(atPath: file.path, contents: data, attributes: nil)
}
private func decodeFile(content: Data) throws -> [String: Any]? {
switch fileFormat {
case .json:
return try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any]
case .yaml:
guard let stringContent = String(data: content, encoding: .utf8) else {
throw SwiftcInputReaderError.invalidYamlString
}
return try Yams.load(yaml: stringContent) as? [String: Any]
}
}
}
extension SwiftCompilationInfo {
init(from object: [String: Any]) throws {
info = try SwiftModuleCompilationInfo(from: object[""])
info = try SwiftModuleCompilationInfo(from: object["", default: [:]])
files = try object.reduce([]) { prev, new in
let (key, value) = new
if key.isEmpty {
@@ -111,14 +136,14 @@ extension SwiftModuleCompilationInfo {
guard let dict = object as? [String: String] else {
throw SwiftcInputReaderError.invalidFormat
}
swiftDependencies = try dict.readURL(key: "swift-dependencies")
swiftDependencies = dict.readURL(key: "swift-dependencies")
dependencies = dict.readURL(key: "dependencies")
}
func dump() -> [String: String] {
return [
"dependencies": dependencies?.path,
"swift-dependencies": swiftDependencies.path,
"swift-dependencies": swiftDependencies?.path,
].compactMapValues { $0 }
}
}
@@ -27,8 +27,10 @@ class SwiftcOrchestrator {
private let mode: SwiftcContext.SwiftcMode
// swiftc command that should be called to generate artifacts
private let swiftcCommand: String
private let objcHeaderOutput: URL
private let moduleOutput: URL
// Might be nil if invoking from frontend compilation: `swift-frontend -c`
private let objcHeaderOutput: URL?
// Might be nil if invoking from frontend compilation: `swift-frontend -c`
private let moduleOutput: URL?
private let arch: String
private let artifactBuilder: ArtifactSwiftProductsBuilder
private let shellOut: ShellOut
@@ -39,8 +41,8 @@ class SwiftcOrchestrator {
mode: SwiftcContext.SwiftcMode,
swiftc: SwiftcProtocol,
swiftcCommand: String,
objcHeaderOutput: URL,
moduleOutput: URL,
objcHeaderOutput: URL?,
moduleOutput: URL?,
arch: String,
artifactBuilder: ArtifactSwiftProductsBuilder,
producerFallbackCommandProcessors: [ShellCommandsProcessor],
@@ -128,10 +130,14 @@ class SwiftcOrchestrator {
try processor.applyArgsRewrite(args)
}
try fallbackToDefaultAndWait(command: swiftcCommand, args: swiftcArgs)
// move generated .h to the location where artifact creator expects it
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
// move generated .swiftmodule to the location where artifact creator expects it
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
if let objcHeaderOutput = objcHeaderOutput {
// move generated .h to the location where artifact creator expects it
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
}
if let moduleOutput = moduleOutput {
// move generated .swiftmodule to the location where artifact creator expects it
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
}
try producerFallbackCommandProcessors.forEach {
try $0.postCommandProcessing()
@@ -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
)
}
}
@@ -45,15 +45,15 @@ public struct SwiftcArgInput {
}
}
public class XCSwiftc {
private let command: String
private let inputArgs: SwiftcArgInput
public class XCSwiftAbstract<InputArgs> {
let command: String
let inputArgs: InputArgs
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
private let touchFactory: (URL, FileManager) -> Touch
public init(
command: String,
inputArgs: SwiftcArgInput,
inputArgs: InputArgs,
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
touchFactory: @escaping (URL, FileManager) -> Touch
) {
@@ -63,27 +63,58 @@ public class XCSwiftc {
self.touchFactory = touchFactory
}
func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
fatalError("Need to override in \(Self.self)")
}
// swiftlint:disable:next function_body_length
public func run() {
public func run() throws {
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: SwiftcContext
do {
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
context = try SwiftcContext(config: config, input: inputArgs)
} catch {
exit(1, "FATAL: Swiftc initialization failed with error: \(error)")
}
let (config, context) = try buildContext()
let swiftcCommand = config.swiftcCommand
let markerURL = context.tempDir.appendingPathComponent(config.modeMarkerPath)
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
let markerWriter = FileMarkerWriter(markerURL, fileAccessor: fileManager)
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
let artifactOrganizer = ZipArtifactOrganizer(targetTempDir: context.tempDir, fileManager: fileManager)
let inputReader: SwiftcInputReader
switch context.inputs {
case .fileMap(let path):
inputReader = SwiftcFilemapInputEditor(
URL(fileURLWithPath: path),
fileFormat: .json,
fileManager: fileManager
)
case .supplementaryFileMap(let path):
// Supplementary file map is endoded in the yaml file (contraty to
// the standard filemap, which is in json)
inputReader = SwiftcFilemapInputEditor(
URL(fileURLWithPath: path),
fileFormat: .yaml,
fileManager: fileManager
)
case .map(let map):
// static - passed via the arguments list
inputReader = StaticSwiftcInputReader(
moduleDependencies: context.steps.emitModule?.dependencies,
// with Xcode 14, inputs via cmd are only used for compilations
swiftDependencies: nil,
compilationFiles: Array(map.values)
)
}
let fileListReader: ListReader
switch context.compilationFiles {
case .fileList(let path):
fileListReader = FileListEditor(URL(fileURLWithPath: path), fileManager: fileManager)
case .list(let paths):
fileListReader = StaticFileListReader(list: paths.map(URL.init(fileURLWithPath:)))
}
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(
@@ -96,11 +127,20 @@ public class XCSwiftc {
moduleName: context.moduleName,
fileManager: fileManager
)
let productsGenerator = DiskSwiftcProductsGenerator(
modulePathOutput: context.modulePathOutput,
objcHeaderOutput: context.objcHeaderOutput,
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
)
let productsGenerator: SwiftcProductsGenerator
if let emitModule = context.steps.emitModule {
productsGenerator = DiskSwiftcProductsGenerator(
modulePathOutput: emitModule.modulePathOutput,
objcHeaderOutput: emitModule.objcHeaderOutput,
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
)
} else {
// If the module was not requested for this proces (compiling files only)
// do nothing, when someone (e.g. a plugin) asks for the products generation
// This generation will happend in a separate process, where the module
// generation is requested
productsGenerator = NoopSwiftcProductsGenerator()
}
let allInvocationsStorage = ExistingFileStorage(
storageFile: context.invocationHistoryFile,
command: swiftcCommand
@@ -112,9 +152,12 @@ public class XCSwiftc {
retrieveIgnoredCommands: [swiftcCommand]
)
let shellOut = ProcessShellOut()
// Always allow an input file from the actool generation step
// As of Xcode15, the filename is confirmed to be static
let allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["GeneratedAssetSymbols.swift"])
let swiftc = Swiftc(
inputFileListReader: fileListEditor,
inputFileListReader: fileListReader,
markerReader: markerReader,
allowedFilesListScanner: allowedFilesListScanner,
artifactOrganizer: artifactOrganizer,
@@ -125,24 +168,35 @@ public class XCSwiftc {
fileManager: fileManager,
dependenciesWriterFactory: dependenciesWriterFactory,
touchFactory: touchFactory,
plugins: []
plugins: [],
allowedInputDeterminer: allowedInputDeterminer
)
let orchestrator = SwiftcOrchestrator(
mode: context.mode,
swiftc: swiftc,
swiftcCommand: swiftcCommand,
objcHeaderOutput: context.objcHeaderOutput,
moduleOutput: context.modulePathOutput,
objcHeaderOutput: context.steps.emitModule?.objcHeaderOutput,
moduleOutput: context.steps.emitModule?.modulePathOutput,
arch: context.arch,
artifactBuilder: artifactBuilder,
producerFallbackCommandProcessors: [],
invocationStorage: invocationStorage,
shellOut: shellOut
)
do {
try orchestrator.run()
} catch {
exit(1, "Swiftc failed with error: \(error)")
}
try orchestrator.run()
}
}
public class XCSwiftc: XCSwiftAbstract<SwiftcArgInput> {
override func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
let fileReader = FileManager.default
let config: XCRemoteCacheConfig
let context: SwiftcContext
let srcRoot: URL = URL(fileURLWithPath: fileReader.currentDirectoryPath)
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileReader)
.readConfiguration()
context = try SwiftcContext(config: config, input: inputArgs)
return (config, context)
}
}
@@ -17,6 +17,8 @@
// specific language governing permissions and limitations
// under the License.
// swiftlint:disable file_length
import Foundation
import Yams
@@ -57,6 +59,8 @@ public struct XCRemoteCacheConfig: Encodable {
var clangCommand: String = "clang"
/// Command for a standard Swift compilation (swiftc)
var swiftcCommand: String = "swiftc"
/// Command for a standard Swift frontend compilation (swift-frontend)
var swiftFrontendCommand: String = "swift-frontend"
/// Path of the primary repository that produces cache artifacts
var primaryRepo: String = ""
/// Main (primary) branch that produces cache artifacts (default to 'master')
@@ -83,6 +87,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 +115,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
@@ -138,6 +147,16 @@ 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
/// Enable experimental integration with swift driver, added in Xcode 14
var enableSwiftDriverIntegration: Bool = false
}
extension XCRemoteCacheConfig {
@@ -170,6 +189,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
@@ -193,6 +214,10 @@ extension XCRemoteCacheConfig {
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
merge.enableSwiftDriverIntegration = scheme.enableSwiftDriverIntegration ?? enableSwiftDriverIntegration
return merge
}
@@ -237,6 +262,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]?
@@ -257,6 +284,9 @@ struct ConfigFileScheme: Decodable {
let disableCertificateVerification: Bool?
let disableVFSOverlay: Bool?
let customRewriteEnvs: [String]?
let irrelevantDependenciesPaths: [String]?
let gracefullyHandleMissingCommonSha: Bool?
let enableSwiftDriverIntegration: Bool?
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
@@ -284,6 +314,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"
@@ -304,6 +336,9 @@ struct ConfigFileScheme: Decodable {
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"
case enableSwiftDriverIntegration = "enable_swift_driver_integration"
}
}
@@ -349,6 +384,7 @@ class XCRemoteCacheConfigReader {
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
}
}
@@ -0,0 +1,77 @@
// 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
/// Parser for `assetcatalog_dependencies` file: an output of the `actool`
/// that lists all dependencies of this command
class AssetsFileDependenciesReader: DependenciesReader {
private let file: URL
private let dirAccessor: DirAccessor
public init(_ file: URL, dirAccessor: DirAccessor) {
self.file = file
self.dirAccessor = dirAccessor
}
public func findDependencies() throws -> [String] {
return try Array(findAllDependencies())
}
public func findInputs() throws -> [String] {
// XCRemoteCache doesn't use it yet
exit(1, "TODO: implement")
}
public func readFilesAndDependencies() throws -> [String: [String]] {
return try ["": findAllDependencies()]
}
private func findAllDependencies() throws -> [String] {
let fileData = try getFileData()
// all dependency files are separated by the \0 byte
// each path has a file type prefix:
// 0x10 - directory
// 0x40 - file
// We only care about dirs, as *.xcassets is a folder
let pathDatas = fileData.split(separator: 0x0)
let paths = pathDatas
.filter { !$0.isEmpty && $0.first == 0x10 }
.map { String(data: $0.dropFirst(), encoding: .utf8)! }
.map(URL.init(fileURLWithPath:))
let xcassetsPaths = paths.filter { path in
path.pathExtension == "xcassets"
}
return try xcassetsPaths.flatMap { try findAssetsContentJsons(xcasset: $0) }
}
private func findAssetsContentJsons(xcasset: URL) throws -> [String] {
return try dirAccessor.recursiveItems(at: xcasset).filter { url in
url.lastPathComponent == "Contents.json"
}.map(\.path)
}
private func getFileData() throws -> Data {
guard let fileData = try dirAccessor.contents(atPath: file.path) else {
throw DependenciesReaderError.readingError
}
return fileData
}
}
@@ -48,6 +48,7 @@ class PhaseCacheModeController: CacheModeController {
private let dependenciesWriter: DependenciesWriter
private let dependenciesReader: DependenciesReader
private let markerWriter: MarkerWriter
private let llbuildLockFile: URL
private let fileManager: FileManager
init(
@@ -59,6 +60,7 @@ class PhaseCacheModeController: CacheModeController {
dependenciesWriter: (URL, FileManager) -> DependenciesWriter,
dependenciesReader: (URL, FileManager) -> DependenciesReader,
markerWriter: (URL, FileManager) -> MarkerWriter,
llbuildLockFile: URL,
fileManager: FileManager
) {
@@ -69,10 +71,12 @@ class PhaseCacheModeController: CacheModeController {
let discoveryURL = tempDir.appendingPathComponent(phaseDependencyPath)
self.dependenciesWriter = dependenciesWriter(discoveryURL, fileManager)
self.dependenciesReader = dependenciesReader(discoveryURL, fileManager)
self.llbuildLockFile = llbuildLockFile
self.markerWriter = markerWriter(modeMarker, fileManager)
}
func enable(allowedInputFiles: [URL], dependencies: [URL]) throws {
try cleanupLlBuildLock()
// marker file contains filepaths that contribute to the build products
// and should invalidate all other target steps (swiftc,libtool etc.)
let targetSensitiveFiles = dependencies + [modeMarker, Self.xcodeSelectLink]
@@ -84,6 +88,7 @@ class PhaseCacheModeController: CacheModeController {
}
func disable() throws {
try cleanupLlBuildLock()
guard !forceCached else {
throw PhaseCacheModeControllerError.cannotUseRemoteCacheForForcedCacheMode
}
@@ -110,8 +115,24 @@ 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
}
// cleanup the build lock file (if exists) as the very last step of this controller
// this is just a non-critical cleanup step to not leave {{LLBUILD_BUILD_ID}}.lock
// files in $TARGET_TEMP_DIR. It is expected that both prebuild and postbuild will
// invoke it, to ensure:
// - swift-frontend synchronization is done per-target build
// - no .lock leftover files
private func cleanupLlBuildLock() throws {
if fileManager.fileExists(atPath: llbuildLockFile.path) {
do {
try fileManager.removeItem(at: llbuildLockFile)
} catch {
printWarning("Removing llbuild lock at \(llbuildLockFile.path) failed. Error: \(error)")
}
}
}
}
@@ -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)
}
}
@@ -22,12 +22,12 @@ import Foundation
/// Reads a list of files from a marker file
class FileMarkerReader: ListReader {
private let file: URL
private let fileManager: FileManager
private let fileReader: FileReader
private var cachedFiles: [URL]?
init(_ file: URL, fileManager: FileManager) {
init(_ file: URL, fileManager: FileReader) {
self.file = file
self.fileManager = fileManager
self.fileReader = fileManager
}
func listFilesURLs() throws -> [URL] {
@@ -45,6 +45,6 @@ class FileMarkerReader: ListReader {
}
func canRead() -> Bool {
return fileManager.fileExists(atPath: file.path)
return fileReader.fileExists(atPath: file.path)
}
}
@@ -0,0 +1,36 @@
// 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 StaticFileListReader: ListReader {
private let list: [URL]
init(list: [URL]) {
self.list = list
}
func listFilesURLs() throws -> [URL] {
list
}
func canRead() -> Bool {
true
}
}
@@ -21,26 +21,35 @@ import Foundation
/// Reads and aggregates all compilation dependencies from a single directory
class TargetDependenciesReader: DependenciesReader {
private let directory: URL
// As of Xcode15, the filename is static
private static let assetsDependenciesFilename = "assetcatalog_dependencies"
private let compilationDirectory: URL
private let assetsCatalogOutputDir: URL
private let dirScanner: DirScanner
private let fileDependeciesReaderFactory: (URL) -> DependenciesReader
private let fileDependenciesReaderFactory: (URL) -> DependenciesReader
private let assetsDependenciesReaderFactory: (URL) -> DependenciesReader
public init(
_ directory: URL,
fileDependeciesReaderFactory: @escaping (URL) -> DependenciesReader,
compilationOutputDir: URL,
assetsCatalogOutputDir: URL,
fileDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
assetsDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
dirScanner: DirScanner
) {
self.directory = directory
self.compilationDirectory = compilationOutputDir
self.assetsCatalogOutputDir = assetsCatalogOutputDir
self.dirScanner = dirScanner
self.fileDependeciesReaderFactory = fileDependeciesReaderFactory
self.fileDependenciesReaderFactory = fileDependenciesReaderFactory
self.assetsDependenciesReaderFactory = assetsDependenciesReaderFactory
}
// Optimized way of finding dependencies only for files that have corresponding .o file on a disk
// includes also inputs to the `actool` assets generator
public func findDependencies() throws -> [String] {
// Not calling `readFilesAndDependencies` as it may unnecessary call expensive `findDependencies()` for
// files that eventually will not be considered
let allURLs = try dirScanner.items(at: directory)
let mergedDependencies = try allURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
let allCompilationOutputURLs = try dirScanner.items(at: compilationDirectory)
var mergedDependencies = try allCompilationOutputURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
// include only these .d files that either have corresponding .o file (incremental) or end
// with '-master' (whole-module)
// Otherwise .d is probably just a leftover from previous builds
@@ -53,20 +62,33 @@ class TargetDependenciesReader: DependenciesReader {
return prev
}
return try prev.union(fileDependeciesReaderFactory(file).findDependencies())
return try prev.union(fileDependenciesReaderFactory(file).findDependencies())
}
// include also dependencies from optional assets compilation (`actool`)
try mergedDependencies.formUnion(findAssetsCatalogDependencies())
return Array(mergedDependencies).sorted()
}
// finds all assets compilation's dependencies, which are always appended to the list of
// files to compare on the consumer side (in the fingerprint comparison)
private func findAssetsCatalogDependencies() throws -> Set<String> {
let expectedAssetsDepsFile = assetsCatalogOutputDir
.appendingPathComponent(Self.assetsDependenciesFilename)
guard try dirScanner.itemType(atPath: expectedAssetsDepsFile.path) == .file else {
return []
}
return try Set(assetsDependenciesReaderFactory(expectedAssetsDepsFile).findDependencies())
}
public func findInputs() throws -> [String] {
fatalError("TODO: implement")
}
public func readFilesAndDependencies() throws -> [String: [String]] {
let allURLs = try dirScanner.items(at: directory)
let allURLs = try dirScanner.items(at: compilationDirectory)
return try allURLs.reduce([String: [String]]()) { prev, file in
var new = prev
new[file.path] = try fileDependeciesReaderFactory(file).findDependencies()
new[file.path] = try fileDependenciesReaderFactory(file).findDependencies()
return new
}
}
@@ -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
@@ -27,7 +27,7 @@ enum RemoteCommitInfo: Equatable {
extension RemoteCommitInfo {
init(_ commit: String?) {
switch commit {
case .some(let value) where !value.isEmpty :
case .some(let value) where !value.isEmpty:
self = .available(commit: value)
default:
self = .unavailable
@@ -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
+2 -2
View File
@@ -84,13 +84,13 @@ private func shellInternal(_ cmd: String, args: [String] = [], stdout: PipeLike?
task.currentDirectoryPath = dir
}
task.launch()
let errorData = (stderr != nil) ? nil : errorHandle.fileHandleForReading.readDataToEndOfFile()
task.waitUntilExit()
if task.terminationStatus != 0 {
if stderr != nil {
guard let errorData = errorData else {
// Error stream was captured so cannot inspect its content
throw ShellError.statusError("Failed command", task.terminationStatus)
}
let errorData = errorHandle.fileHandleForReading.readDataToEndOfFile()
let errorString = String(data: errorData, encoding: .utf8)?.trim() ?? "No error returned from the process."
throw ShellError.statusError(
"status \(task.terminationStatus): \(errorString)", task.terminationStatus
@@ -37,7 +37,7 @@ class ActionSpecificCacheHitLogger: CacheHitLogger {
case .index:
hitCounter = .indexingTargetHitCount
missCounter = .indexingTargetMissCount
case .build:
case .build, .archive:
hitCounter = .targetCacheHit
missCounter = .targetCacheMiss
case .unknown:
@@ -0,0 +1,29 @@
// 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
public extension Array {
func get(_ i: Index) -> Element? {
guard count > i else {
return nil
}
return self[i]
}
}
@@ -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"
}
}
@@ -25,6 +25,8 @@ enum BuildActionType: String, Codable {
case build
/// An extra build, exclusive for indexing (Introduced in Xcode 13)
case index = "indexbuild"
/// Archive build
case archive = "install"
/// Unknown type, probably incompatible Xcode version used
case unknown
}
+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: Array(args.dropFirst()))
try XCLibtool(mode).run()
} catch {
exit(1, "Failed with: \(error). Args: \(args)")
@@ -0,0 +1,80 @@
// 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 input.starts(with: "/") &&
["", "a"].contains(URL(fileURLWithPath: input).pathExtension):
// Assume always absolute paths to the library
// 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()
}
@@ -0,0 +1,150 @@
// 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 `swift-frontend` that skips compilation and
/// produces empty output files (.o). Just like in xcswiftc, compilation dependencies
/// (.d) files are copied from the prebuild marker file which includes all relevant files
/// Fallbacks to a standard `swift-frontend` when the
/// ramote cache is not applicable (e.g. modified sources)
public class XCSwiftcFrontendMain {
// swiftlint:disable:next function_body_length cyclomatic_complexity
public func main() {
let env = ProcessInfo.processInfo.environment
// Do not invoke raw swift-frontend because that would lead to the infinite loop
// swift-frontent -> xcswift-frontent -> swift-frontent
//
// Note: Returning the `swiftc` executaion here because it is possible to pass all arguments
// from swift-frontend to `swiftc` and swiftc will be able to redirect to swift-frontend
// (because the first argument is `-frontend`). If that is not a case (might change in
// future swift compiler versions), invoke swift-frontend from the Xcode, but that introduces
// a limitation that disallows custom toolchains in Xcode:
// $DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/{ ProcessInfo().processName}
let command = "swiftc"
let args = ProcessInfo().arguments
var compile = false
var emitModule = false
var objcHeaderOutput: String?
var moduleName: String?
var target: String?
var inputPaths: [String] = []
var primaryInputPaths: [String] = []
var outputPaths: [String] = []
var dependenciesPaths: [String] = []
var diagnosticsPaths: [String] = []
var sourceInfoPath: String?
var docPath: String?
var supplementaryOutputFileMap: String?
for i in 0..<args.count {
let arg = args[i]
switch arg {
case "-c":
compile = true
case "-emit-module":
emitModule = true
case "-o":
outputPaths.append(args[i + 1])
case "-emit-objc-header-path":
objcHeaderOutput = args[i + 1]
case "-module-name":
moduleName = args[i + 1]
case "-target":
target = args[i + 1]
case "-serialize-diagnostics-path":
// .dia
diagnosticsPaths.append(args[i + 1])
case "-emit-dependencies-path":
// .d
dependenciesPaths.append(args[i + 1])
case "-emit-module-source-info-path":
// .swiftsourceinfo
sourceInfoPath = args[i + 1]
case "-emit-module-doc-path":
// .swiftdoc
docPath = args[i + 1]
case "-primary-file":
// .swift
primaryInputPaths.append(args[i + 1])
case "-supplementary-output-file-map":
supplementaryOutputFileMap = args[i + 1]
default:
if arg.hasSuffix(".swift") {
inputPaths.append(arg)
}
}
}
// support either emitModule (the preflight step) or compilation
// all other types of invocations (like -print-target-info) should be
// automatically redirected to the original swift-frontend
let argInput = SwiftFrontendArgInput(
compile: compile,
emitModule: emitModule,
objcHeaderOutput: objcHeaderOutput,
moduleName: moduleName,
target: target,
primaryInputPaths: primaryInputPaths,
inputPaths: inputPaths,
outputPaths: outputPaths,
dependenciesPaths: dependenciesPaths,
diagnosticsPaths: diagnosticsPaths,
sourceInfoPath: sourceInfoPath,
docPath: docPath,
supplementaryOutputFileMap: supplementaryOutputFileMap
)
// swift-frontend is first invoked with some "probing" args like
// -print-target-info
guard emitModule != compile else {
runFallback(envs: env)
}
do {
let frontend = try XCSwiftFrontend(
command: command,
inputArgs: argInput,
env: env,
dependenciesWriter: FileDependenciesWriter.init,
touchFactory: FileTouch.init)
try frontend.run()
} catch {
runFallback(envs: env)
}
}
private func runFallback(envs env: [String: String]) -> Never {
// DEVELOPER_DIR is always set by Xcode
let developerDir = env["DEVELOPER_DIR"]!
// limitation: always using the Xcode's toolchain, otherwise
// there will be a loop for invoking swift-frontend wrapper from XCRemoteCache
// Cause: for injecting into the swift driver pipeline, Xcode looks for
// an executable with the name `swift-frontend` that is placed in the same
// dir as `SWIFT_EXEC`'s `swiftc` wrapper
let swiftFrontendCommand = "\(developerDir)/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend"
let args = ProcessInfo().arguments
let paramList = [swiftFrontendCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(swiftFrontendCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
}
+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
XCSwiftcFrontendMain().main()
+31 -16
View File
@@ -60,22 +60,37 @@ public class XCSwiftcMain {
let targetInputInput = target,
let swiftFileListInput = swiftFileList
else {
print("Missing argument. Args: \(args)")
exit(1)
executeFallback()
}
let swiftcArgsInput = SwiftcArgInput(
objcHeaderOutput: objcHeaderOutputInput,
moduleName: moduleNameInput,
modulePathOutput: modulePathOutputInput,
filemap: filemapInput,
target: targetInputInput,
fileList: swiftFileListInput
)
XCSwiftc(
command: command,
inputArgs: swiftcArgsInput,
dependenciesWriter: FileDependenciesWriter.init,
touchFactory: FileTouch.init
).run()
do {
let swiftcArgsInput = SwiftcArgInput(
objcHeaderOutput: objcHeaderOutputInput,
moduleName: moduleNameInput,
modulePathOutput: modulePathOutputInput,
filemap: filemapInput,
target: targetInputInput,
fileList: swiftFileListInput
)
try XCSwiftc(
command: command,
inputArgs: swiftcArgsInput,
dependenciesWriter: FileDependenciesWriter.init,
touchFactory: FileTouch.init
).run()
} catch {
executeFallback()
}
}
private func executeFallback() -> Never {
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)
}
}
@@ -0,0 +1,85 @@
// 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.
@testable import XCRemoteCache
import XCTest
class ArtifactMetaUpdaterTests: XCTestCase {
private let accessorFake = FileAccessorFake(mode: .normal)
private var metaWriter: MetaWriter!
private var fileRemapper: FileDependenciesRemapper!
private var updater: ArtifactMetaUpdater!
private let sampleMeta = MainArtifactMeta(
dependencies: [],
fileKey: "abc",
rawFingerprint: "",
generationCommit: "",
targetName: "",
configuration: "",
platform: "",
xcode: "",
inputs: ["$(BASE)/myFile.swift"],
pluginsKeys: [:]
)
override func setUp() async throws {
metaWriter = JsonMetaWriter(
fileWriter: accessorFake,
pretty: true
)
fileRemapper = TextFileDependenciesRemapper(
remapper: StringDependenciesRemapper(
mappings: [
.init(generic: "$(BASE)", local: "/base")
]
),
fileAccessor: accessorFake
)
updater = ArtifactMetaUpdater(
fileRemapper: fileRemapper,
metaWriter: metaWriter
)
}
func testStoresInTheRawArtifact() throws {
try updater.process(rawArtifact: "/artifact")
try updater.run(meta: sampleMeta)
XCTAssertTrue(accessorFake.fileExists(atPath: "/artifact/abc.json"))
}
func testRewirtesMetaPaths() throws {
try updater.process(rawArtifact: "/artifact")
try updater.run(meta: sampleMeta)
let diskMetaData = try XCTUnwrap(accessorFake.contents(atPath: "/artifact/abc.json"))
let diskMeta = try JSONDecoder().decode(MainArtifactMeta.self, from: diskMetaData)
XCTAssertEqual(diskMeta.inputs, ["/base/myFile.swift"])
}
func testFailsIfProcessorTriggerIsNotCalledBeforeRunningAPlugin() throws {
XCTAssertThrowsError(try updater.run(meta: sampleMeta)) { error in
switch error {
case ArtifactMetaUpdaterError.artifactLocationIsUnknown: break
default:
XCTFail("Not expected error")
}
}
}
}
@@ -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,10 +146,49 @@ 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()
XCTAssertEqual(fileKey, expectedFileKey)
}
func testPrepareRunsProcessorsForAlreadyExistingArtifacts() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let artifactURL = zipURL.deletingPathExtension()
let processor = DestroyerArtifactProcessor(fileManager)
let organizer: ZipArtifactOrganizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [processor],
fileManager: fileManager
)
try fileManager.createDirectory(
at: artifactURL,
withIntermediateDirectories: true
)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
XCTAssertFalse(fileManager.fileExists(atPath: preparedArtifact.path))
}
func testPrepareRunsProcessorsForNewlyUnzippedArtifacts() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let processor = DestroyerArtifactProcessor(fileManager)
let organizer: ZipArtifactOrganizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [processor],
fileManager: fileManager
)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
XCTAssertFalse(fileManager.fileExists(atPath: preparedArtifact.path))
}
}
@@ -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,8 +26,9 @@ 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" ,
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal",
"CONFIGURATION": "CONFIGURATION",
"PLATFORM_NAME": "PLATFORM_NAME",
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
@@ -43,6 +44,8 @@ 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",
"LLBUILD_BUILD_ID": "1",
]
override func setUpWithError() throws {
@@ -129,4 +132,72 @@ 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)
}
func testFailsIfLlBuildIdEnvIsMissing() throws {
var envs = Self.SampleEnvs
envs.removeValue(forKey: "LLBUILD_BUILD_ID")
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
}
func testBuildsLockValidFileUrl() throws {
let context = try PostbuildContext(config, env: Self.SampleEnvs)
XCTAssertEqual(context.llbuildIdLockFile, "TARGET_TEMP_DIR/1.lock")
}
}
@@ -27,6 +27,7 @@ class PostbuildTests: FileXCTestCase {
mode: .producer,
targetName: "",
targetTempDir: "",
derivedFilesDir: "",
compilationTempDir: "",
configuration: "",
platform: "",
@@ -53,7 +54,11 @@ class PostbuildTests: FileXCTestCase {
thinnedTargets: [],
action: .build,
modeMarkerPath: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
irrelevantDependenciesPaths: [],
publicHeadersFolderPath: nil,
disabled: false,
llbuildIdLockFile: "/file"
)
private var network = RemoteNetworkClientImpl(
NetworkClientFake(fileManager: .default),
@@ -78,7 +83,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 +646,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))
}
}
@@ -0,0 +1,72 @@
// 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.
@testable import XCRemoteCache
import XCTest
class PrebuildContextTests: FileXCTestCase {
private var config: XCRemoteCacheConfig!
private var remoteCommitFile: URL!
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",
"PLATFORM_NAME": "PLATFORM_NAME",
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
"TARGET_BUILD_DIR": "TARGET_BUILD_DIR",
"PRODUCT_MODULE_NAME": "PRODUCT_MODULE_NAME",
"EXECUTABLE_PATH": "EXECUTABLE_PATH",
"SRCROOT": "SRCROOT",
"DEVELOPER_DIR": "DEVELOPER_DIR",
"MACH_O_TYPE": "MACH_O_TYPE",
"DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT": "DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT",
"DWARF_DSYM_FOLDER_PATH": "DWARF_DSYM_FOLDER_PATH",
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
"CURRENT_VARIANT": "normal",
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
"LLBUILD_BUILD_ID": "1",
]
override func setUpWithError() throws {
try super.setUpWithError()
let workingDir = try prepareTempDir()
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
_ = workingDir.appendingPathComponent("mpo")
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
config.recommendedCacheAddress = "http://test.com"
}
func testFailsIfLlBuildIdEnvIsMissing() throws {
var envs = Self.SampleEnvs
envs.removeValue(forKey: "LLBUILD_BUILD_ID")
XCTAssertThrowsError(try PrebuildContext(config, env: envs))
}
func testBuildsLockValidFileUrl() throws {
let context = try PrebuildContext(config, env: Self.SampleEnvs)
XCTAssertEqual(context.llbuildIdLockFile, "TARGET_TEMP_DIR/1.lock")
}
}
@@ -20,6 +20,7 @@
@testable import XCRemoteCache
import XCTest
// swiftlint:disable file_length
// swiftlint:disable:next type_body_length
class PrebuildTests: FileXCTestCase {
@@ -52,6 +53,13 @@ class PrebuildTests: FileXCTestCase {
remoteNetwork = RemoteNetworkClientImpl(network, URLBuilderFake(remoteCacheURL))
remapper = DependenciesRemapperFake(baseURL: URL(fileURLWithPath: "/"))
metaReader = JsonMetaReader(fileAccessor: FileManager.default)
setupNonCachedContext()
setupCachedContext()
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
}
private func setupNonCachedContext() {
contextNonCached = PrebuildContext(
targetTempDir: sampleURL,
productsDir: sampleURL,
@@ -63,8 +71,13 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false,
llbuildIdLockFile: "/tmp/lock"
)
}
private func setupCachedContext() {
contextCached = PrebuildContext(
targetTempDir: sampleURL,
productsDir: sampleURL,
@@ -76,10 +89,10 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false,
llbuildIdLockFile: "/tmp/lock"
)
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
}
override func tearDownWithError() throws {
@@ -241,7 +254,9 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false,
llbuildIdLockFile: "/tmp/lock"
)
let prebuild = Prebuild(
@@ -272,7 +287,9 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: true,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false,
llbuildIdLockFile: "/tmp/lock"
)
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
@@ -335,7 +352,9 @@ class PrebuildTests: FileXCTestCase {
compilationHistoryFile: compilationHistory,
turnOffRemoteCacheOnFirstTimeout: false,
targetName: "",
overlayHeadersPath: ""
overlayHeadersPath: "",
disabled: false,
llbuildIdLockFile: "/tmp/lock"
)
try globalCacheSwitcher.enable(sha: "1")
let prebuild = Prebuild(
@@ -353,4 +372,35 @@ 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,
llbuildIdLockFile: "/tmp/lock"
)
let prebuild = Prebuild(
context: contextNonCached,
networkClient: remoteNetwork,
remapper: remapper,
fingerprintAccumulator: generator,
artifactsOrganizer: organizer,
globalCacheSwitcher: globalCacheSwitcher,
metaReader: metaReader,
artifactConsumerPrebuildPlugins: []
)
XCTAssertEqual(try prebuild.perform(), .disabled)
}
}

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