├── .gitignore ├── Logo.png ├── Demo ├── .gitignore ├── Demo-iOS │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── ViewController.swift │ ├── Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── AppDelegate.swift ├── Demo-macOS │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Demo_macOS.entitlements │ ├── ViewController.swift │ ├── AppDelegate.swift │ └── Info.plist ├── Demo-tvOS │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── App Icon & Top Shelf Image.brandassets │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Middle.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Middle.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Top Shelf Image.imageset │ │ │ │ └── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Launch Image.launchimage │ │ │ └── Contents.json │ ├── ViewController.swift │ ├── Info.plist │ ├── Base.lproj │ │ └── Main.storyboard │ └── AppDelegate.swift ├── Shared │ ├── AppDependencies │ │ ├── Bagel.swift │ │ ├── Siren.swift │ │ ├── Bond.swift │ │ ├── RealmSwift.swift │ │ ├── Hero.swift │ │ ├── Moya.swift │ │ ├── Realm.swift │ │ ├── SwiftDate.swift │ │ ├── Material.swift │ │ ├── ReSwift.swift │ │ ├── FoldingCell.swift │ │ ├── Haneke.swift │ │ ├── LayoutKit.swift │ │ ├── SQLite.swift │ │ ├── Alamofire.swift │ │ ├── CryptoSwift.swift │ │ ├── Lottie.swift │ │ ├── SwifterSwift.swift │ │ ├── Cartography.swift │ │ ├── JGProgressHUD.swift │ │ ├── MBProgressHUD.swift │ │ ├── Spring.swift │ │ ├── SwiftMessages.swift │ │ ├── SwiftyStoreKit.swift │ │ ├── Eureka.swift │ │ ├── LTMorphingLabel.swift │ │ ├── PromiseKit.swift │ │ ├── RxSwift.swift │ │ ├── SocketIO.swift │ │ ├── Starscream.swift │ │ ├── XCDYouTubeKit.swift │ │ ├── SkeletonView.swift │ │ ├── Kingfisher.swift │ │ ├── XLPagerTabStrip.swift │ │ ├── AFNetworking.swift │ │ ├── XLActionController.swift │ │ ├── CocoaAsyncSocket.swift │ │ ├── NVActivityIndicatorView.swift │ │ ├── SwiftyUserDefaults.swift │ │ ├── RAMAnimatedTabBarController.swift │ │ └── Chatto.swift │ └── TestDependencies │ │ ├── Quick.swift │ │ └── Nimble.swift ├── Demo.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Demo-iOSTests │ ├── Info.plist │ └── Demo_iOSTests.swift ├── Demo-iOSUITests │ ├── Info.plist │ └── Demo_iOSUITests.swift ├── Demo-macOSTests │ ├── Info.plist │ └── Demo_macOSTests.swift ├── Demo-tvOSTests │ ├── Info.plist │ └── Demo_tvOSTests.swift ├── Demo-macOSUITests │ ├── Info.plist │ └── Demo_macOSUITests.swift ├── Demo-tvOSUITests │ ├── Info.plist │ └── Demo_tvOSUITests.swift └── Demo-ShareExtension │ ├── ShareViewController.swift │ ├── Info.plist │ └── Base.lproj │ └── MainInterface.storyboard ├── Sources ├── AccioKit │ ├── Models │ │ ├── InstallationType.swift │ │ ├── CachedFrameworkProduct.swift │ │ ├── Config.swift │ │ ├── AppTarget.swift │ │ ├── FrameworkProduct.swift │ │ ├── Platform.swift │ │ ├── DependencyGraph.swift │ │ ├── Framework.swift │ │ └── Manifest.swift │ ├── Globals │ │ ├── Extensions │ │ │ ├── PBXProjExtension.swift │ │ │ ├── JSONDecoderExtension.swift │ │ │ ├── StringExtension.swift │ │ │ ├── ArrayExtension.swift │ │ │ └── FileManagerExtensions.swift │ │ ├── GlobalOptions.swift │ │ ├── TestHelper.swift │ │ ├── Constants.swift │ │ └── Console.swift │ ├── Services │ │ ├── GitResetService.swift │ │ ├── InstallationTypeDetectorService.swift │ │ ├── TargetTypeDetectorService.swift │ │ ├── GitIgnoreIntegrationService.swift │ │ ├── PlatformDetectorService.swift │ │ ├── SwiftVersionDetectorService.swift │ │ ├── XcodeProjectSchemeHandlerService.swift │ │ ├── DependencyResolverService.swift │ │ ├── ResolvedManifestCachingService.swift │ │ ├── ManifestHandlerService.swift │ │ ├── CarthageBuilderService.swift │ │ ├── CachedBuilderService.swift │ │ ├── FrameworkCachingService.swift │ │ └── XcodeProjectGeneratorService.swift │ └── Commands │ │ ├── SetSharedCacheCommand.swift │ │ ├── UpdateCommand.swift │ │ ├── ClearCacheCommand.swift │ │ ├── InstallCommand.swift │ │ ├── InitCommand.swift │ │ ├── CleanCommand.swift │ │ └── Protocols │ │ └── DependencyInstaller.swift └── Accio │ └── main.swift ├── Tests ├── LinuxMain.swift └── AccioKitTests │ ├── Globals │ ├── Resource.swift │ └── Extensions │ │ └── XCTestCaseExtension.swift │ ├── Services │ ├── XcodeProjectGeneratorServiceTests.swift │ ├── GitIgnoreIntegrationServiceTests.swift │ ├── SwiftVersionDetectorServiceTests.swift │ ├── InstallationTypeDetectorServiceTests.swift │ ├── PlatformDetectorServiceTests.swift │ ├── ResolvedManifestCachingServiceTests.swift │ ├── DependencyResolverServiceTests.swift │ ├── FrameworkCachingServiceTests.swift │ └── XcodeProjectIntegrationServiceTests.swift │ └── Models │ ├── AppTargetTests.swift │ ├── FrameworkTests.swift │ └── ManifestTests.swift ├── Formula └── accio.rb ├── Makefile ├── LICENSE ├── Package.swift ├── CONTRIBUTING.md ├── Package.resolved ├── CODE_OF_CONDUCT.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamitLabs/Accio/HEAD/Logo.png -------------------------------------------------------------------------------- /Demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Accio dependency management 2 | Dependencies/ 3 | .accio/ 4 | 5 | # Demo Testing 6 | Package.resolved 7 | -------------------------------------------------------------------------------- /Demo/Demo-iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo-macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/AccioKit/Models/InstallationType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum InstallationType { 4 | case swiftPackageManager 5 | case carthage 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Bagel.swift: -------------------------------------------------------------------------------- 1 | import Bagel 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let bagelType = Bagel.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Siren.swift: -------------------------------------------------------------------------------- 1 | import Siren 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let sirenType = Siren.self 5 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import AccioKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += AccioKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Bond.swift: -------------------------------------------------------------------------------- 1 | import Bond 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let bondIntType = Bond.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/RealmSwift.swift: -------------------------------------------------------------------------------- 1 | import RealmSwift 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let realmType = Realm.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/TestDependencies/Quick.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let quickSpecType = QuickSpec.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Hero.swift: -------------------------------------------------------------------------------- 1 | import Hero 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let heroAnimatorType = HeroAnimator.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Moya.swift: -------------------------------------------------------------------------------- 1 | import Moya 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let moyaTargetType = Moya.TargetType.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Realm.swift: -------------------------------------------------------------------------------- 1 | import Realm 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let realmRLMObjectType = RLMObject.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SwiftDate.swift: -------------------------------------------------------------------------------- 1 | import SwiftDate 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let swiftDateType = SwiftDate.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Material.swift: -------------------------------------------------------------------------------- 1 | import Material 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let materialViewType = Material.View.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/ReSwift.swift: -------------------------------------------------------------------------------- 1 | import ReSwift 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let reSwiftActionType = ReSwift.Action.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/FoldingCell.swift: -------------------------------------------------------------------------------- 1 | import FoldingCell 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let foldingCellType = FoldingCell.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Haneke.swift: -------------------------------------------------------------------------------- 1 | import Haneke 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let hanekeCacheType = Haneke.Cache.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/LayoutKit.swift: -------------------------------------------------------------------------------- 1 | import LayoutKit 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let layoutKitType = LayoutKit.Layout.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SQLite.swift: -------------------------------------------------------------------------------- 1 | import SQLite 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let sqliteConnectionType = SQLite.Connection.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Alamofire.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let alamofireRequestType = Alamofire.Request.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/CryptoSwift.swift: -------------------------------------------------------------------------------- 1 | import CryptoSwift 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let cryptoSwiftAESType = CryptoSwift.AES.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Lottie.swift: -------------------------------------------------------------------------------- 1 | import Lottie 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let lottieAnimationView = Lottie.AnimationView.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SwifterSwift.swift: -------------------------------------------------------------------------------- 1 | import SwifterSwift 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let swifterSwiftType = SwifterSwift.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Cartography.swift: -------------------------------------------------------------------------------- 1 | import Cartography 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let cartographyEdgeType = Cartography.Edge.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/JGProgressHUD.swift: -------------------------------------------------------------------------------- 1 | import JGProgressHUD 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let jgProgressHUDType = JGProgressHUD.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/MBProgressHUD.swift: -------------------------------------------------------------------------------- 1 | import MBProgressHUD 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let mbProgressHUDType = MBProgressHUD.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Spring.swift: -------------------------------------------------------------------------------- 1 | import Spring 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let springAnimationCurveType = Spring.AnimationCurve.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SwiftMessages.swift: -------------------------------------------------------------------------------- 1 | import SwiftMessages 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let swiftMessagesType = SwiftMessages.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SwiftyStoreKit.swift: -------------------------------------------------------------------------------- 1 | import SwiftyStoreKit 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let swiftyStoreKitType = SwiftyStoreKit.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/TestDependencies/Nimble.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let nimbleExpectationType = Nimble.Expectation.self 5 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Eureka.swift: -------------------------------------------------------------------------------- 1 | import Eureka 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let eurekaFormViewControllerType = Eureka.FormViewController.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/LTMorphingLabel.swift: -------------------------------------------------------------------------------- 1 | import LTMorphingLabel 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let ltMorphingLabelType = LTMorphingLabel.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/PromiseKit.swift: -------------------------------------------------------------------------------- 1 | import PromiseKit 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let promiseKitPromiseType = PromiseKit.Promise.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/RxSwift.swift: -------------------------------------------------------------------------------- 1 | import RxSwift 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let rxSwiftObservableIntType = RxSwift.Observable.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SocketIO.swift: -------------------------------------------------------------------------------- 1 | import SocketIO 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let socketIOSocketManagerType = SocketIO.SocketManager.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Starscream.swift: -------------------------------------------------------------------------------- 1 | import Starscream 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let starscreamWebsocketType = Starscream.WebSocket.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/XCDYouTubeKit.swift: -------------------------------------------------------------------------------- 1 | import XCDYouTubeKit 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let xcdYouTubeClientType = XCDYouTubeClient.self 5 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SkeletonView.swift: -------------------------------------------------------------------------------- 1 | import SkeletonView 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let skeletonTableViewDelegateType = SkeletonTableViewDelegate.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Kingfisher.swift: -------------------------------------------------------------------------------- 1 | import Kingfisher 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let kingfisherKingfisherCompatibleType = Kingfisher.KingfisherCompatible.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/XLPagerTabStrip.swift: -------------------------------------------------------------------------------- 1 | import XLPagerTabStrip 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let xlPagerTabStripViewControllerType = PagerTabStripViewController.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/AFNetworking.swift: -------------------------------------------------------------------------------- 1 | import AFNetworking 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let afNetworkingAFURLSessionManagerType = AFNetworking.AFURLSessionManager.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/XLActionController.swift: -------------------------------------------------------------------------------- 1 | import XLActionController 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let xlActionControllerActionType = XLActionController.Action.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/CocoaAsyncSocket.swift: -------------------------------------------------------------------------------- 1 | import CocoaAsyncSocket 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let cocoaAsyncSocketGCDAsyncSocketType = CocoaAsyncSocket.GCDAsyncSocket.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/NVActivityIndicatorView.swift: -------------------------------------------------------------------------------- 1 | import NVActivityIndicatorView 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let nvActivityIndicatorViewType = NVActivityIndicatorView.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/SwiftyUserDefaults.swift: -------------------------------------------------------------------------------- 1 | import SwiftyUserDefaults 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let swiftyUserDefaultsDefaultsKeysType = SwiftyUserDefaults.DefaultsKeys.self 5 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/RAMAnimatedTabBarController.swift: -------------------------------------------------------------------------------- 1 | import RAMAnimatedTabBarController 2 | 3 | // Ensure that framework was correctly integrated by using public API: 4 | let ramAnimatedTabBarControllerType = RAMAnimatedTabBarController.self 5 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Extensions/PBXProjExtension.swift: -------------------------------------------------------------------------------- 1 | import XcodeProj 2 | 3 | extension PBXProj { 4 | func deleteAllTemporaryFileReferences() { 5 | fileReferences.filter { $0.uuid.hasPrefix("TEMP_") }.forEach { delete(object: $0) } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Extensions/JSONDecoderExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension JSONDecoder { 4 | static var swiftPM: JSONDecoder { 5 | let jsonDecoder = JSONDecoder() 6 | jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase 7 | return jsonDecoder 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Demo/Shared/AppDependencies/Chatto.swift: -------------------------------------------------------------------------------- 1 | import Chatto 2 | import ChattoAdditions 3 | 4 | // Ensure that framework was correctly integrated by using public API: 5 | let chattoBaseChatViewControllerType = Chatto.BaseChatViewController.self 6 | let chattoAdditionsHorizontalAlignmentType = ChattoAdditions.HorizontalAlignment.self 7 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Globals/Resource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | 4 | struct Resource { 5 | let url: URL 6 | let contents: String 7 | 8 | var data: Data? { 9 | return contents.data(using: .utf8) 10 | } 11 | 12 | init(url: URL, contents: String) { 13 | self.url = url 14 | self.contents = contents 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo-macOS/Demo_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Demo/Demo-iOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-iOS 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-tvOS 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/CachedFrameworkProduct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CachedFrameworkProduct: Codable { 4 | let libraryName: String 5 | let commitHash: String 6 | let platform: String 7 | 8 | func getCacheFileSubPath() throws -> String { 9 | let swiftVersion = try SwiftVersionDetectorService.shared.getCurrentSwiftVersion() 10 | return "\(swiftVersion)/\(libraryName)/\(commitHash)/\(platform).zip" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Accio/main.swift: -------------------------------------------------------------------------------- 1 | import AccioKit 2 | import Foundation 3 | import SwiftCLI 4 | 5 | // MARK: - CLI 6 | let cli = CLI(name: "accio", version: "0.6.6", description: "A dependency manager driven by SwiftPM that works for iOS/tvOS/watchOS/macOS projects.") 7 | 8 | cli.commands = [InitCommand(), InstallCommand(), UpdateCommand(), CleanCommand(), ClearCacheCommand(), SetSharedCacheCommand()] 9 | cli.globalOptions.append(contentsOf: GlobalOptions.all) 10 | cli.goAndExit() 11 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/GlobalOptions.swift: -------------------------------------------------------------------------------- 1 | import SwiftCLI 2 | 3 | public enum GlobalOptions { 4 | static let verbose = Flag("-v", "--verbose", description: "Prints more detailed information about the executed command") 5 | static let workingDirectory = Key("-d", "--working-directory", description: "The directory to run the subcommand within") 6 | 7 | public static var all: [Option] { 8 | return [verbose, workingDirectory] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "version" : 1, 22 | "author" : "xcode" 23 | } 24 | } -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "version" : 1, 22 | "author" : "xcode" 23 | } 24 | } -------------------------------------------------------------------------------- /Formula/accio.rb: -------------------------------------------------------------------------------- 1 | class Accio < Formula 2 | desc "Dependency manager driven by SwiftPM for iOS/macOS/tvOS/watchOS" 3 | homepage "https://github.com/JamitLabs/Accio" 4 | url "https://github.com/JamitLabs/Accio.git", :tag => "0.6.6", :revision => "6621950d6bcb0dfc84011ae5ccb3af824eaec20a" 5 | head "https://github.com/JamitLabs/Accio.git" 6 | 7 | depends_on :xcode => ["10.2", :build] 8 | 9 | def install 10 | system "make", "install", "prefix=#{prefix}" 11 | end 12 | 13 | test do 14 | system bin/"accio", "version" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "11.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "landscape", 12 | "idiom" : "tv", 13 | "extent" : "full-screen", 14 | "minimum-system-version" : "9.0", 15 | "scale" : "1x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | 4 | extension String { 5 | var fileNameWithoutExtension: String { 6 | return lastPathComponent.components(separatedBy: ".").dropLast().joined(separator: ".") 7 | } 8 | 9 | var lastPathComponent: String { 10 | return components(separatedBy: "/").last! 11 | } 12 | 13 | var isAliasFile: Bool { 14 | return try! URL(fileURLWithPath: self).resourceValues(forKeys: [URLResourceKey.isAliasFileKey]).isAliasFile ?? false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/GitResetService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class GitResetService { 4 | static let shared = GitResetService() 5 | 6 | private init() { } 7 | 8 | func resetGit(atPath directory: String, includeUntrackedFiles: Bool = true) throws { 9 | try bash("git -C '\(directory)' reset HEAD --hard --quiet 2> /dev/null") 10 | try bash("git -C '\(directory)' clean -fd --quiet 2> /dev/null") 11 | 12 | if includeUntrackedFiles { 13 | try bash("git -C '\(directory)' clean -fdX --quiet 2> /dev/null") 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo-macOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-macOS 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // Do any additional setup after loading the view. 17 | } 18 | 19 | override var representedObject: Any? { 20 | didSet { 21 | // Update the view, if already loaded. 22 | } 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Demo/Demo-macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-macOS 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/InstallationTypeDetectorService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | final class InstallationTypeDetectorService { 5 | static let shared = InstallationTypeDetectorService() 6 | 7 | func detectInstallationType(for framework: Framework) throws -> InstallationType { 8 | let sharedSchemePaths: [String] = try framework.sharedSchemePaths() 9 | let librarySchemePaths: [String] = framework.librarySchemePaths(in: sharedSchemePaths, framework: framework) 10 | 11 | if !librarySchemePaths.isEmpty { 12 | return .carthage 13 | } else { 14 | return .swiftPackageManager 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/TestHelper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A helper class for Unit Testing only. Only put data in here when `isStartedByUnitTests` is set to true. 4 | /// Never read other data in framework than that property. 5 | final class TestHelper { 6 | typealias PrintOutput = (message: String, level: PrintLevel) 7 | 8 | static let shared = TestHelper() 9 | 10 | /// Set to `true` within unit tests (in `setup()`). Defaults to `false`. 11 | var isStartedByUnitTests: Bool = false 12 | 13 | /// Use only in Unit Tests. 14 | var printOutputs: [PrintOutput] = [] 15 | 16 | /// Deletes all data collected until now. 17 | func reset() { 18 | printOutputs = [] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | prefix ?= /usr/local 4 | bindir ?= $(prefix)/bin 5 | libdir ?= $(prefix)/lib 6 | srcdir = Sources 7 | 8 | REPODIR = $(shell pwd) 9 | BUILDDIR = $(REPODIR)/.build 10 | SOURCES = $(wildcard $(srcdir)/**/*.swift) 11 | 12 | .DEFAULT_GOAL = all 13 | 14 | .PHONY: all 15 | all: accio 16 | 17 | accio: $(SOURCES) 18 | @swift build \ 19 | -c release \ 20 | --disable-sandbox \ 21 | --build-path "$(BUILDDIR)" 22 | 23 | .PHONY: install 24 | install: accio 25 | @install -d "$(bindir)" "$(libdir)" 26 | @install "$(BUILDDIR)/release/accio" "$(bindir)" 27 | 28 | .PHONY: uninstall 29 | uninstall: 30 | @rm -rf "$(bindir)/accio" 31 | 32 | .PHONY: clean 33 | distclean: 34 | @rm -f $(BUILDDIR)/release 35 | 36 | .PHONY: clean 37 | clean: distclean 38 | @rm -rf $(BUILDDIR) 39 | -------------------------------------------------------------------------------- /Demo/Demo-iOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo-iOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo-macOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/SetSharedCacheCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public class SetSharedCacheCommand: Command { 5 | // MARK: - Command 6 | public let name: String = "set-shared-cache" 7 | public let shortDescription: String = "Sets a shared cache path to be used by default for future Accio commands" 8 | 9 | let sharedCachePath = Parameter() 10 | 11 | // MARK: - Initializers 12 | public init() {} 13 | 14 | // MARK: - Instance Methods 15 | public func execute() throws { 16 | let config = try Config.load() 17 | config.defaultSharedCachePath = sharedCachePath.value 18 | try config.save() 19 | 20 | print("Successfully set default shared cache path to '\(config.defaultSharedCachePath!)'.", level: .info) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Demo/Demo-macOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo-tvOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - App Store.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } -------------------------------------------------------------------------------- /Tests/AccioKitTests/Globals/Extensions/XCTestCaseExtension.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | extension XCTestCase { 5 | func resourcesLoaded(_ resources: [Resource], testCode: () -> Void) { 6 | removeResourcesIfNeeded(resources) 7 | 8 | for resource in resources { 9 | try! FileManager.default.createFile(atPath: resource.url.path, withIntermediateDirectories: true, contents: resource.data, attributes: nil) 10 | } 11 | 12 | testCode() 13 | removeResourcesIfNeeded(resources) 14 | } 15 | 16 | private func removeResourcesIfNeeded(_ resources: [Resource]) { 17 | for resource in resources { 18 | if FileManager.default.fileExists(atPath: resource.url.path) { 19 | try! FileManager.default.removeItem(atPath: resource.url.path) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Demo/Demo-iOSTests/Demo_iOSTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_iOSTests.swift 3 | // Demo-iOSTests 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Demo_iOS 11 | 12 | class Demo_iOSTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo-tvOSTests/Demo_tvOSTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_tvOSTests.swift 3 | // Demo-tvOSTests 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Demo_tvOS 11 | 12 | class Demo_tvOSTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo-macOSTests/Demo_macOSTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_macOSTests.swift 3 | // Demo-macOSTests 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Demo_macOS 11 | 12 | class Demo_macOSTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIMainStoryboardFile 24 | Main 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | 32 | 33 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Constants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Constants { 4 | static var useTestPaths: Bool = false 5 | 6 | static let buildPath: String = ".accio" 7 | static let dependenciesPath: String = "Dependencies" 8 | static let xcodeDependenciesGroup: String = "Dependencies" 9 | static let copyBuildScript: String = "Accio" 10 | static let copyFilesPhase: String = "Accio" 11 | static let configFilePath: String = FileManager.applicationSupportDirUrl.appendingPathComponent("Accio/config.json").path 12 | static let temporaryFrameworksUrl: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Accio/BuildProducts") 13 | static let temporaryUncachingUrl: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Accio/Uncaching") 14 | 15 | static var localCachePath: String { 16 | if useTestPaths { 17 | return FileManager.userCacheDirUrl.appendingPathComponent("AccioTest/Cache").path 18 | } 19 | 20 | return FileManager.userCacheDirUrl.appendingPathComponent("Accio/Cache").path 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jamit Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Demo/Demo-macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2019 Jamit Labs GmbH. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo/Demo-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Sources/AccioKit/Models/Config.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Config: Codable { 4 | var defaultSharedCachePath: String? 5 | 6 | init(defaultSharedCachePath: String?) { 7 | self.defaultSharedCachePath = defaultSharedCachePath 8 | } 9 | 10 | static func load() throws -> Config { 11 | guard FileManager.default.fileExists(atPath: Constants.configFilePath) else { 12 | let config = Config(defaultSharedCachePath: nil) 13 | try config.save() 14 | return config 15 | } 16 | 17 | let data = try String(contentsOfFile: Constants.configFilePath, encoding: .utf8).data(using: .utf8)! 18 | let config = try JSONDecoder().decode(Config.self, from: data) 19 | return config 20 | } 21 | 22 | func save() throws { 23 | if FileManager.default.fileExists(atPath: Constants.configFilePath) { 24 | try FileManager.default.removeItem(atPath: Constants.configFilePath) 25 | } 26 | 27 | let data = try JSONEncoder().encode(self) 28 | try FileManager.default.createFile(atPath: Constants.configFilePath, withIntermediateDirectories: true, contents: data, attributes: nil) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Demo/Demo-ShareExtension/ShareViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareViewController.swift 3 | // Demo-ShareExtension 4 | // 5 | // Created by Cihat Gündüz on 18.04.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Social 11 | 12 | class ShareViewController: SLComposeServiceViewController { 13 | 14 | override func isContentValid() -> Bool { 15 | // Do validation of contentText and/or NSExtensionContext attachments here 16 | return true 17 | } 18 | 19 | override func didSelectPost() { 20 | // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. 21 | 22 | // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. 23 | self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) 24 | } 25 | 26 | override func configurationItems() -> [Any]! { 27 | // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. 28 | return [] 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/UpdateCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public class UpdateCommand: Command { 5 | // MARK: - Command 6 | public let name: String = "update" 7 | public let shortDescription: String = "Updates the dependencies" 8 | 9 | let sharedCachePath = Key("-c", "--shared-cache-path", description: "Path used by multiple users for caching built products") 10 | 11 | // MARK: - Initializers 12 | public init() {} 13 | 14 | // MARK: - Instance Methods 15 | public func execute() throws { 16 | let config = try Config.load() 17 | try revertCheckoutChanges() 18 | try DependencyResolverService.shared.updateDependencies() 19 | 20 | let manifest = try loadManifest() 21 | let dependencyGraph = try DependencyResolverService.shared.dependencyGraph() 22 | 23 | try buildFrameworksAndIntegrateWithXcode( 24 | manifest: manifest, 25 | dependencyGraph: dependencyGraph, 26 | sharedCachePath: sharedCachePath.value ?? config.defaultSharedCachePath 27 | ) 28 | print("Successfully updated dependencies.", level: .info) 29 | } 30 | } 31 | 32 | extension UpdateCommand: DependencyInstaller {} 33 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XcodeProj 3 | 4 | extension Array where Element == Framework { 5 | /// Flattens frameworks including subdependencies returns all in correct build order. 6 | func flattenedDeepFirstOrder() -> [Framework] { 7 | return flatMap { $0.requiredFrameworks.flattenedDeepFirstOrder() + [$0] } 8 | } 9 | } 10 | 11 | extension Array where Element == PBXFileElement { 12 | mutating func removeDuplicates() { 13 | var uniqElements: [PBXFileElement] = [] 14 | 15 | for element in self { 16 | if !uniqElements.contains(where: { $0.name == element.name }) { 17 | uniqElements.append(element) 18 | } 19 | } 20 | 21 | self = uniqElements 22 | } 23 | } 24 | 25 | extension Array where Element == FrameworkProduct { 26 | func removingDuplicates() -> [FrameworkProduct] { 27 | var uniqElements: [FrameworkProduct] = [] 28 | 29 | for element in self { 30 | if !uniqElements.contains(where: { $0.frameworkDirPath == element.frameworkDirPath }) { 31 | uniqElements.append(element) 32 | } 33 | } 34 | 35 | return uniqElements 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Demo/Demo-ShareExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Demo-ShareExtension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | NSExtensionActivationRule 28 | TRUEPREDICATE 29 | 30 | NSExtensionMainStoryboard 31 | MainInterface 32 | NSExtensionPointIdentifier 33 | com.apple.share-services 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/TargetTypeDetectorService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XcodeProj 3 | import PathKit 4 | 5 | enum TargetTypeDetectorError: Error { 6 | case targetNotFound 7 | case platformNotSpecified 8 | } 9 | 10 | final class TargetTypeDetectorService { 11 | static let shared = TargetTypeDetectorService(workingDirectory: GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) 12 | 13 | private let workingDirectory: String 14 | 15 | init(workingDirectory: String) { 16 | self.workingDirectory = workingDirectory 17 | } 18 | 19 | func detectTargetType(ofTarget targetName: String, in projectName: String) throws -> AppTarget.TargetType { 20 | let xcodeProjectPath = "\(workingDirectory)/\(projectName).xcodeproj" 21 | let projectFile = try XcodeProj(path: Path(xcodeProjectPath)) 22 | 23 | for targetType in [AppTarget.TargetType.app, AppTarget.TargetType.appExtension] { 24 | if projectFile.pbxproj.fileReferences.contains(where: { $0.path == "\(targetName).\(targetType.wrapperExtension)" }) { 25 | return targetType 26 | } 27 | } 28 | 29 | return targetName.contains("Tests") ? .test : .app // fall back to target name based logic 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/ClearCacheCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public class ClearCacheCommand: Command { 5 | // MARK: - Command 6 | public let name: String = "clear-cache" 7 | public let shortDescription: String = "Deletes all cached build products from local cache to make up space" 8 | 9 | // MARK: - Initializers 10 | public init() {} 11 | 12 | // MARK: - Instance Methods 13 | public func execute() throws { 14 | guard FileManager.default.fileExists(atPath: Constants.localCachePath) else { 15 | print("Local cache is already empty.", level: .info) 16 | return 17 | } 18 | 19 | print("Calculating size of local cache ...", level: .info) 20 | let localCacheDirectorySizeInBytes = try FileManager.default.directorySizeInBytes(atPath: Constants.localCachePath) 21 | 22 | print("Clearing local cache ...", level: .info) 23 | try bash("rm -rf '\(Constants.localCachePath)'") 24 | 25 | let byteCountFormatter = ByteCountFormatter() 26 | let localCacheSizeString = byteCountFormatter.string(fromByteCount: localCacheDirectorySizeInBytes) 27 | print("Successfully cleared local cache. Total space freed up: \(localCacheSizeString)", level: .info) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Demo/Demo-iOSUITests/Demo_iOSUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_iOSUITests.swift 3 | // Demo-iOSUITests 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Demo_iOSUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo-tvOSUITests/Demo_tvOSUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_tvOSUITests.swift 3 | // Demo-tvOSUITests 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Demo_tvOSUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo-macOSUITests/Demo_macOSUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Demo_macOSUITests.swift 3 | // Demo-macOSUITests 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Demo_macOSUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 20 | XCUIApplication().launch() 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/AppTarget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AppTarget { 4 | enum TargetType: String, CaseIterable { 5 | case app 6 | case test 7 | case appExtension 8 | 9 | var packageSpecifier: String { 10 | switch self { 11 | case .app, .appExtension: 12 | return "target" 13 | 14 | case .test: 15 | return "testTarget" 16 | } 17 | } 18 | 19 | var wrapperExtension: String { 20 | switch self { 21 | case .app: 22 | return "app" 23 | 24 | case .appExtension: 25 | return "appex" 26 | 27 | case .test: 28 | return "xctest" 29 | } 30 | } 31 | } 32 | 33 | let projectName: String 34 | let targetName: String 35 | let dependentLibraryNames: [String] 36 | let targetType: TargetType 37 | } 38 | 39 | extension AppTarget { 40 | func frameworkDependencies(manifest: Manifest, dependencyGraph: DependencyGraph) throws -> [Framework] { 41 | var frameworks: [Framework] = [] 42 | 43 | for libraryName in dependentLibraryNames { 44 | frameworks.append(try dependencyGraph.framework(libraryName: libraryName)) 45 | } 46 | 47 | return frameworks 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/InstallCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public class InstallCommand: Command { 5 | // MARK: - Command 6 | public let name: String = "install" 7 | public let shortDescription: String = "Installs the already resolved dependencies" 8 | 9 | let sharedCachePath = Key("-c", "--shared-cache-path", description: "Path used by multiple users for caching built products") 10 | 11 | // MARK: - Initializers 12 | public init() {} 13 | 14 | // MARK: - Instance Methods 15 | public func execute() throws { 16 | let config = try Config.load() 17 | 18 | if try loadRequiredFrameworksFromCache(sharedCachePath: sharedCachePath.value ?? config.defaultSharedCachePath) { 19 | print("No changes found & successfully copied dependencies from cache.", level: .info) 20 | return 21 | } 22 | 23 | try revertCheckoutChanges() 24 | try DependencyResolverService.shared.resolveDependencies() 25 | 26 | let manifest = try loadManifest() 27 | let dependencyGraph = try DependencyResolverService.shared.dependencyGraph() 28 | 29 | try buildFrameworksAndIntegrateWithXcode( 30 | manifest: manifest, 31 | dependencyGraph: dependencyGraph, 32 | sharedCachePath: sharedCachePath.value ?? config.defaultSharedCachePath 33 | ) 34 | print("Successfully installed dependencies.", level: .info) 35 | } 36 | } 37 | 38 | extension InstallCommand: DependencyInstaller {} 39 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/GitIgnoreIntegrationService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | 4 | enum GitIgnoreIntegrationError: Error { 5 | case gitignoreFileReadingFailed 6 | } 7 | 8 | final class GitIgnoreIntegrationService { 9 | static let shared = GitIgnoreIntegrationService(workingDirectory: GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) 10 | 11 | private let workingDirectory: String 12 | 13 | init(workingDirectory: String) { 14 | self.workingDirectory = workingDirectory 15 | } 16 | 17 | func addIgnoreEntriesIfNeeded() throws { 18 | let gitignoreUrl = URL(fileURLWithPath: workingDirectory).appendingPathComponent(".gitignore") 19 | 20 | if !FileManager.default.fileExists(atPath: gitignoreUrl.path) { 21 | try bash("touch \(gitignoreUrl.path)") 22 | } 23 | 24 | var gitignoreContents: String = try String(contentsOfFile: gitignoreUrl.path) 25 | if !gitignoreContents.contains("\n\(Constants.dependenciesPath)/\n") || !gitignoreContents.contains("\n\(Constants.buildPath)/\n") { 26 | print("Adding .gitignore entries for build & dependencies directories.", level: .info) 27 | 28 | if !gitignoreContents.isBlank { 29 | gitignoreContents += "\n\n" 30 | } 31 | 32 | gitignoreContents += "# Accio dependency management\n\(Constants.dependenciesPath)/\n\(Constants.buildPath)/\n" 33 | try gitignoreContents.write(toFile: gitignoreUrl.path, atomically: true, encoding: .utf8) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/InitCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | enum InitCommandError: Error { 5 | case missingProjectName 6 | case missingTargetNames 7 | } 8 | 9 | public class InitCommand: Command { 10 | // MARK: - Command 11 | public let name: String = "init" 12 | public let shortDescription: String = "Initializes Accio in this project" 13 | 14 | let projectName = Key("-p", "--project-name", description: "The name of the Xcode project file (without the .xcodeproj extension)") 15 | let targetNames = Key("-t", "--target-name", description: "A comma-separated list of your App targets") 16 | 17 | // MARK: - Initializers 18 | public init() {} 19 | 20 | // MARK: - Instance Methods 21 | public func execute() throws { 22 | guard let projectName = self.projectName.value else { 23 | print("Missing parameter: \(self.projectName.names.last!)", level: .error) 24 | throw InitCommandError.missingProjectName 25 | } 26 | 27 | guard let targetNames = targetNames.value?.components(separatedBy: ",") else { 28 | print("Missing parameter: \(self.targetNames.names.last!)", level: .error) 29 | throw InitCommandError.missingTargetNames 30 | } 31 | 32 | try ManifestHandlerService.shared.createManifestFromDefaultTemplateIfNeeded(projectName: projectName, targetNames: targetNames) 33 | try GitIgnoreIntegrationService.shared.addIgnoreEntriesIfNeeded() 34 | 35 | print("Successfully initialized project.", level: .info) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Accio", 7 | platforms: [.macOS(.v10_12)], 8 | products: [ 9 | .executable(name: "accio", targets: ["Accio"]), 10 | .library(name: "AccioKit", type: .dynamic, targets: ["AccioKit"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.0.0")), 14 | .package(url: "https://github.com/Flinesoft/HandySwift.git", .upToNextMajor(from: "3.0.0")), 15 | .package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMajor(from: "3.1.4")), 16 | .package(url: "https://github.com/jakeheis/SwiftCLI.git", .upToNextMajor(from: "5.2.2")), 17 | .package(url: "https://github.com/kareman/SwiftShell.git", .upToNextMajor(from: "5.1.0")), 18 | .package(url: "https://github.com/tuist/xcodeproj.git", .upToNextMajor(from: "7.0.0")), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "Accio", 23 | dependencies: ["AccioKit"] 24 | ), 25 | .target( 26 | name: "AccioKit", 27 | dependencies: [ 28 | "CryptoSwift", 29 | "HandySwift", 30 | "Rainbow", 31 | "SwiftCLI", 32 | "SwiftShell", 33 | "XcodeProj", 34 | ] 35 | ), 36 | .testTarget( 37 | name: "AccioKitTests", 38 | dependencies: ["CryptoSwift", "AccioKit", "HandySwift", "XcodeProj"] 39 | ) 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /Demo/Demo-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/CleanCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public class CleanCommand: Command { 5 | // MARK: - Command 6 | public let name: String = "clean" 7 | public let shortDescription: String = "Deletes all checkouts and build prodcuts from within the current projects directory" 8 | 9 | // MARK: - Initializers 10 | public init() {} 11 | 12 | // MARK: - Instance Methods 13 | public func execute() throws { 14 | print("Calculating size of local build path & temp dirs ...", level: .info) 15 | let localBuildDirectorySizeInBytes = try FileManager.default.directorySizeInBytes(atPath: Constants.buildPath) 16 | let temporaryUncachingUrlSizeInBytes = try FileManager.default.directorySizeInBytes(atPath: Constants.temporaryFrameworksUrl.path) 17 | let temporaryFrameworksUrlSizeInBytes = try FileManager.default.directorySizeInBytes(atPath: Constants.temporaryFrameworksUrl.path) 18 | 19 | print("Cleaning local build path & temp dirs ...", level: .info) 20 | try bash("rm -rf '\(Constants.buildPath)'") 21 | try bash("rm -rf '\(Constants.temporaryUncachingUrl.path)'") 22 | try bash("rm -rf '\(Constants.temporaryFrameworksUrl.path)'") 23 | 24 | let byteCountFormatter = ByteCountFormatter() 25 | let localBuildPathSizeString = byteCountFormatter.string( 26 | fromByteCount: localBuildDirectorySizeInBytes + temporaryUncachingUrlSizeInBytes + temporaryFrameworksUrlSizeInBytes 27 | ) 28 | print("Successfully cleaned local build path & temp dirs. Total space freed up: \(localBuildPathSizeString)", level: .info) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Demo/Demo-iOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Demo/Demo-iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/Demo-ShareExtension/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/FrameworkProduct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct FrameworkProduct { 4 | let frameworkDirPath: String 5 | let symbolsFilePath: String 6 | let commitHash: String 7 | 8 | init(frameworkDirPath: String, symbolsFilePath: String, commitHash: String) { 9 | self.frameworkDirPath = frameworkDirPath 10 | self.symbolsFilePath = symbolsFilePath 11 | self.commitHash = commitHash 12 | } 13 | 14 | init(libraryName: String, platformName: String, commitHash: String) { 15 | self.frameworkDirPath = Constants.temporaryFrameworksUrl.appendingPathComponent("\(platformName)/\(libraryName).framework").path 16 | self.symbolsFilePath = Constants.temporaryFrameworksUrl.appendingPathComponent("\(platformName)/\(libraryName).framework.dSYM").path 17 | self.commitHash = commitHash 18 | } 19 | 20 | var frameworkDirUrl: URL { 21 | return URL(fileURLWithPath: frameworkDirPath) 22 | } 23 | 24 | var symbolsFileUrl: URL { 25 | return URL(fileURLWithPath: symbolsFilePath) 26 | } 27 | 28 | var libraryName: String { 29 | return frameworkDirUrl.lastPathComponent.replacingOccurrences(of: ".framework", with: "") 30 | } 31 | 32 | var platformName: String { 33 | return frameworkDirUrl.pathComponents.suffix(2).first! 34 | } 35 | 36 | // This is a workaround for issues with frameworks that symlink to themselves (first found in RxSwift) 37 | func cleanupRecursiveFrameworkIfNeeded() throws { 38 | let recursiveFrameworkPath: String = frameworkDirUrl.appendingPathComponent(frameworkDirUrl.lastPathComponent).path 39 | if FileManager.default.fileExists(atPath: recursiveFrameworkPath) { 40 | try FileManager.default.removeItem(atPath: recursiveFrameworkPath) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Console.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Rainbow 3 | import SwiftShell 4 | 5 | /// The print level type. 6 | enum PrintLevel { 7 | /// Print (potentially) long data or less interesting information. Only printed if tool executed in vebose mode. 8 | case verbose 9 | 10 | /// Print any kind of information potentially interesting to users. 11 | case info 12 | 13 | /// Print information that might potentially be problematic. 14 | case warning 15 | 16 | /// Print information that probably is problematic. 17 | case error 18 | } 19 | 20 | /// Prints a message to command line with proper formatting based on level, source & output target. 21 | /// 22 | /// - Parameters: 23 | /// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning. 24 | /// - level: The level of the print statement. 25 | func print(_ message: String, level: PrintLevel) { 26 | if TestHelper.shared.isStartedByUnitTests { 27 | TestHelper.shared.printOutputs.append((message, level)) 28 | } 29 | 30 | switch level { 31 | case .verbose: 32 | if GlobalOptions.verbose.value { 33 | print("✨ ", message.lightCyan) 34 | } 35 | 36 | case .info: 37 | print("✨ ", message.lightBlue) 38 | 39 | case .warning: 40 | print("⚠️ ", message.yellow) 41 | 42 | case .error: 43 | print("❌ ", message.red) 44 | } 45 | } 46 | 47 | /// Executes a bash command on the command line and shows both the executed commands and its outputs. 48 | /// 49 | /// - Parameters: 50 | /// - command: The bash command to be executed on the command line. 51 | func bash(_ command: String) throws { 52 | if GlobalOptions.verbose.value { 53 | print("⏳ Executing '\(command.italic.lightYellow)'".bold) 54 | } 55 | 56 | try runAndPrint(bash: command) 57 | } 58 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/PlatformDetectorService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XcodeProj 3 | import PathKit 4 | 5 | enum PlatformDetectorError: Error { 6 | case targetNotFound 7 | case platformNotSpecified 8 | } 9 | 10 | final class PlatformDetectorService { 11 | static let shared = PlatformDetectorService(workingDirectory: GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) 12 | 13 | private let workingDirectory: String 14 | 15 | init(workingDirectory: String) { 16 | self.workingDirectory = workingDirectory 17 | } 18 | 19 | func detectPlatform(of appTarget: AppTarget) throws -> Platform { 20 | let xcodeProjectPath = "\(workingDirectory)/\(appTarget.projectName).xcodeproj" 21 | let projectFile = try XcodeProj(path: Path(xcodeProjectPath)) 22 | let rootProject = try projectFile.pbxproj.rootProject() 23 | 24 | guard let targetObject = projectFile.pbxproj.targets(named: appTarget.targetName).first else { 25 | print("Could not find any target named '\(appTarget.targetName)' at Xcode project path '\(xcodeProjectPath)'.", level: .error) 26 | throw PlatformDetectorError.targetNotFound 27 | } 28 | 29 | let targetPlatformSpecifier: String? = targetObject.buildConfigurationList?.buildConfigurations.first?.buildSettings["SDKROOT"] as? String 30 | let projectPlatformSpecifier: String? = rootProject?.buildConfigurationList?.buildConfigurations.first?.buildSettings["SDKROOT"] as? String 31 | 32 | guard let platformSpecifier = targetPlatformSpecifier ?? projectPlatformSpecifier else { 33 | print("Could not detect platform type from Xcode project at '\(xcodeProjectPath)'.", level: .error) 34 | throw PlatformDetectorError.platformNotSpecified 35 | } 36 | 37 | return Platform.with(target: platformSpecifier) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/AccioKit/Globals/Extensions/FileManagerExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FileManager { 4 | static var userCacheDirUrl: URL { 5 | return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! 6 | } 7 | 8 | static var applicationSupportDirUrl: URL { 9 | return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 10 | } 11 | 12 | func createFile(atPath path: String, withIntermediateDirectories: Bool, contents: Data?, attributes: [FileAttributeKey: Any]?) throws { 13 | let directoryUrl = URL(fileURLWithPath: path).deletingLastPathComponent() 14 | 15 | if withIntermediateDirectories && !FileManager.default.fileExists(atPath: directoryUrl.path) { 16 | try createDirectory(at: directoryUrl, withIntermediateDirectories: true, attributes: attributes) 17 | } 18 | 19 | createFile(atPath: path, contents: contents, attributes: attributes) 20 | } 21 | 22 | func directorySizeInBytes(atPath path: String) throws -> Int64 { 23 | guard FileManager.default.fileExists(atPath: path) else { return 0 } 24 | 25 | let folderUrl = URL(fileURLWithPath: path) 26 | let relativeFilePaths: [String] = try FileManager.default.subpathsOfDirectory(atPath: path) 27 | let filePaths: [String] = relativeFilePaths.map { folderUrl.appendingPathComponent($0).path } 28 | return try filePaths.reduce(0) { $0 + (try fileSizeInBytes(atPath: $1)) } 29 | } 30 | 31 | func fileSizeInBytes(atPath path: String) throws -> Int64 { 32 | return Int64(try FileManager.default.attributesOfItem(atPath: path)[FileAttributeKey.size] as! UInt64) 33 | } 34 | 35 | func isDirectory(atPath path: String) throws -> Bool { 36 | var isDirectory: ObjCBool = false 37 | 38 | if fileExists(atPath: path, isDirectory: &isDirectory) { 39 | return isDirectory.boolValue 40 | } else { 41 | return false 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Demo/Demo-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Bug reports and pull requests are welcome on GitHub at https://github.com/JamitLabs/Accio. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 4 | 5 | ## Getting Started 6 | 7 | This section will tell you how you can get started contributing to Accio. 8 | 9 | ### Prerequisites 10 | 11 | Before you start developing, please make sure you have the following tools installed on your machine: 12 | 13 | - Xcode 10+ 14 | - [SwiftLint](https://github.com/realm/SwiftLint) 15 | 16 | ### Useful Commands 17 | 18 | In order to generate the **Xcode project** to develop within, run this command: 19 | 20 | ``` 21 | swift package generate-xcodeproj 22 | ``` 23 | 24 | To check if all **tests** are passing correctly: 25 | 26 | ``` 27 | swift test 28 | ``` 29 | 30 | To check if the integration tests in the Demo project pass successfully: 31 | 32 | ``` 33 | cd Demo 34 | accio update 35 | open Demo.xcodeproj/ 36 | ``` 37 | 38 | Then build & run the tests via `Cmd+U` for each of the available schemes schemes (iOS / tvOS / macOS). 39 | 40 | To check if the **linter** shows any warnings or errors: 41 | 42 | ``` 43 | swiftlint 44 | ``` 45 | 46 | Alternatively you can also add `swiftlint` as a build script to the target `AccioKit` so warnings & errors show off right within Xcode when building. (Recommended) 47 | 48 | ### Development Tips 49 | 50 | #### Debugging with Xcode 51 | To run the Accio tool right from within Xcode for testing, replace the line 52 | 53 | ```swift 54 | cli.goAndExit() 55 | ``` 56 | 57 | from the file at path `Sources/Accio/main.swift` with something like: 58 | 59 | ```swift 60 | cli.go(with: ["install", "-d", "/Users/You/path/to/Accio/Demo", "-v"]) 61 | ``` 62 | 63 | Note that the `-d` option specifies the path from within to run Accio and `-v` makes sure the logging level is set to `verbose`. 64 | 65 | ### Commit Messages 66 | 67 | Please also try to follow the same syntax and semantic in your **commit messages** (see rationale [here](http://chris.beams.io/posts/git-commit/)). 68 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/Platform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Platform: String, CaseIterable { 4 | case iOS 5 | case macOS 6 | case tvOS 7 | case watchOS 8 | 9 | static func with(target: String) -> Platform { 10 | switch target.lowercased() { 11 | case Platform.iOS.rawValue.lowercased(), "iphone", "ipad", "iphoneos": 12 | return .iOS 13 | 14 | case Platform.macOS.rawValue.lowercased(), "osx", "mac", "macosx": 15 | return .macOS 16 | 17 | case Platform.tvOS.rawValue.lowercased(), "appletv", "appletvos": 18 | return .tvOS 19 | 20 | case Platform.watchOS.rawValue.lowercased(), "applewatch": 21 | return .watchOS 22 | 23 | default: 24 | return .iOS 25 | } 26 | } 27 | 28 | var deploymentTargetBuildSetting: String { 29 | switch self { 30 | case .iOS: 31 | return "IPHONEOS_DEPLOYMENT_TARGET" 32 | 33 | case .macOS: 34 | return "MACOSX_DEPLOYMENT_TARGET" 35 | 36 | case .tvOS: 37 | return "TVOS_DEPLOYMENT_TARGET" 38 | 39 | case .watchOS: 40 | return "WATCHOS_DEPLOYMENT_TARGET" 41 | } 42 | } 43 | 44 | var carthageBuildFolderName: String { 45 | switch self { 46 | case .iOS, .tvOS, .watchOS: 47 | return rawValue 48 | 49 | case .macOS: 50 | return "Mac" 51 | } 52 | } 53 | 54 | var specifiers: [String] { 55 | switch self { 56 | case .iOS: 57 | return [rawValue, "iPhone", "iPad"] 58 | 59 | case .macOS: 60 | return [rawValue, "OSX", "OS X", "Mac", "Mac OSX", "Mac OS X"] 61 | 62 | case .tvOS: 63 | return [rawValue, "Apple TV", "AppleTV"] 64 | 65 | case .watchOS: 66 | return [rawValue, "Apple Watch", "AppleWatch"] 67 | } 68 | } 69 | 70 | var pathToPlist: String { 71 | switch self { 72 | case .iOS: 73 | return "" 74 | 75 | case .macOS: 76 | return "Resources" 77 | 78 | case .tvOS: 79 | return "" 80 | 81 | case .watchOS: 82 | return "" 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/SwiftVersionDetectorService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | import SwiftShell 4 | 5 | enum SwiftVersionDetectorError: Error { 6 | case swiftVersionCommandError 7 | case parsingError 8 | } 9 | 10 | class SwiftVersionDetectorService { 11 | static let shared = SwiftVersionDetectorService() 12 | 13 | func getCurrentSwiftVersion() throws -> String { 14 | let result = run(bash: "swift --version") 15 | guard result.succeeded else { 16 | throw SwiftVersionDetectorError.swiftVersionCommandError 17 | } 18 | 19 | return try convertToSwiftVersion(swiftVersionOutput: result.stdout) 20 | } 21 | 22 | func convertToSwiftVersion(swiftVersionOutput: String) throws -> String { 23 | do { 24 | let regex = try Regex(#"Apple Swift version ([\d.]*) \(swiftlang"#) 25 | guard let versionNumber = regex.firstMatch(in: swiftVersionOutput)?.captures.first ?? nil else { 26 | throw SwiftVersionDetectorError.parsingError 27 | } 28 | 29 | return "Swift-\(versionNumber)" 30 | } 31 | catch { 32 | throw SwiftVersionDetectorError.parsingError 33 | } 34 | } 35 | 36 | func detectSwiftVersion(ofFrameworkProduct frameworkProduct: FrameworkProduct) throws -> String { 37 | let swiftHeaderFileUrl = frameworkProduct.frameworkDirUrl.appendingPathComponent( 38 | "Headers/\(frameworkProduct.libraryName)-Swift.h" 39 | ) 40 | 41 | if 42 | FileManager.default.fileExists(atPath: swiftHeaderFileUrl.path), 43 | let swiftHeaderFileContents = try? String(contentsOf: swiftHeaderFileUrl, encoding: .utf8) 44 | { 45 | do { 46 | let regex = try Regex(#"// Generated by Apple Swift version ([\d.]*) "#) 47 | guard let versionNumber = regex.firstMatch(in: swiftHeaderFileContents)?.captures.first ?? nil else { 48 | throw SwiftVersionDetectorError.parsingError 49 | } 50 | 51 | return "Swift-\(versionNumber)" 52 | } 53 | catch { 54 | throw SwiftVersionDetectorError.parsingError 55 | } 56 | } 57 | 58 | throw SwiftVersionDetectorError.parsingError 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Demo/Demo-tvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-tvOS 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/DependencyGraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum DependencyGraphError: Error { 4 | case libraryNotFound 5 | } 6 | 7 | class DependencyGraph: Decodable { 8 | // MARK: - Sub Types 9 | class Dependency: Decodable { 10 | let name: String 11 | let path: String 12 | let dependencies: [Dependency] 13 | 14 | private var cachedManifest: Manifest? 15 | 16 | func manifest() throws -> Manifest { 17 | if cachedManifest == nil { 18 | cachedManifest = try ManifestHandlerService(workingDirectory: path).loadManifest(isDependency: true) 19 | } 20 | 21 | return cachedManifest! 22 | } 23 | } 24 | 25 | // MARK: - Properties 26 | let name: String 27 | let dependencies: [Dependency] 28 | 29 | private var cachedDeepFirstDependencies: [Dependency]? 30 | 31 | /// Flattened all dependencies including subdependencies into one array. 32 | var deepFirstDependencies: [Dependency] { 33 | if cachedDeepFirstDependencies == nil { 34 | cachedDeepFirstDependencies = dependencies.flatMap { $0.deepFirstDependencies() } 35 | } 36 | 37 | return cachedDeepFirstDependencies! 38 | } 39 | } 40 | 41 | extension DependencyGraph.Dependency { 42 | /// Flattened all dependencies including self & subdependencies into one array. 43 | func deepFirstDependencies() -> [DependencyGraph.Dependency] { 44 | return dependencies.flatMap { $0.deepFirstDependencies() } + [self] 45 | } 46 | } 47 | 48 | extension DependencyGraph { 49 | func framework(libraryName: String) throws -> Framework { 50 | guard let dependency = try deepFirstDependencies.first(where: { try $0.manifest().products.contains { $0.name == libraryName } }) else { 51 | print("DependencyGraph: Could not find library product with name '\(libraryName)' in package manifest for project '\(name)'.", level: .error) 52 | throw DependencyGraphError.libraryNotFound 53 | } 54 | 55 | return Framework( 56 | projectName: dependency.name, 57 | libraryName: libraryName, 58 | projectDirectory: dependency.path, 59 | requiredFrameworks: try dependency.manifest().frameworkDependencies(ofLibrary: libraryName, dependencyGraph: self) 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Demo/Demo-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-iOS 4 | // 5 | // Created by Cihat Gündüz on 28.03.19. 6 | // Copyright © 2019 Jamit Labs GmbH. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/XcodeProjectSchemeHandlerService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class XcodeProjectSchemeHandlerService { 4 | static let shared = XcodeProjectSchemeHandlerService() 5 | 6 | func removeUnnecessarySharedSchemes(from framework: Framework, platform: Platform) throws { 7 | let sharedSchemePaths: [String] = try framework.sharedSchemePaths() 8 | let librarySchemePaths: [String] = framework.librarySchemePaths(in: sharedSchemePaths, framework: framework) 9 | 10 | let expectedSchemeNames: [String] = platform.specifiers.flatMap { platformSpecifier in 11 | return [ 12 | framework.libraryName, 13 | "\(framework.libraryName) Framework", 14 | "\(framework.libraryName) Library", 15 | "\(framework.libraryName) \(platformSpecifier)", 16 | "\(framework.libraryName) (\(platformSpecifier))", 17 | "\(framework.libraryName)-\(platformSpecifier)", 18 | "\(framework.libraryName)_\(platformSpecifier)", 19 | "\(framework.libraryName) Framework \(platformSpecifier)", 20 | "\(framework.libraryName) Library \(platformSpecifier)", 21 | "\(framework.libraryName) Framework (\(platformSpecifier))", 22 | "\(framework.libraryName) Library (\(platformSpecifier))", 23 | "\(framework.libraryName)-Package" 24 | ].map { $0.lowercased() } 25 | } 26 | let matchingSchemePaths: [String] = librarySchemePaths.filter { expectedSchemeNames.contains($0.fileNameWithoutExtension.lowercased()) } 27 | let matchingSchemeNames: [String] = matchingSchemePaths.map { $0.fileNameWithoutExtension } 28 | 29 | guard !librarySchemePaths.isEmpty else { 30 | print("No shared scheme(s) found; still resuming build.", level: .warning) 31 | return 32 | } 33 | 34 | if !matchingSchemePaths.isEmpty { 35 | let schemePathsToRemove: [String] = sharedSchemePaths.filter { !matchingSchemePaths.contains($0) } 36 | print("Found shared scheme(s) \(matchingSchemeNames) matching specified library – removing others: \(schemePathsToRemove.map { $0.fileNameWithoutExtension })", level: .verbose) 37 | 38 | for schemePathToRemove in schemePathsToRemove { 39 | try FileManager.default.removeItem(atPath: schemePathToRemove) 40 | } 41 | } else { 42 | print("No shared scheme(s) found matching library name '\(framework.libraryName)' – can't remove potentially unnecessary shared schemes, keeping all", level: .warning) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/XcodeProjectGeneratorServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class XcodeProjectGeneratorServiceTests: XCTestCase { 5 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 6 | 7 | private var singleLineCommentedManifestResourceWithStringSpecifiers: Resource { 8 | return Resource( 9 | url: testResourcesDir.appendingPathComponent("Package.swift"), 10 | contents: #"// platforms: [.iOS("12.0"), .macOS("10.12"), .tvOS("11.2"), .watchOS("3.0")],"# 11 | ) 12 | } 13 | 14 | private var multilineManifestResourceWithEnumCaseSpecifiers: Resource { 15 | return Resource( 16 | url: testResourcesDir.appendingPathComponent("Package.swift"), 17 | contents: """ 18 | platforms: [ 19 | .macOS(.v10_12), .iOS(.v12), .tvOS(.v11_2), 20 | ], 21 | """ 22 | ) 23 | } 24 | 25 | override func setUp() { 26 | super.setUp() 27 | 28 | clean() 29 | } 30 | 31 | private func clean() { 32 | try! bash("rm -rf '\(testResourcesDir.path)'") 33 | } 34 | 35 | func testPlatformToVersionWithSingleLineCommentedStringSpecifiers() { 36 | let projectGeneratorService = XcodeProjectGeneratorService() 37 | let framework = Framework(projectName: "X", libraryName: "X", projectDirectory: testResourcesDir.path, requiredFrameworks: []) 38 | 39 | resourcesLoaded([singleLineCommentedManifestResourceWithStringSpecifiers]) { 40 | let platformToVersion: [Platform: String] = try! projectGeneratorService.platformToVersion(framework: framework) 41 | 42 | XCTAssertEqual(platformToVersion[.iOS], "12.0") 43 | XCTAssertEqual(platformToVersion[.macOS], "10.12") 44 | XCTAssertEqual(platformToVersion[.tvOS], "11.2") 45 | XCTAssertEqual(platformToVersion[.watchOS], "3.0") 46 | } 47 | } 48 | 49 | func testPlatformToVersionWithMultiLineEnumCaseSpecifiers() { 50 | let projectGeneratorService = XcodeProjectGeneratorService() 51 | let framework = Framework(projectName: "X", libraryName: "X", projectDirectory: testResourcesDir.path, requiredFrameworks: []) 52 | 53 | resourcesLoaded([multilineManifestResourceWithEnumCaseSpecifiers]) { 54 | let platformToVersion: [Platform: String] = try! projectGeneratorService.platformToVersion(framework: framework) 55 | 56 | XCTAssertEqual(platformToVersion[.iOS], "12.0") 57 | XCTAssertEqual(platformToVersion[.macOS], "10.12") 58 | XCTAssertEqual(platformToVersion[.tvOS], "11.2") 59 | XCTAssertEqual(platformToVersion[.watchOS], "2.0") // default value for watchOS 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/DependencyResolverService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | enum DependencyResolverError: Error { 5 | case dependencyGraphGenerationFailed 6 | } 7 | 8 | final class DependencyResolverService { 9 | static let shared = DependencyResolverService(workingDirectory: GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) 10 | 11 | private let workingDirectory: String 12 | 13 | init(workingDirectory: String) { 14 | self.workingDirectory = workingDirectory 15 | } 16 | 17 | func resolveDependencies() throws { 18 | print("Resolving dependencies ...", level: .info) 19 | try bash("swift package \(contextSpecifiers()) resolve") 20 | } 21 | 22 | func updateDependencies() throws { 23 | print("Updating dependencies ...", level: .info) 24 | try bash("swift package \(contextSpecifiers()) update") 25 | } 26 | 27 | func dependencyGraph() throws -> DependencyGraph { 28 | print("Generating dependency graph ...", level: .info) 29 | let output = run(bash: "swift package \(contextSpecifiers()) show-dependencies --format json") 30 | 31 | guard output.exitcode == 0 else { 32 | print(output.stderror, level: .error) 33 | 34 | if output.stderror.contains("contains mixed language source files") { 35 | print( 36 | """ 37 | Please make sure that the 'path' of all targets in Package.swift are set to directories containing only Swift files. 38 | For additional details, please see here: https://github.com/JamitLabs/Accio/issues/3 39 | """, 40 | level: .warning 41 | ) 42 | } else if output.stderror.contains("multiple targets named") { 43 | print("This is a known issue. For more details, please see here: https://github.com/JamitLabs/Accio/issues/26", level: .warning) 44 | } 45 | 46 | throw DependencyResolverError.dependencyGraphGenerationFailed 47 | } 48 | 49 | let dependencyGraphJson: String = { 50 | if output.stdout.hasPrefix("{") { 51 | return output.stdout 52 | } else { 53 | let separator = "\n{" 54 | return separator + output.stdout.components(separatedBy: separator).dropFirst().joined(separator: separator) 55 | } 56 | }() 57 | 58 | print("Dependency graph JSON output is:\n\(dependencyGraphJson)", level: .verbose) 59 | return try JSONDecoder.swiftPM.decode(DependencyGraph.self, from: dependencyGraphJson.data(using: .utf8)!) 60 | } 61 | 62 | private func contextSpecifiers() -> String { 63 | return "--package-path '\(workingDirectory)' --build-path '\(workingDirectory)/\(Constants.buildPath)'" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/GitIgnoreIntegrationServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class GitIgnoreIntegrationServiceTests: XCTestCase { 5 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 6 | 7 | private var gitignore: Resource { 8 | return Resource( 9 | url: testResourcesDir.appendingPathComponent(".gitignore"), 10 | contents: """ 11 | ## Build generated 12 | build/ 13 | DerivedData/ 14 | """ 15 | ) 16 | } 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | try! bash("rm -rf '\(testResourcesDir.path)'") 22 | try! bash("mkdir '\(testResourcesDir.path)'") 23 | } 24 | 25 | func testAddIgnoreEntriesWithoutGitignore() { 26 | let gitignoreIntegrationService = GitIgnoreIntegrationService(workingDirectory: testResourcesDir.path) 27 | 28 | XCTAssertFalse(FileManager.default.fileExists(atPath: gitignore.url.path)) 29 | 30 | try! gitignoreIntegrationService.addIgnoreEntriesIfNeeded() 31 | 32 | XCTAssertTrue(FileManager.default.fileExists(atPath: gitignore.url.path)) 33 | XCTAssert(try! String(contentsOf: gitignore.url).contains("\n\(Constants.dependenciesPath)/\n")) 34 | XCTAssert(try! String(contentsOf: gitignore.url).contains("\n\(Constants.buildPath)/\n")) 35 | 36 | XCTAssertFalse(try! String(contentsOf: gitignore.url).hasPrefix("\n")) 37 | XCTAssertTrue(try! String(contentsOf: gitignore.url).hasSuffix("\n")) 38 | } 39 | 40 | func testAddIgnoreEntriesWithExistingGitignore() { 41 | let gitignoreIntegrationService = GitIgnoreIntegrationService(workingDirectory: testResourcesDir.path) 42 | 43 | resourcesLoaded([gitignore]) { 44 | XCTAssertFalse(try! String(contentsOf: gitignore.url).contains("\n\(Constants.dependenciesPath)/\n")) 45 | XCTAssertFalse(try! String(contentsOf: gitignore.url).contains("\n\(Constants.buildPath)/\n")) 46 | 47 | try! gitignoreIntegrationService.addIgnoreEntriesIfNeeded() 48 | 49 | XCTAssertTrue(try! String(contentsOf: gitignore.url).contains("\n\(Constants.dependenciesPath)/\n")) 50 | XCTAssertTrue(try! String(contentsOf: gitignore.url).contains("\n\(Constants.buildPath)/\n")) 51 | 52 | XCTAssertFalse(try! String(contentsOf: gitignore.url).hasPrefix("\n")) 53 | XCTAssertTrue(try! String(contentsOf: gitignore.url).hasSuffix("\n")) 54 | 55 | let gitignoreContentsAfterFirstExecution = try! String(contentsOf: gitignore.url) 56 | 57 | try! gitignoreIntegrationService.addIgnoreEntriesIfNeeded() 58 | try! gitignoreIntegrationService.addIgnoreEntriesIfNeeded() 59 | 60 | XCTAssertEqual(try! String(contentsOf: gitignore.url), gitignoreContentsAfterFirstExecution) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/SwiftVersionDetectorServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class SwiftVersionDetectorServiceTests: XCTestCase { 5 | func testGetCurrentSwiftVersion() { 6 | XCTAssertNoThrow(try SwiftVersionDetectorService.shared.getCurrentSwiftVersion()) 7 | } 8 | 9 | func testConvertToSwiftVersion() { 10 | typealias Test = (parsingShouldSucceed: Bool, swiftVersionOutput: String, expectedSwiftVersion: String) 11 | let tests: [Test] = [ 12 | Test( 13 | parsingShouldSucceed: true, 14 | swiftVersionOutput: """ 15 | Apple Swift version 5.1 (swiftlang-1100.0.43.3 clang-1100.0.26.3) 16 | Target: x86_64-apple-darwin18.6.0 17 | """, 18 | expectedSwiftVersion: "Swift-5.1" 19 | ), 20 | Test( 21 | parsingShouldSucceed: true, 22 | swiftVersionOutput: """ 23 | Apple Swift version 5.0.1 (swiftlang-1001.0.82.4 clang-1001.0.46.5) 24 | Target: x86_64-apple-darwin18.6.0 25 | """, 26 | expectedSwiftVersion: "Swift-5.0.1" 27 | ), 28 | Test( 29 | parsingShouldSucceed: true, 30 | swiftVersionOutput: """ 31 | Apple Swift version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1) 32 | Target: x86_64-apple-darwin18.6.0 33 | """, 34 | expectedSwiftVersion: "Swift-4.2.1" 35 | ), 36 | Test( 37 | parsingShouldSucceed: false, 38 | swiftVersionOutput: """ 39 | Apple Swift version 4.2.1 40 | """, 41 | expectedSwiftVersion: "" 42 | ), 43 | Test( 44 | parsingShouldSucceed: false, 45 | swiftVersionOutput: """ 46 | Apple Swift Version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1) 47 | Target: x86_64-apple-darwin18.6.0 48 | """, 49 | expectedSwiftVersion: "" 50 | ), 51 | ] 52 | 53 | for test in tests { 54 | guard test.parsingShouldSucceed else { 55 | XCTAssertThrowsError( 56 | try SwiftVersionDetectorService.shared.convertToSwiftVersion( 57 | swiftVersionOutput: test.swiftVersionOutput 58 | ) 59 | ) 60 | continue 61 | } 62 | 63 | do { 64 | let parsedSwiftVersion = try SwiftVersionDetectorService.shared.convertToSwiftVersion( 65 | swiftVersionOutput: test.swiftVersionOutput 66 | ) 67 | 68 | XCTAssertEqual(parsedSwiftVersion, test.expectedSwiftVersion) 69 | } 70 | catch { 71 | XCTFail("convertToSwiftVersion(swiftVersionOutput:) unexpectedly throwed") 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "AEXML", 6 | "repositoryURL": "https://github.com/tadija/AEXML", 7 | "state": { 8 | "branch": null, 9 | "revision": "e4d517844dd03dac557e35d77a8e9ab438de91a6", 10 | "version": "4.4.0" 11 | } 12 | }, 13 | { 14 | "package": "CryptoSwift", 15 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3a2acbb32ab68215ee1596ee6004da8e90c3721b", 19 | "version": "1.0.0" 20 | } 21 | }, 22 | { 23 | "package": "HandySwift", 24 | "repositoryURL": "https://github.com/Flinesoft/HandySwift.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "4225d7654f87886b09dddf52761fc898e4bfd137", 28 | "version": "3.0.0" 29 | } 30 | }, 31 | { 32 | "package": "PathKit", 33 | "repositoryURL": "https://github.com/kylef/PathKit", 34 | "state": { 35 | "branch": null, 36 | "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 37 | "version": "1.0.0" 38 | } 39 | }, 40 | { 41 | "package": "Rainbow", 42 | "repositoryURL": "https://github.com/onevcat/Rainbow.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", 46 | "version": "3.1.5" 47 | } 48 | }, 49 | { 50 | "package": "Shell", 51 | "repositoryURL": "https://github.com/tuist/Shell", 52 | "state": { 53 | "branch": null, 54 | "revision": "d38121f89401db902b0d0bfc30b987e2c84c254e", 55 | "version": "2.0.3" 56 | } 57 | }, 58 | { 59 | "package": "Spectre", 60 | "repositoryURL": "https://github.com/kylef/Spectre.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 64 | "version": "0.9.0" 65 | } 66 | }, 67 | { 68 | "package": "SwiftCLI", 69 | "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "ba2268e67c07b9f9cfbc0801385e6238b36255eb", 73 | "version": "5.3.2" 74 | } 75 | }, 76 | { 77 | "package": "SwiftShell", 78 | "repositoryURL": "https://github.com/kareman/SwiftShell.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "99680b2efc7c7dbcace1da0b3979d266f02e213c", 82 | "version": "5.1.0" 83 | } 84 | }, 85 | { 86 | "package": "XcodeProj", 87 | "repositoryURL": "https://github.com/tuist/xcodeproj.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "b951777f42e9acbfb8f19da623b43aaa604422f9", 91 | "version": "7.0.0" 92 | } 93 | } 94 | ] 95 | }, 96 | "version": 1 97 | } 98 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/ResolvedManifestCachingService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CryptoSwift 3 | 4 | final class ResolvedManifestCachingService { 5 | private let sharedCachePath: String? 6 | 7 | init(sharedCachePath: String?) { 8 | self.sharedCachePath = sharedCachePath 9 | } 10 | 11 | func cacheResolvedManifest(at url: URL, with cachedFrameworkProducts: [CachedFrameworkProduct]) throws { 12 | let resolvedManifestHash = try fileHash(at: url) 13 | let subpath = cacheFileSubPath(hash: resolvedManifestHash) 14 | 15 | if 16 | let sharedCachePath = sharedCachePath, 17 | FileManager.default.fileExists(atPath: URL(fileURLWithPath: sharedCachePath).deletingLastPathComponent().path) 18 | { 19 | let sharedCacheFileUrl = URL(fileURLWithPath: sharedCachePath).appendingPathComponent(subpath) 20 | try bash("mkdir -p '\(sharedCacheFileUrl.deletingLastPathComponent().path)'") 21 | 22 | let data = try JSONEncoder().encode(cachedFrameworkProducts) 23 | try data.write(to: sharedCacheFileUrl) 24 | print("Saved resolved manifest in shared cache.", level: .info) 25 | } else { 26 | let localCacheFileUrl = URL(fileURLWithPath: Constants.localCachePath).appendingPathComponent(subpath) 27 | try bash("mkdir -p '\(localCacheFileUrl.deletingLastPathComponent().path)'") 28 | 29 | let data = try JSONEncoder().encode(cachedFrameworkProducts) 30 | try data.write(to: localCacheFileUrl) 31 | print("Saved resolved manifest in local cache.", level: .info) 32 | } 33 | } 34 | 35 | func cachedFrameworkProducts(forResolvedManifestAt url: URL) throws -> [CachedFrameworkProduct]? { 36 | guard FileManager.default.fileExists(atPath: url.path) else { return nil } 37 | 38 | let resolvedManifestHash = try fileHash(at: url) 39 | let subpath = cacheFileSubPath(hash: resolvedManifestHash) 40 | let localCachedFileUrl = URL(fileURLWithPath: Constants.localCachePath).appendingPathComponent(subpath) 41 | 42 | if FileManager.default.fileExists(atPath: localCachedFileUrl.path) { 43 | print("Found cached resolved manifest results in local cache - trying to reuse cached build products.", level: .info) 44 | return try? JSONDecoder().decode([CachedFrameworkProduct].self, from: Data(contentsOf: localCachedFileUrl)) 45 | } 46 | 47 | if let sharedCachePath = sharedCachePath { 48 | let sharedCacheFileUrl = URL(fileURLWithPath: sharedCachePath).appendingPathComponent(subpath) 49 | 50 | if FileManager.default.fileExists(atPath: sharedCacheFileUrl.path) { 51 | print("Found cached resolved manifest results in shared cache - trying to reuse cached build products.", level: .info) 52 | return try? JSONDecoder().decode([CachedFrameworkProduct].self, from: Data(contentsOf: sharedCacheFileUrl)) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | private func fileHash(at url: URL) throws -> String { 60 | return try Data(contentsOf: url).sha1().toHexString() 61 | } 62 | 63 | private func cacheFileSubPath(hash: String) -> String { 64 | return "ResolvedManifests/\(hash).json" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/Framework.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | enum FrameworkError: Error { 5 | case noSharedSchemes 6 | } 7 | 8 | struct Framework: Equatable { 9 | let projectName: String 10 | let libraryName: String 11 | let projectDirectory: String 12 | let requiredFrameworks: [Framework] 13 | 14 | var commitHash: String { 15 | if TestHelper.shared.isStartedByUnitTests && projectDirectory.isEmpty { 16 | return "PSEUDO_HASH_\(libraryName)" 17 | } 18 | 19 | return run(bash: "git -C '\(projectDirectory)' rev-parse HEAD").stdout 20 | } 21 | 22 | var generatedXcodeProjectPath: String { 23 | return URL(fileURLWithPath: projectDirectory).appendingPathComponent("\(projectName).xcodeproj").path 24 | } 25 | 26 | // Returns whether the framework is a local repo (instead of a checked out repo stored in the `.accio/` folder) 27 | var isLocalRepo: Bool { 28 | return !projectDirectory.contains("/\(Constants.buildPath)/") 29 | } 30 | 31 | func xcodeProjectPaths(in directory: String) throws -> [String] { 32 | let directoryUrl: URL = URL(fileURLWithPath: directory) 33 | let visibleContentNames: [String] = try FileManager.default.contentsOfDirectory(atPath: directoryUrl.path).filter { !$0.hasPrefix(".") } 34 | let visibleContentPaths: [String] = visibleContentNames.map { directoryUrl.appendingPathComponent($0).path } 35 | 36 | let directoryPaths: [String] = try visibleContentPaths.filter { try FileManager.default.isDirectory(atPath: $0) && !pathIsProjectFile($0) } 37 | let projectFilePaths: [String] = visibleContentPaths.filter { pathIsProjectFile($0) && !$0.isAliasFile } 38 | 39 | let projectFilePathsInDirectories: [String] = try directoryPaths.reduce([]) { $0 + (try xcodeProjectPaths(in: $1)) } 40 | let xcodeProjectFilePaths: [String] = projectFilePaths + projectFilePathsInDirectories 41 | return xcodeProjectFilePaths.filter { !$0.hasSuffix(".playground/playground.xcworkspace") } 42 | } 43 | 44 | func sharedSchemePaths() throws -> [String] { 45 | return try xcodeProjectPaths(in: projectDirectory).reduce([]) { result, xcodeProjectPath in 46 | let schemesDirUrl: URL = URL(fileURLWithPath: xcodeProjectPath).appendingPathComponent("xcshareddata/xcschemes") 47 | guard FileManager.default.fileExists(atPath: schemesDirUrl.path) else { return result } 48 | 49 | let sharedSchemeFileNames: [String] = try FileManager.default.contentsOfDirectory(atPath: schemesDirUrl.path).filter { $0.hasSuffix(".xcscheme") } 50 | return result + sharedSchemeFileNames.map { schemesDirUrl.appendingPathComponent($0).path } 51 | } 52 | } 53 | 54 | func librarySchemePaths(in schemePaths: [String], framework: Framework) -> [String] { 55 | let nonLibrarySchemeSubstrings: [String] = ["Example", "Demo", "Sample", "Tests"] 56 | return schemePaths.filter { schemePath in 57 | let relativeSchemePath = schemePath.replacingOccurrences(of: framework.projectDirectory, with: "") 58 | return !nonLibrarySchemeSubstrings.contains { relativeSchemePath.contains($0) } 59 | } 60 | } 61 | 62 | private func pathIsProjectFile(_ path: String) -> Bool { 63 | return path.hasSuffix(".xcodeproj") || path.hasSuffix(".xcworkspace") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Models/AppTargetTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class AppTargetTests: XCTestCase { 5 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 6 | 7 | private var manifestResource: Resource { 8 | return Resource( 9 | url: testResourcesDir.appendingPathComponent("Package.swift"), 10 | contents: """ 11 | // swift-tools-version:4.2 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "TestProject", 16 | products: [], 17 | dependencies: [ 18 | .package(url: "https://github.com/Flinesoft/HandySwift.git", .upToNextMajor(from: "2.8.0")), 19 | .package(url: "https://github.com/Flinesoft/HandyUIKit.git", .upToNextMajor(from: "1.9.0")), 20 | .package(url: "https://github.com/Flinesoft/Imperio.git", .upToNextMajor(from: "3.0.0")), 21 | .package(url: "https://github.com/JamitLabs/MungoHealer.git", .upToNextMajor(from: "0.3.0")), 22 | .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "13.0.1")), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "TestProject-iOS", 27 | dependencies: [ 28 | "HandySwift", 29 | "HandyUIKit", 30 | "Imperio", 31 | "MungoHealer", 32 | "Moya", 33 | ], 34 | path: "TestProject-iOS" 35 | ) 36 | ] 37 | ) 38 | 39 | """ 40 | ) 41 | } 42 | 43 | private var xcodeProjectResource: Resource { 44 | return Resource( 45 | url: testResourcesDir.appendingPathComponent("TestProject.xcodeproj/project.pbxproj"), 46 | contents: ResourceData.iOSProjectFileContents 47 | ) 48 | } 49 | 50 | private var exampleSwiftFile: Resource { 51 | return Resource( 52 | url: testResourcesDir.appendingPathComponent("TestProject-iOS/Example.swift"), 53 | contents: "class Example {}" 54 | ) 55 | } 56 | 57 | override func setUp() { 58 | super.setUp() 59 | 60 | try! bash("rm -rf '\(testResourcesDir.path)'") 61 | try! bash("mkdir '\(testResourcesDir.path)'") 62 | } 63 | 64 | func testFrameworkDependencies() { 65 | resourcesLoaded([manifestResource, xcodeProjectResource, exampleSwiftFile]) { 66 | let manifest = try! ManifestHandlerService(workingDirectory: testResourcesDir.path).loadManifest(isDependency: false) 67 | let dependencyGraph = try! DependencyResolverService(workingDirectory: testResourcesDir.path).dependencyGraph() 68 | let appTarget = try! manifest.appTargets(workingDirectory: testResourcesDir.path).first! 69 | 70 | let frameworks: [Framework] = try! appTarget.frameworkDependencies(manifest: manifest, dependencyGraph: dependencyGraph).flattenedDeepFirstOrder() 71 | XCTAssertEqual(frameworks.map { $0.libraryName }, ["HandySwift", "HandyUIKit", "Imperio", "MungoHealer", "Alamofire", "Result", "Moya"]) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at cihat.guenduez@jamitlabs.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Models/FrameworkTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class FrameworkTests: XCTestCase { 5 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 6 | 7 | private var manifestResource: Resource { 8 | return Resource( 9 | url: testResourcesDir.appendingPathComponent("Package.swift"), 10 | contents: """ 11 | // swift-tools-version:4.2 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "TestProject", 16 | products: [], 17 | dependencies: [ 18 | .package(url: "https://github.com/Flinesoft/HandySwift.git", .upToNextMajor(from: "2.8.0")), 19 | .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "13.0.1")), 20 | .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "4.0.0")), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "TestProject-iOS", 25 | dependencies: [ 26 | "HandySwift", 27 | "Moya", 28 | "RxSwift", 29 | ], 30 | path: "TestProject-iOS" 31 | ) 32 | ] 33 | ) 34 | 35 | """ 36 | ) 37 | } 38 | 39 | private var xcodeProjectResource: Resource { 40 | return Resource( 41 | url: testResourcesDir.appendingPathComponent("TestProject.xcodeproj/project.pbxproj"), 42 | contents: ResourceData.iOSProjectFileContents 43 | ) 44 | } 45 | 46 | private var exampleSwiftFile: Resource { 47 | return Resource( 48 | url: testResourcesDir.appendingPathComponent("TestProject-iOS/Example.swift"), 49 | contents: "class Example {}" 50 | ) 51 | } 52 | 53 | override func setUp() { 54 | super.setUp() 55 | 56 | try! bash("rm -rf '\(testResourcesDir.path)'") 57 | try! bash("mkdir '\(testResourcesDir.path)'") 58 | } 59 | 60 | func testXcodeProjectPaths() { 61 | resourcesLoaded([manifestResource, xcodeProjectResource, exampleSwiftFile]) { 62 | let manifest = try! ManifestHandlerService(workingDirectory: testResourcesDir.path).loadManifest(isDependency: false) 63 | let dependencyGraph = try! DependencyResolverService(workingDirectory: testResourcesDir.path).dependencyGraph() 64 | let appTarget = try! manifest.appTargets(workingDirectory: testResourcesDir.path).first! 65 | 66 | let frameworks: [Framework] = try! appTarget.frameworkDependencies(manifest: manifest, dependencyGraph: dependencyGraph).flattenedDeepFirstOrder() 67 | let rxFramework: Framework = frameworks.last! 68 | 69 | XCTAssertEqual(rxFramework.libraryName, "RxSwift") 70 | XCTAssertEqual( 71 | try! rxFramework.xcodeProjectPaths(in: rxFramework.projectDirectory).sorted(), 72 | [ 73 | "\(rxFramework.projectDirectory)/Preprocessor/Preprocessor.xcodeproj", 74 | "\(rxFramework.projectDirectory)/Rx.xcodeproj", 75 | "\(rxFramework.projectDirectory)/Rx.xcworkspace", 76 | "\(rxFramework.projectDirectory)/RxExample/RxExample.xcodeproj", 77 | ] 78 | ) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/InstallationTypeDetectorServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class InstallationTypeDetectorServiceTests: XCTestCase { 5 | private let manifestContents: String = """ 6 | // swift-tools-version:4.2 7 | import PackageDescription 8 | 9 | let package = Package( 10 | name: "App", 11 | products: [], 12 | dependencies: [ 13 | .package(url: "https://github.com/mw99/DataCompression.git", .upToNextMajor(from: "3.3.0")), 14 | .package(url: "https://github.com/Flinesoft/HandySwift.git", .upToNextMajor(from: "2.8.0")), 15 | .package(url: "https://github.com/Flinesoft/HandyUIKit.git", .upToNextMajor(from: "1.9.0")), 16 | .package(url: "https://github.com/Flinesoft/Imperio.git", .upToNextMajor(from: "3.0.0")), 17 | .package(url: "https://github.com/JamitLabs/MungoHealer.git", .upToNextMajor(from: "0.3.0")), 18 | .package(url: "https://github.com/SwiftyBeaver/SwiftyBeaver.git", .upToNextMajor(from: "1.6.2")), 19 | .package(url: "https://github.com/Dschee/WheelPicker.git", .branch("master")), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "App", 24 | dependencies: [ 25 | "DataCompression", 26 | "HandySwift", 27 | "HandyUIKit", 28 | "Imperio", 29 | "MungoHealer", 30 | "SwiftyBeaver", 31 | "WheelPicker" 32 | ], 33 | path: "App" 34 | ) 35 | ] 36 | ) 37 | 38 | """ 39 | 40 | let testResourcesDir = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 41 | 42 | override func setUp() { 43 | super.setUp() 44 | 45 | try! bash("mkdir -p '\(testResourcesDir.path)'") 46 | 47 | FileManager.default.createFile(atPath: testResourcesDir.appendingPathComponent("Package.swift").path, contents: manifestContents.data(using: .utf8)) 48 | try! DependencyResolverService(workingDirectory: testResourcesDir.path).resolveDependencies() 49 | } 50 | 51 | func testDetectInstallationTypeTests() { 52 | let expectedTypes: [String: InstallationType] = [ 53 | "DataCompression": InstallationType.carthage, 54 | "HandySwift": InstallationType.carthage, 55 | "HandyUIKit": InstallationType.carthage, 56 | "Imperio": InstallationType.carthage, 57 | "MungoHealer": InstallationType.carthage, 58 | "SwiftyBeaver": InstallationType.carthage, 59 | "WheelPicker": InstallationType.swiftPackageManager 60 | ] 61 | 62 | let checkoutsDir = testResourcesDir.appendingPathComponent(".accio/checkouts") 63 | 64 | for (frameworkName, expectedInstallationType) in expectedTypes { 65 | let frameworkDirName = try! FileManager.default.contentsOfDirectory(atPath: checkoutsDir.path).first { $0.hasPrefix(frameworkName) }! 66 | let frameworkDir = checkoutsDir.appendingPathComponent(frameworkDirName) 67 | 68 | let framework = Framework(projectName: "TestProject", libraryName: frameworkName, projectDirectory: frameworkDir.path, requiredFrameworks: []) 69 | 70 | let installationType = try! InstallationTypeDetectorService.shared.detectInstallationType(for: framework) 71 | XCTAssertEqual(installationType, expectedInstallationType, "Expected \(frameworkName) to be of type \(expectedInstallationType).") 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/ManifestHandlerService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftShell 3 | 4 | final class ManifestHandlerService { 5 | static let shared = ManifestHandlerService(workingDirectory: GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) 6 | 7 | private let workingDirectory: String 8 | 9 | init(workingDirectory: String) { 10 | self.workingDirectory = workingDirectory 11 | } 12 | 13 | func loadManifest(isDependency: Bool) throws -> Manifest { 14 | print("Reading package manifest at \(workingDirectory)/Package.swift ...", level: isDependency ? .verbose : .info) 15 | let contextSpecifiers = "--package-path '\(workingDirectory)' --build-path '\(workingDirectory)/\(Constants.buildPath)'" 16 | let manifestJson = run(bash: "swift package \(contextSpecifiers) dump-package").stdout 17 | return try JSONDecoder.swiftPM.decode(Manifest.self, from: manifestJson.data(using: .utf8)!) 18 | } 19 | 20 | func createManifestFromDefaultTemplateIfNeeded(projectName: String, targetNames: [String]) throws { 21 | let packageManifestPath = URL(fileURLWithPath: workingDirectory).appendingPathComponent("Package.swift").path 22 | 23 | if FileManager.default.fileExists(atPath: packageManifestPath) { 24 | guard let manifestContents = try? String(contentsOfFile: packageManifestPath), manifestContents.isBlank else { 25 | print("A non-empty Package.swift file already exists, skipping template based creation.", level: .warning) 26 | return 27 | } 28 | 29 | try FileManager.default.removeItem(atPath: packageManifestPath) 30 | } 31 | 32 | let targetsContents = try self.targetsContents(workingDirectory: workingDirectory, projectName: projectName, targetNames: targetNames) 33 | let manifestTemplate = self.manifestTemplate(projectName: projectName, targetsContents: targetsContents) 34 | 35 | FileManager.default.createFile(atPath: packageManifestPath, contents: manifestTemplate.data(using: .utf8), attributes: nil) 36 | print("Created manifest file Package.swift from template.", level: .info) 37 | } 38 | 39 | private func manifestTemplate(projectName: String, targetsContents: String) -> String { 40 | return """ 41 | // swift-tools-version:5.0 42 | import PackageDescription 43 | 44 | let package = Package( 45 | name: \"\(projectName)\", 46 | products: [], 47 | dependencies: [ 48 | // add your dependencies here, for example: 49 | // .package(url: \"https://github.com/User/Project.git\", .upToNextMajor(from: \"1.0.0\")), 50 | ], 51 | targets: [ 52 | \(targetsContents) ] 53 | ) 54 | 55 | """ 56 | } 57 | 58 | private func targetsContents(workingDirectory: String, projectName: String, targetNames: [String]) throws -> String { 59 | return try targetNames.reduce("") { result, targetName in 60 | let targetTypeDetectorService = TargetTypeDetectorService(workingDirectory: workingDirectory) 61 | let targetType: AppTarget.TargetType = try targetTypeDetectorService.detectTargetType(ofTarget: targetName, in: projectName) 62 | return result + """ 63 | .\(targetType.packageSpecifier)( 64 | name: \"\(targetName)\", 65 | dependencies: [ 66 | // add your dependencies scheme names here, for example: 67 | // \"Project\", 68 | ], 69 | path: \"\(targetName)\" 70 | ), 71 | 72 | """ 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/PlatformDetectorServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class PlatformDetectorServiceTests: XCTestCase { 5 | private let testProjectsDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestProjects") 6 | 7 | private var iOSTestProjectFile: Resource { 8 | return Resource( 9 | url: testProjectsDir.appendingPathComponent("TestProject-iOS.xcodeproj/project.pbxproj"), 10 | contents: ResourceData.iOSProjectFileContents 11 | ) 12 | } 13 | 14 | private var macOSTestProjectFile: Resource { 15 | return Resource( 16 | url: testProjectsDir.appendingPathComponent("TestProject-macOS.xcodeproj/project.pbxproj"), 17 | contents: ResourceData.macOSProjectFileContents 18 | ) 19 | } 20 | 21 | private var tvOSTestProjectFile: Resource { 22 | return Resource( 23 | url: testProjectsDir.appendingPathComponent("TestProject-tvOS.xcodeproj/project.pbxproj"), 24 | contents: ResourceData.tvOSProjectFileContents 25 | ) 26 | } 27 | 28 | private var watchOSTestProjectFile: Resource { 29 | return Resource( 30 | url: testProjectsDir.appendingPathComponent("TestProject-watchOS.xcodeproj/project.pbxproj"), 31 | contents: ResourceData.watchOSProjectFileContents 32 | ) 33 | } 34 | 35 | func testDetectPlatformWithDifferentPlatforms() { 36 | resourcesLoaded([iOSTestProjectFile]) { 37 | let xcodeProjectUrl = iOSTestProjectFile.url.deletingLastPathComponent() 38 | let platformDetectorService = PlatformDetectorService(workingDirectory: xcodeProjectUrl.deletingLastPathComponent().path) 39 | let appTarget = AppTarget(projectName: "TestProject-iOS", targetName: "TestProject-iOS", dependentLibraryNames: [], targetType: .app) 40 | let detectedPlatform = try! platformDetectorService.detectPlatform(of: appTarget) 41 | XCTAssertEqual(detectedPlatform, Platform.iOS) 42 | } 43 | 44 | resourcesLoaded([macOSTestProjectFile]) { 45 | let xcodeProjectUrl = macOSTestProjectFile.url.deletingLastPathComponent() 46 | let platformDetectorService = PlatformDetectorService(workingDirectory: xcodeProjectUrl.deletingLastPathComponent().path) 47 | let appTarget = AppTarget(projectName: "TestProject-macOS", targetName: "TestProject-macOS", dependentLibraryNames: [], targetType: .app) 48 | let detectedPlatform = try! platformDetectorService.detectPlatform(of: appTarget) 49 | XCTAssertEqual(detectedPlatform, Platform.macOS) 50 | } 51 | 52 | resourcesLoaded([tvOSTestProjectFile]) { 53 | let xcodeProjectUrl = tvOSTestProjectFile.url.deletingLastPathComponent() 54 | let platformDetectorService = PlatformDetectorService(workingDirectory: xcodeProjectUrl.deletingLastPathComponent().path) 55 | let appTarget = AppTarget(projectName: "TestProject-tvOS", targetName: "TestProject-tvOS", dependentLibraryNames: [], targetType: .app) 56 | let detectedPlatform = try! platformDetectorService.detectPlatform(of: appTarget) 57 | XCTAssertEqual(detectedPlatform, Platform.tvOS) 58 | } 59 | 60 | resourcesLoaded([watchOSTestProjectFile]) { 61 | let xcodeProjectUrl = watchOSTestProjectFile.url.deletingLastPathComponent() 62 | let platformDetectorService = PlatformDetectorService(workingDirectory: xcodeProjectUrl.deletingLastPathComponent().path) 63 | let appTarget = AppTarget(projectName: "TestProject-watchOS", targetName: "TestProject-watchOS WatchKit App", dependentLibraryNames: [], targetType: .app) 64 | let detectedPlatform = try! platformDetectorService.detectPlatform(of: appTarget) 65 | XCTAssertEqual(detectedPlatform, Platform.watchOS) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/CarthageBuilderService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum CarthageBuilderError: Error { 4 | case buildProductsMissing 5 | case requiredBuildProductsMissing 6 | } 7 | 8 | final class CarthageBuilderService { 9 | private let frameworkCachingService: FrameworkCachingService 10 | 11 | init(frameworkCachingService: FrameworkCachingService) { 12 | self.frameworkCachingService = frameworkCachingService 13 | } 14 | 15 | func build( 16 | framework: Framework, 17 | platform: Platform, 18 | swiftVersion: String, 19 | alreadyBuiltFrameworkProducts: [FrameworkProduct] 20 | ) throws -> FrameworkProduct { 21 | print("Building library \(framework.libraryName) with Carthage ...", level: .info) 22 | 23 | // link already built subdependencies from previous calls of this method 24 | for requiredFramework in framework.requiredFrameworks.flattenedDeepFirstOrder() { 25 | guard let requiredFrameworkProduct = alreadyBuiltFrameworkProducts.first(where: { $0.libraryName == requiredFramework.libraryName }) else { 26 | print("Could not find required framework '\(requiredFramework.libraryName)'s build products in already built frameworks.", level: .error) 27 | throw CarthageBuilderError.requiredBuildProductsMissing 28 | } 29 | 30 | let productsTargetDirectoryUrl = URL(fileURLWithPath: framework.projectDirectory).appendingPathComponent("Carthage/Build/\(platform.carthageBuildFolderName)") 31 | 32 | print("Linking required frameworks build products '\(requiredFrameworkProduct.frameworkDirPath)(.dSYM)' into directory '\(productsTargetDirectoryUrl.path)' ...", level: .verbose) 33 | 34 | try bash("mkdir -p '\(productsTargetDirectoryUrl.path)'") 35 | try bash("ln -f -s '\(requiredFrameworkProduct.frameworkDirPath)' '\(productsTargetDirectoryUrl.path)'") 36 | try bash("ln -f -s '\(requiredFrameworkProduct.symbolsFilePath)' '\(productsTargetDirectoryUrl.path)'") 37 | } 38 | 39 | // remove Cartfile before `carthage build` command as subdependencies have already been built via Accio 40 | try bash("rm -rf '\(framework.projectDirectory)/Cartfile'") 41 | try bash("rm -rf '\(framework.projectDirectory)/Cartfile.resolved'") 42 | 43 | try XcodeProjectSchemeHandlerService.shared.removeUnnecessarySharedSchemes(from: framework, platform: platform) 44 | 45 | try bash("/usr/local/bin/carthage build --project-directory '\(framework.projectDirectory)' --platform \(platform.rawValue) --no-skip-current --no-use-binaries") 46 | 47 | let frameworkProduct = FrameworkProduct(libraryName: framework.libraryName, platformName: platform.rawValue, commitHash: framework.commitHash) 48 | let platformBuildDir = "\(framework.projectDirectory)/Carthage/Build/\(platform.carthageBuildFolderName)" 49 | 50 | try bash("mkdir -p '\(frameworkProduct.frameworkDirUrl.deletingLastPathComponent().path)'") 51 | try bash("cp -R '\(platformBuildDir)/\(framework.libraryName).framework' '\(frameworkProduct.frameworkDirPath)'") 52 | try bash("cp -R '\(platformBuildDir)/\(framework.libraryName).framework.dSYM' '\(frameworkProduct.symbolsFilePath)'") 53 | 54 | // revert any changes to prevent issues when removing checked out dependency 55 | try bash("rm -rf '\(framework.projectDirectory)/Carthage/Build'") 56 | 57 | if !framework.isLocalRepo { 58 | try GitResetService.shared.resetGit(atPath: framework.projectDirectory) 59 | } 60 | 61 | guard FileManager.default.fileExists(atPath: frameworkProduct.frameworkDirPath) && FileManager.default.fileExists(atPath: frameworkProduct.symbolsFilePath) else { 62 | print("Failed to build products to \(platformBuildDir)/\(framework.libraryName).framework(.dSYM).", level: .error) 63 | throw CarthageBuilderError.buildProductsMissing 64 | } 65 | 66 | try frameworkProduct.cleanupRecursiveFrameworkIfNeeded() 67 | 68 | print("Completed building scheme \(framework.libraryName) with Carthage.", level: .info) 69 | try frameworkCachingService.cache(product: frameworkProduct, framework: framework, platform: platform, swiftVersion: swiftVersion) 70 | 71 | return frameworkProduct 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/AccioKit/Models/Manifest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum ManifestError: Error { 4 | case libraryNotFound 5 | } 6 | 7 | class Manifest: Decodable { 8 | // MARK: - Sub Types 9 | struct Target: Decodable { 10 | struct Dependency { 11 | let names: [String] 12 | } 13 | 14 | let name: String 15 | let type: String 16 | let dependencies: [Dependency] 17 | } 18 | 19 | struct Product: Decodable { 20 | struct ProductType: Decodable { 21 | let library: [String]? 22 | } 23 | 24 | let name: String 25 | let type: ProductType 26 | let targets: [String] 27 | } 28 | 29 | // MARK: - Properties 30 | let name: String 31 | let products: [Product] 32 | let targets: [Target] 33 | } 34 | 35 | extension Manifest { 36 | func appTargets(workingDirectory: String = GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) throws -> [AppTarget] { 37 | return try targets.compactMap { 38 | var targetType: AppTarget.TargetType? 39 | 40 | switch $0.type { 41 | case AppTarget.TargetType.test.rawValue: 42 | targetType = AppTarget.TargetType.test 43 | 44 | default: 45 | let targetTypeDetectorService = TargetTypeDetectorService(workingDirectory: workingDirectory) 46 | targetType = try targetTypeDetectorService.detectTargetType(ofTarget: $0.name, in: name) 47 | 48 | if targetType! == .test { 49 | print("Unexpectedly found '\(targetType!.wrapperExtension)' wrapper extension product for non-test target '\($0.name)'.", level: .warning) 50 | } 51 | } 52 | 53 | return AppTarget(projectName: name, targetName: $0.name, dependentLibraryNames: $0.dependencies.flatMap { $0.names }, targetType: targetType!) 54 | } 55 | } 56 | 57 | func frameworkDependencies(ofLibrary libraryName: String, dependencyGraph: DependencyGraph) throws -> [Framework] { 58 | let libraryProducts: [Product] = products.filter { $0.type.library != nil && !$0.type.library!.isEmpty } 59 | 60 | guard let product: Product = libraryProducts.first(where: { $0.name == libraryName }) else { 61 | print("Manifest: Could not find library product with name '\(libraryName)' in package manifest for project '\(name)'.", level: .error) 62 | throw ManifestError.libraryNotFound 63 | } 64 | 65 | let productsTargets: [Target] = targets.filter { product.targets.contains($0.name) } 66 | let dependencyNames: [String] = Array(Set(productsTargets.flatMap { $0.dependencies.flatMap { $0.names } })).sorted() 67 | 68 | let projectTargetNames: [String] = targets.map { $0.name } 69 | let projectProductNames: [String] = products.map { $0.name } 70 | 71 | let dependencyInternalLibraryNames: [String] = dependencyNames.filter { projectProductNames.contains($0) } 72 | let dependencyExternalLibraryNames: [String] = dependencyNames.filter { !projectTargetNames.contains($0) } 73 | 74 | let libraryNames: [String] = dependencyInternalLibraryNames + dependencyExternalLibraryNames 75 | return try libraryNames.map { try dependencyGraph.framework(libraryName: $0) } 76 | } 77 | } 78 | 79 | extension Manifest.Target.Dependency: Decodable { 80 | enum ManifestTargetDependencyParsingError: Error { case error } 81 | enum CodingKeys: String, CodingKey { 82 | case byName 83 | case target 84 | case product 85 | } 86 | 87 | init(from decoder: Decoder) throws { 88 | let container = try decoder.container(keyedBy: CodingKeys.self) 89 | 90 | if let byName = try container.decodeIfPresent([String?].self, forKey: .byName) { 91 | names = byName.compactMap { $0 } 92 | } else if let target = try container.decodeIfPresent([String?].self, forKey: .target) { 93 | names = target.compactMap { $0 } 94 | } else if let product = try container.decodeIfPresent([String?].self, forKey: .product) { 95 | names = product.compactMap { $0 } 96 | } else { 97 | throw ManifestTargetDependencyParsingError.error 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/ResolvedManifestCachingServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import CryptoSwift 3 | import XCTest 4 | 5 | class ResolvedManifestCachingServiceTests: XCTestCase { 6 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 7 | 8 | private var cachedResolvedManifestFileUrl: URL { 9 | Constants.useTestPaths = true 10 | let hash: String = resolvedManifestResource.contents.sha1() 11 | return URL(fileURLWithPath: Constants.localCachePath).appendingPathComponent("ResolvedManifests/\(hash).json") 12 | } 13 | 14 | private var resolvedManifestResource: Resource { 15 | return Resource( 16 | url: testResourcesDir.appendingPathComponent("Package.resolved"), 17 | contents: """ 18 | { 19 | "object": { 20 | "pins": [ 21 | { 22 | "package": "HandySwift", 23 | "repositoryURL": "https://github.com/Flinesoft/HandySwift.git", 24 | "state": { 25 | "branch": null, 26 | "revision": "f736ec0ab264269cd4df91d6a685b4c78292cd76", 27 | "version": "2.8.0" 28 | } 29 | }, 30 | { 31 | "package": "HandyUIKit", 32 | "repositoryURL": "https://github.com/Flinesoft/HandyUIKit.git", 33 | "state": { 34 | "branch": null, 35 | "revision": "9b56780efbc48dd372647729b9750b94b6c47561", 36 | "version": "1.9.1" 37 | } 38 | }, 39 | { 40 | "package": "Imperio", 41 | "repositoryURL": "https://github.com/Flinesoft/Imperio.git", 42 | "state": { 43 | "branch": null, 44 | "revision": "238b9bc7de239d3e99a03ba94157d49a4ecd8b61", 45 | "version": "3.0.2" 46 | } 47 | }, 48 | { 49 | "package": "MungoHealer", 50 | "repositoryURL": "https://github.com/JamitLabs/MungoHealer.git", 51 | "state": { 52 | "branch": null, 53 | "revision": "132e5e454298958f60ca6f1f34c733bc41f299e0", 54 | "version": "0.3.2" 55 | } 56 | }, 57 | { 58 | "package": "SwiftyBeaver", 59 | "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver.git", 60 | "state": { 61 | "branch": null, 62 | "revision": "ad66cc41c5d8acbd63d9dcdc9d3609f152e08ed1", 63 | "version": "1.7.0" 64 | } 65 | } 66 | ] 67 | }, 68 | "version": 1 69 | } 70 | 71 | """ 72 | ) 73 | } 74 | 75 | override func setUp() { 76 | super.setUp() 77 | 78 | Constants.useTestPaths = true 79 | 80 | try! bash("rm -rf '\(testResourcesDir.path)'") 81 | try! bash("mkdir '\(testResourcesDir.path)'") 82 | 83 | try! bash("rm -rf '\(Constants.localCachePath)'") 84 | try! bash("mkdir '\(Constants.localCachePath)'") 85 | } 86 | 87 | func testCacheResolvedManifest() { 88 | let checkCachedResolvedManifestExistence: () -> Bool = { 89 | return FileManager.default.fileExists(atPath: self.cachedResolvedManifestFileUrl.path) 90 | } 91 | 92 | resourcesLoaded([resolvedManifestResource]) { 93 | XCTAssertFalse(checkCachedResolvedManifestExistence()) 94 | 95 | try! ResolvedManifestCachingService(sharedCachePath: nil).cacheResolvedManifest( 96 | at: resolvedManifestResource.url, 97 | with: [] 98 | ) 99 | 100 | XCTAssertTrue(checkCachedResolvedManifestExistence()) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Models/ManifestTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class ManifestTests: XCTestCase { 5 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 6 | 7 | private var manifestResource: Resource { 8 | return Resource( 9 | url: testResourcesDir.appendingPathComponent("Package.swift"), 10 | contents: """ 11 | // swift-tools-version:4.2 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "TestProject", 16 | products: [], 17 | dependencies: [ 18 | .package(url: "https://github.com/Flinesoft/HandySwift.git", .upToNextMajor(from: "2.8.0")), 19 | .package(url: "https://github.com/Flinesoft/HandyUIKit.git", .upToNextMajor(from: "1.9.0")), 20 | .package(url: "https://github.com/Flinesoft/Imperio.git", .upToNextMajor(from: "3.0.0")), 21 | .package(url: "https://github.com/JamitLabs/MungoHealer.git", .upToNextMajor(from: "0.3.0")), 22 | .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "13.0.1")), 23 | .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "1.0.0")), 24 | .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "7.0.0")), 25 | ], 26 | targets: [ 27 | .target( 28 | name: "TestProject-iOS", 29 | dependencies: [ 30 | "HandySwift", 31 | "HandyUIKit", 32 | "Imperio", 33 | "MungoHealer", 34 | "Moya", 35 | ], 36 | path: "TestProject-iOS" 37 | ), 38 | .testTarget( 39 | name: "TestProject-iOS Tests", 40 | dependencies: [ 41 | "Quick", 42 | "Nimble" 43 | ], 44 | path: "Tests" 45 | ), 46 | ] 47 | ) 48 | 49 | """ 50 | ) 51 | } 52 | 53 | private var xcodeProjectResource: Resource { 54 | return Resource( 55 | url: testResourcesDir.appendingPathComponent("TestProject.xcodeproj/project.pbxproj"), 56 | contents: ResourceData.iOSProjectFileContents 57 | ) 58 | } 59 | 60 | private var exampleSwiftFile: Resource { 61 | return Resource( 62 | url: testResourcesDir.appendingPathComponent("TestProject-iOS/Example.swift"), 63 | contents: "class Example {}" 64 | ) 65 | } 66 | 67 | private var exampleSwiftTestFile: Resource { 68 | return Resource( 69 | url: testResourcesDir.appendingPathComponent("TestProject-iOS Tests/ExampleTests.swift"), 70 | contents: "import XCTest\n\nclass ExampleTests: XCTestCase {}" 71 | ) 72 | } 73 | 74 | override func setUp() { 75 | super.setUp() 76 | 77 | try! bash("rm -rf '\(testResourcesDir.path)'") 78 | try! bash("mkdir '\(testResourcesDir.path)'") 79 | } 80 | 81 | func testAppTargets() { 82 | resourcesLoaded([manifestResource, xcodeProjectResource, exampleSwiftFile, exampleSwiftTestFile]) { 83 | let manifest = try! ManifestHandlerService(workingDirectory: testResourcesDir.path).loadManifest(isDependency: false) 84 | 85 | let appTargets = try! manifest.appTargets(workingDirectory: testResourcesDir.path) 86 | XCTAssertEqual(appTargets.count, 2) 87 | 88 | XCTAssertEqual(appTargets[0].projectName, "TestProject") 89 | XCTAssertEqual(appTargets[0].targetName, "TestProject-iOS") 90 | XCTAssertEqual(appTargets[0].dependentLibraryNames, ["HandySwift", "HandyUIKit", "Imperio", "MungoHealer", "Moya"]) 91 | 92 | XCTAssertEqual(appTargets[1].projectName, "TestProject") 93 | XCTAssertEqual(appTargets[1].targetName, "TestProject-iOS Tests") 94 | XCTAssertEqual(appTargets[1].dependentLibraryNames, ["Quick", "Nimble"]) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/CachedBuilderService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum CachedBuilderServiceError: Error { 4 | case unableToRetrieveSwiftVersion 5 | case swiftVersionChanged 6 | } 7 | 8 | extension CachedBuilderServiceError: LocalizedError { 9 | var errorDescription: String? { 10 | switch self { 11 | case .unableToRetrieveSwiftVersion: 12 | return "Unable to retrieve Swift version used for command line." 13 | 14 | case .swiftVersionChanged: 15 | return "Swift version used for command line apparently changed during runtime." 16 | } 17 | } 18 | } 19 | 20 | final class CachedBuilderService { 21 | private let frameworkCachingService: FrameworkCachingService 22 | private let carthageBuilderService: CarthageBuilderService 23 | 24 | init(sharedCachePath: String?) { 25 | frameworkCachingService = FrameworkCachingService(sharedCachePath: sharedCachePath) 26 | carthageBuilderService = CarthageBuilderService(frameworkCachingService: frameworkCachingService) 27 | } 28 | 29 | func frameworkProducts( 30 | manifest: Manifest, 31 | appTarget: AppTarget, 32 | dependencyGraph: DependencyGraph, 33 | platform: Platform, 34 | swiftVersion: String 35 | ) throws -> [FrameworkProduct] { 36 | var frameworkProducts: [FrameworkProduct] = [] 37 | 38 | let frameworks = try appTarget.frameworkDependencies(manifest: manifest, dependencyGraph: dependencyGraph).flattenedDeepFirstOrder() 39 | let frameworksWithoutDuplicates: [Framework] = frameworks.reduce(into: []) { result, framework in 40 | if !result.contains(framework) { result.append(framework) } 41 | } 42 | 43 | for framework in frameworksWithoutDuplicates { 44 | if 45 | let cachedFrameworkProduct = try frameworkCachingService.cachedProduct( 46 | framework: framework, 47 | platform: platform, 48 | swiftVersion: swiftVersion 49 | ) 50 | { 51 | frameworkProducts.append(cachedFrameworkProduct) 52 | } else { 53 | let frameworkProduct: FrameworkProduct 54 | switch try InstallationTypeDetectorService.shared.detectInstallationType(for: framework) { 55 | case .swiftPackageManager: 56 | try XcodeProjectGeneratorService.shared.generateXcodeProject(framework: framework) 57 | frameworkProduct = try carthageBuilderService.build( 58 | framework: framework, 59 | platform: platform, 60 | swiftVersion: swiftVersion, 61 | alreadyBuiltFrameworkProducts: frameworkProducts 62 | ) 63 | 64 | case .carthage: 65 | if !framework.isLocalRepo { 66 | try GitResetService.shared.resetGit(atPath: framework.projectDirectory, includeUntrackedFiles: false) 67 | } 68 | 69 | frameworkProduct = try carthageBuilderService.build( 70 | framework: framework, 71 | platform: platform, 72 | swiftVersion: swiftVersion, 73 | alreadyBuiltFrameworkProducts: frameworkProducts 74 | ) 75 | } 76 | 77 | if 78 | let frameworkSwiftVersion = ( 79 | try? SwiftVersionDetectorService.shared.detectSwiftVersion(ofFrameworkProduct: frameworkProduct) 80 | ) ?? ( 81 | try? SwiftVersionDetectorService.shared.getCurrentSwiftVersion() 82 | ) 83 | { 84 | // If detectSwiftVersion doesn't work (e. g. happening for RxAtomic because of missing Swift header file), 85 | // fallback to just retrieving current swift version via bash command. 86 | guard frameworkSwiftVersion == swiftVersion else { 87 | throw CachedBuilderServiceError.swiftVersionChanged 88 | } 89 | } else { 90 | throw CachedBuilderServiceError.unableToRetrieveSwiftVersion 91 | } 92 | 93 | frameworkProducts.append(frameworkProduct) 94 | } 95 | } 96 | 97 | return frameworkProducts 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/FrameworkCachingService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | final class FrameworkCachingService { 5 | private let sharedCachePath: String? 6 | 7 | init(sharedCachePath: String?) { 8 | self.sharedCachePath = sharedCachePath 9 | } 10 | 11 | func cachedProduct(framework: Framework, platform: Platform, swiftVersion: String) throws -> FrameworkProduct? { 12 | let subpath: String = cacheFileSubPath(framework: framework, platform: platform, swiftVersion: swiftVersion) 13 | let localCachedFileUrl = URL(fileURLWithPath: Constants.localCachePath).appendingPathComponent(subpath) 14 | 15 | if FileManager.default.fileExists(atPath: localCachedFileUrl.path) { 16 | print("Found cached build products for \(framework.libraryName) in local cache - skipping build.", level: .info) 17 | return try frameworkProduct(forCachedFileAt: localCachedFileUrl) 18 | } 19 | 20 | if let sharedCachePath = sharedCachePath { 21 | let sharedCacheFileUrl = URL(fileURLWithPath: sharedCachePath).appendingPathComponent(subpath) 22 | 23 | if FileManager.default.fileExists(atPath: sharedCacheFileUrl.path) { 24 | print("Found cached build products for \(framework.libraryName) in shared cache - skipping build.", level: .info) 25 | return try frameworkProduct(forCachedFileAt: sharedCacheFileUrl) 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func cache(product: FrameworkProduct, framework: Framework, platform: Platform, swiftVersion: String) throws { 33 | let subpath: String = cacheFileSubPath(framework: framework, platform: platform, swiftVersion: swiftVersion) 34 | 35 | if 36 | let sharedCachePath = sharedCachePath, 37 | FileManager.default.fileExists(atPath: URL(fileURLWithPath: sharedCachePath).deletingLastPathComponent().path) 38 | { 39 | try cache(product: product, to: URL(fileURLWithPath: sharedCachePath).appendingPathComponent(subpath)) 40 | print("Saved build products for \(framework.libraryName) in shared cache.", level: .info) 41 | } else { 42 | try cache(product: product, to: URL(fileURLWithPath: Constants.localCachePath).appendingPathComponent(subpath)) 43 | print("Saved build products for \(framework.libraryName) in local cache.", level: .info) 44 | } 45 | } 46 | 47 | public func frameworkProduct(forCachedFileAt cachedFileUrl: URL) throws -> FrameworkProduct { 48 | let libraryName: String = cachedFileUrl.pathComponents.suffix(3).first! 49 | let platformName: String = cachedFileUrl.deletingPathExtension().lastPathComponent 50 | let commitHash: String = cachedFileUrl.pathComponents.suffix(2).first! 51 | 52 | let frameworkProduct = FrameworkProduct(libraryName: libraryName, platformName: platformName, commitHash: commitHash) 53 | 54 | let subpath: String = cachedFileUrl.deletingPathExtension().pathComponents.suffix(3).joined(separator: "/") 55 | let unzippingUrl: URL = Constants.temporaryUncachingUrl.appendingPathComponent(subpath) 56 | 57 | try bash("mkdir -p '\(unzippingUrl.path)'") 58 | try Task.run(bash: "unzip -n -q '\(cachedFileUrl.path)' -d '\(unzippingUrl.path)'") 59 | 60 | let unzippedFrameworkDirPath = unzippingUrl.appendingPathComponent("\(libraryName).framework").path 61 | let unzippedSymbolsFilePath = unzippingUrl.appendingPathComponent("\(libraryName).framework.dSYM").path 62 | 63 | try bash("mkdir -p '\(frameworkProduct.frameworkDirUrl.deletingLastPathComponent().path)'") 64 | 65 | try Task.run(bash: "cp -R '\(unzippedFrameworkDirPath)' '\(frameworkProduct.frameworkDirPath)'") 66 | try Task.run(bash: "cp -R '\(unzippedSymbolsFilePath)' '\(frameworkProduct.symbolsFilePath)'") 67 | 68 | try frameworkProduct.cleanupRecursiveFrameworkIfNeeded() 69 | 70 | return frameworkProduct 71 | } 72 | 73 | private func cacheFileSubPath(framework: Framework, platform: Platform, swiftVersion: String) -> String { 74 | return "\(swiftVersion)/\(framework.libraryName)/\(framework.commitHash)/\(platform.rawValue).zip" 75 | } 76 | 77 | private func cache(product: FrameworkProduct, to targetUrl: URL) throws { 78 | try bash("mkdir -p '\(targetUrl.deletingLastPathComponent().path)'") 79 | 80 | let previousCurrentDirectoryPath = FileManager.default.currentDirectoryPath 81 | defer { FileManager.default.changeCurrentDirectoryPath(previousCurrentDirectoryPath) } 82 | 83 | FileManager.default.changeCurrentDirectoryPath(product.frameworkDirUrl.deletingLastPathComponent().path) 84 | try Task.run(bash: "zip -r -q -y '\(targetUrl.path)' '\(product.frameworkDirUrl.lastPathComponent)' '\(product.symbolsFileUrl.lastPathComponent)'") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/AccioKit/Services/XcodeProjectGeneratorService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | import PathKit 4 | import XcodeProj 5 | 6 | final class XcodeProjectGeneratorService { 7 | static let shared = XcodeProjectGeneratorService() 8 | 9 | func generateXcodeProject(framework: Framework) throws { 10 | print("Generating Xcode project at \(framework.generatedXcodeProjectPath) using SwiftPM ...", level: .info) 11 | 12 | let swiftPMBuildUrl: URL = URL(fileURLWithPath: framework.projectDirectory).appendingPathComponent(".build") 13 | if FileManager.default.fileExists(atPath: swiftPMBuildUrl.path) { 14 | try bash("rm -rf '\(swiftPMBuildUrl.path)'") 15 | } 16 | 17 | if FileManager.default.fileExists(atPath: framework.generatedXcodeProjectPath) { 18 | try bash("rm -rf '\(framework.generatedXcodeProjectPath)'") 19 | } 20 | 21 | try bash("chmod -R 775 '\(framework.projectDirectory)'") 22 | try bash("swift package --package-path '\(framework.projectDirectory)' generate-xcodeproj") 23 | try createSharedSchemeIfNeeded(framework: framework) 24 | try setDeploymentTargets(framework: framework) 25 | 26 | print("Generated Xcode project at \(framework.generatedXcodeProjectPath) using SwiftPM.", level: .info) 27 | } 28 | 29 | /// Swift 4.2 doesn't support specifying the platform deployment versions. This can be removed in Swift 5. 30 | func setDeploymentTargets(framework: Framework) throws { 31 | let xcodeProjectPath: String = framework.generatedXcodeProjectPath 32 | let projectFile: XcodeProj = try XcodeProj(path: Path(xcodeProjectPath)) 33 | let pbxproj: PBXProj = projectFile.pbxproj 34 | 35 | let platformToVersion: [Platform: String] = try self.platformToVersion(framework: framework) 36 | 37 | for targetObject in pbxproj.nativeTargets { 38 | for buildConfiguration in targetObject.buildConfigurationList!.buildConfigurations { 39 | for (platform, version) in platformToVersion { 40 | buildConfiguration.buildSettings[platform.deploymentTargetBuildSetting] = "\(version)" 41 | } 42 | } 43 | } 44 | 45 | try projectFile.write(path: Path(xcodeProjectPath), override: true) 46 | } 47 | 48 | func createSharedSchemeIfNeeded(framework: Framework) throws { 49 | let xcodeProjectPath: String = framework.generatedXcodeProjectPath 50 | let projectFile: XcodeProj = try XcodeProj(path: Path(xcodeProjectPath)) 51 | 52 | if projectFile.sharedData == nil { 53 | projectFile.sharedData = XCSharedData(schemes: []) 54 | } 55 | 56 | if projectFile.sharedData!.schemes.isEmpty { 57 | print("Manually creating shared scheme for generated project to fix issues with SwiftPM ...", level: .verbose) 58 | 59 | let sharedScheme: XCScheme = XCScheme(name: "\(framework.libraryName)-Package", lastUpgradeVersion: "9999", version: "1.3") 60 | let frameworkBuildableReference = XCScheme.BuildableReference( 61 | referencedContainer: "container:\(framework.projectName).xcodeproj", 62 | blueprint: projectFile.pbxproj.nativeTargets.first { $0.name == framework.libraryName }!, 63 | buildableName: "\(framework.projectName).xcodeproj", 64 | blueprintName: framework.libraryName 65 | ) 66 | let frameworkBuildAction = XCScheme.BuildAction.Entry( 67 | buildableReference: frameworkBuildableReference, 68 | buildFor: XCScheme.BuildAction.Entry.BuildFor.default 69 | ) 70 | sharedScheme.buildAction = XCScheme.BuildAction(buildActionEntries: [frameworkBuildAction]) 71 | 72 | projectFile.sharedData!.schemes = [sharedScheme] 73 | try projectFile.write(path: Path(xcodeProjectPath), override: true) 74 | } 75 | } 76 | 77 | /// Swift 4.2 doesn't support the `platform` parameter in the Package manifest, thus read it from a comment with this method. Also ensure Swift 5 support. 78 | func platformToVersion(framework: Framework) throws -> [Platform: String] { 79 | let platformRegex = try Regex(#"\.(iOS|macOS|tvOS|watchOS)\((?:"|.v)(\d+)[\._]?(\d+)?"?\)"#) 80 | 81 | let manifestPath: String = URL(fileURLWithPath: framework.projectDirectory).appendingPathComponent("Package.swift").path 82 | let manifestContents: String = try String(contentsOfFile: manifestPath) 83 | 84 | var platformToVersion: [Platform: String] = [.iOS: "8.0", .macOS: "10.10", .tvOS: "9.0", .watchOS: "2.0"] 85 | 86 | for match in platformRegex.matches(in: manifestContents) { 87 | guard let platform = Platform(rawValue: match.captures[0]!) else { fatalError("Matched unknown platform rawValue.") } 88 | 89 | let majorVersionString = match.captures[1]! 90 | let minorVersionString = match.captures[2] ?? "0" 91 | 92 | platformToVersion[platform] = "\(majorVersionString).\(minorVersionString)" 93 | } 94 | 95 | return platformToVersion 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/DependencyResolverServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class DependencyResolverServiceTests: XCTestCase { 5 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 6 | 7 | private var manifestResource: Resource { 8 | return Resource( 9 | url: testResourcesDir.appendingPathComponent("Package.swift"), 10 | contents: """ 11 | // swift-tools-version:4.2 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "TestProject", 16 | products: [], 17 | dependencies: [ 18 | .package(url: "https://github.com/Flinesoft/HandySwift.git", .upToNextMajor(from: "2.8.0")), 19 | .package(url: "https://github.com/Flinesoft/HandyUIKit.git", .upToNextMajor(from: "1.9.0")), 20 | .package(url: "https://github.com/Flinesoft/Imperio.git", .upToNextMajor(from: "3.0.0")), 21 | .package(url: "https://github.com/JamitLabs/MungoHealer.git", .upToNextMajor(from: "0.3.0")), 22 | .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "13.0.1")), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "TestProject-iOS", 27 | dependencies: [ 28 | "HandySwift", 29 | "HandyUIKit", 30 | "Imperio", 31 | "MungoHealer", 32 | "Moya", 33 | ], 34 | path: "TestProject-iOS" 35 | ) 36 | ] 37 | ) 38 | 39 | """ 40 | ) 41 | } 42 | 43 | private var xcodeProjectResource: Resource { 44 | return Resource( 45 | url: testResourcesDir.appendingPathComponent("TestProject.xcodeproj/project.pbxproj"), 46 | contents: ResourceData.iOSProjectFileContents 47 | ) 48 | } 49 | 50 | private var exampleSwiftFile: Resource { 51 | return Resource( 52 | url: testResourcesDir.appendingPathComponent("TestProject-iOS/Example.swift"), 53 | contents: "class Example {}" 54 | ) 55 | } 56 | 57 | private var exampleObjCFile: Resource { 58 | return Resource( 59 | url: testResourcesDir.appendingPathComponent("TestProject-iOS/AppDelegate.m"), 60 | contents: """ 61 | @interface AppDelegate 62 | @end 63 | 64 | @implementation AppDelegate 65 | #end 66 | """ 67 | ) 68 | } 69 | 70 | override func setUp() { 71 | super.setUp() 72 | 73 | try! bash("rm -rf '\(testResourcesDir.path)'") 74 | try! bash("mkdir '\(testResourcesDir.path)'") 75 | } 76 | 77 | func testDependencyGraph() { 78 | resourcesLoaded([manifestResource, xcodeProjectResource, exampleSwiftFile]) { 79 | let dependencyGraph = try! DependencyResolverService(workingDirectory: testResourcesDir.path).dependencyGraph() 80 | 81 | XCTAssertEqual(dependencyGraph.name, "TestProject") 82 | XCTAssertEqual(dependencyGraph.dependencies.count, 5) 83 | 84 | 85 | XCTAssertEqual(dependencyGraph.dependencies.map { $0.name }.sorted(), ["HandySwift", "HandyUIKit", "Imperio", "Moya", "MungoHealer"]) 86 | 87 | let moyaDependency = dependencyGraph.dependencies.first { $0.name == "Moya" }! 88 | XCTAssertEqual(moyaDependency.path, testResourcesDir.appendingPathComponent("\(Constants.buildPath)/checkouts/Moya").path) 89 | XCTAssertEqual(moyaDependency.dependencies.count, 4) 90 | XCTAssertEqual(moyaDependency.dependencies.map { $0.name }.sorted(), ["Alamofire", "ReactiveSwift", "Result", "RxSwift"]) 91 | } 92 | } 93 | 94 | func testMixedProjectOutput() { 95 | TestHelper.shared.isStartedByUnitTests = true 96 | 97 | resourcesLoaded([manifestResource, xcodeProjectResource, exampleSwiftFile, exampleObjCFile]) { 98 | do { 99 | _ = try DependencyResolverService(workingDirectory: testResourcesDir.path).dependencyGraph() 100 | XCTFail("Expected DependencyResolverService to throw exception.") 101 | } catch { 102 | XCTAssertEqual( 103 | TestHelper.shared.printOutputs.last?.message, 104 | """ 105 | Please make sure that the 'path' of all targets in Package.swift are set to directories containing only Swift files. 106 | For additional details, please see here: https://github.com/JamitLabs/Accio/issues/3 107 | """ 108 | ) 109 | XCTAssertEqual(TestHelper.shared.printOutputs.last?.level, .warning) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/FrameworkCachingServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import XCTest 3 | 4 | class FrameworkCachingServiceTests: XCTestCase { 5 | private let sharedCachePath: String = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestSharedCache").path 6 | 7 | private let testFramework = Framework(projectName: "TestProject", libraryName: "Example", projectDirectory: "", requiredFrameworks: []) 8 | private let testFrameworkProduct = FrameworkProduct( 9 | frameworkDirPath: FileManager.userCacheDirUrl.appendingPathComponent("AccioTestFrameworks/Example.framework").path, 10 | symbolsFilePath: FileManager.userCacheDirUrl.appendingPathComponent("AccioTestFrameworks/Example.framework.dSYM").path, 11 | commitHash: "abc" 12 | ) 13 | 14 | override func setUp() { 15 | super.setUp() 16 | 17 | Constants.useTestPaths = true 18 | 19 | try! bash("rm -rf '\(Constants.localCachePath)'") 20 | try! bash("rm -rf '\(sharedCachePath)'") 21 | 22 | try! bash("mkdir -p '\(FileManager.userCacheDirUrl.appendingPathComponent("AccioTestFrameworks/Example.framework").path)'") 23 | try! bash("touch '\(FileManager.userCacheDirUrl.appendingPathComponent("AccioTestFrameworks/Example.framework/Example").path)'") 24 | try! bash("touch '\(FileManager.userCacheDirUrl.appendingPathComponent("AccioTestFrameworks/Example.framework.dSYM").path)'") 25 | } 26 | 27 | override func tearDown() { 28 | super.tearDown() 29 | 30 | try! bash("rm -rf '\(Constants.dependenciesPath)'") 31 | } 32 | 33 | func testCachingProductWithoutSharedCachePath() { 34 | TestHelper.shared.isStartedByUnitTests = true 35 | let frameworkCachingService = FrameworkCachingService(sharedCachePath: nil) 36 | 37 | let swiftVersion = try! SwiftVersionDetectorService.shared.getCurrentSwiftVersion() 38 | 39 | let testFrameworkLocalCacheFilePath: String = "\(Constants.localCachePath)/\(swiftVersion)/\(testFramework.libraryName)/\(testFramework.commitHash)/\(Platform.iOS.rawValue).zip" 40 | let testFrameworkSharedCacheFilePath: String = "\(sharedCachePath)/\(swiftVersion)/\(testFramework.libraryName)/\(testFramework.commitHash)/\(Platform.iOS.rawValue).zip" 41 | 42 | var cachedProduct: FrameworkProduct? = try! frameworkCachingService.cachedProduct(framework: testFramework, platform: .iOS, swiftVersion: swiftVersion) 43 | XCTAssertNil(cachedProduct) 44 | 45 | XCTAssert(!FileManager.default.fileExists(atPath: testFrameworkLocalCacheFilePath)) 46 | XCTAssert(!FileManager.default.fileExists(atPath: testFrameworkSharedCacheFilePath)) 47 | 48 | try! frameworkCachingService.cache(product: testFrameworkProduct, framework: testFramework, platform: .iOS, swiftVersion: swiftVersion) 49 | 50 | cachedProduct = try! frameworkCachingService.cachedProduct(framework: testFramework, platform: .iOS, swiftVersion: swiftVersion) 51 | XCTAssertNotNil(cachedProduct) 52 | 53 | XCTAssert(cachedProduct!.frameworkDirPath.hasPrefix(Constants.temporaryFrameworksUrl.path)) 54 | XCTAssert(cachedProduct!.symbolsFilePath.hasPrefix(Constants.temporaryFrameworksUrl.path)) 55 | 56 | XCTAssert(FileManager.default.fileExists(atPath: cachedProduct!.frameworkDirPath)) 57 | XCTAssert(FileManager.default.fileExists(atPath: cachedProduct!.symbolsFilePath)) 58 | 59 | XCTAssert(FileManager.default.fileExists(atPath: testFrameworkLocalCacheFilePath)) 60 | XCTAssert(!FileManager.default.fileExists(atPath: testFrameworkSharedCacheFilePath)) 61 | } 62 | 63 | func testCachingProductWithSharedCachePath() { 64 | TestHelper.shared.isStartedByUnitTests = true 65 | let frameworkCachingService = FrameworkCachingService(sharedCachePath: sharedCachePath) 66 | 67 | let swiftVersion = try! SwiftVersionDetectorService.shared.getCurrentSwiftVersion() 68 | 69 | let testFrameworkLocalCacheFilePath: String = "\(Constants.localCachePath)/\(swiftVersion)/\(testFramework.libraryName)/\(testFramework.commitHash)/\(Platform.iOS.rawValue).zip" 70 | let testFrameworkSharedCacheFilePath: String = "\(sharedCachePath)/\(swiftVersion)/\(testFramework.libraryName)/\(testFramework.commitHash)/\(Platform.iOS.rawValue).zip" 71 | 72 | var cachedProduct: FrameworkProduct? = try! frameworkCachingService.cachedProduct(framework: testFramework, platform: .iOS, swiftVersion: swiftVersion) 73 | XCTAssertNil(cachedProduct) 74 | 75 | XCTAssert(!FileManager.default.fileExists(atPath: testFrameworkLocalCacheFilePath)) 76 | XCTAssert(!FileManager.default.fileExists(atPath: testFrameworkSharedCacheFilePath)) 77 | 78 | try! frameworkCachingService.cache(product: testFrameworkProduct, framework: testFramework, platform: .iOS, swiftVersion: swiftVersion) 79 | 80 | cachedProduct = try! frameworkCachingService.cachedProduct(framework: testFramework, platform: .iOS, swiftVersion: swiftVersion) 81 | XCTAssertNotNil(cachedProduct) 82 | 83 | XCTAssert(cachedProduct!.frameworkDirPath.hasPrefix(Constants.temporaryFrameworksUrl.path)) 84 | XCTAssert(cachedProduct!.symbolsFilePath.hasPrefix(Constants.temporaryFrameworksUrl.path)) 85 | 86 | XCTAssert(FileManager.default.fileExists(atPath: cachedProduct!.frameworkDirPath)) 87 | XCTAssert(FileManager.default.fileExists(atPath: cachedProduct!.symbolsFilePath)) 88 | 89 | XCTAssert(!FileManager.default.fileExists(atPath: testFrameworkLocalCacheFilePath)) 90 | XCTAssert(FileManager.default.fileExists(atPath: testFrameworkSharedCacheFilePath)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | ### Added 8 | - None. 9 | ### Changed 10 | - None. 11 | ### Deprecated 12 | - None. 13 | ### Removed 14 | - None. 15 | ### Fixed 16 | - None. 17 | ### Security 18 | - None. 19 | 20 | ## [0.6.6] - 2020-10-22 21 | ### Added 22 | - None. 23 | ### Changed 24 | - None. 25 | ### Deprecated 26 | - None. 27 | ### Removed 28 | - None. 29 | ### Fixed 30 | - Fix Xcode 12 installation issues. 31 | Issue: [#91](https://github.com/JamitLabs/Accio/issues/91) | PR: [#93](https://github.com/JamitLabs/Accio/pull/93) | Author: [Frederick Pietschmann](https://github.com/fredpi) 32 | - Fix git reset when using SwiftPM local repos. 33 | Issue: [#67](https://github.com/JamitLabs/Accio/issues/67) | PR: [#68](https://github.com/JamitLabs/Accio/pull/68) | Author: [Frederick Pietschmann](https://github.com/fredpi) 34 | 35 | ### Security 36 | - None. 37 | 38 | ## [0.6.5] - 2019-09-11 39 | ### Fixed 40 | - Improve compatibility with rare "non-standard" Package.swift configurations 41 | Issue: [#79](https://github.com/JamitLabs/Accio/issues/79) | PR: [#80](https://github.com/JamitLabs/Accio/pull/80) | Author: [Frederick Pietschmann](https://github.com/fredpi) 42 | 43 | ## [0.6.4] - 2019-09-01 44 | ### Fixed 45 | - Adjusted the bundle version fallback to support both macOS and iOS frameworks 46 | Issue: [#76](https://github.com/JamitLabs/Accio/issues/76) | PR: [#77](https://github.com/JamitLabs/Accio/pull/77) | Author: [Torsten Curdt](https://github.com/tcurdti) 47 | 48 | ## [0.6.3] - 2019-07-12 49 | ### Fixed 50 | - Fix mixed caching of frameworks with different Swift versions. 51 | Issue: [#61](https://github.com/JamitLabs/Accio/issues/61) | PR: [#62](https://github.com/JamitLabs/Accio/pull/62) | Author: [Frederick Pietschmann](https://github.com/fredpi) 52 | - Fix missing CFBundleVersion in Info.plist of build Frameworks by adding it implicity with the default value "1" 53 | Issue: [#69](https://github.com/JamitLabs/Accio/issues/69) | PR: [#70](https://github.com/JamitLabs/Accio/pull/70) | Author: [Murat Yilmaz](https://github.com/mrylmz) 54 | 55 | ## [0.6.2] - 2019-06-20 56 | ### Fixed 57 | - Redownload dependencies as a fallback when previously checked out repositories are broken. 58 | Issue: [#27](https://github.com/JamitLabs/Accio/issues/27) | PR: [#40](https://github.com/JamitLabs/Accio/pull/40) | Author: [Frederick Pietschmann](https://github.com/fredpi) 59 | - Avoid misleading output when building via Carthage. 60 | Issue: [#56](https://github.com/JamitLabs/Accio/issues/56) | PR: [#57](https://github.com/JamitLabs/Accio/pull/57) | Author: [Frederick Pietschmann](https://github.com/fredpi) 61 | - Warn properly when no schemes can be found. 62 | PR: [#59](https://github.com/JamitLabs/Accio/pull/59) | Author: [Frederick Pietschmann](https://github.com/fredpi) 63 | - Remove duplicated processing of frameworks referenced by multiple other frameworks. 64 | Issue: [#51](https://github.com/JamitLabs/Accio/issues/51) | PR: [#53](https://github.com/JamitLabs/Accio/pull/53) | Author: [Frederick Pietschmann](https://github.com/fredpi) 65 | 66 | ## [0.6.1] - 2019-04-26 67 | ### Added 68 | - Adds several popular GitHub projects for official integration support testing to the Demo project. 69 | PR: [#10](https://github.com/JamitLabs/Accio/pull/10) | Author: [Cihat Gündüz](https://github.com/Dschee) 70 | ### Fixed 71 | - Fixes an issue where two or more targets for the same platform would cause project linking issues. 72 | Issue: [#29](https://github.com/JamitLabs/Accio/issues/29) | PR: [#34](https://github.com/JamitLabs/Accio/pull/34) | Author: [Murat Yilmaz](https://github.com/mrylmz) 73 | - Fixes an issue where temporary changes to SwiftPM-only frameworks would be reset before building. 74 | Issue: [#35](https://github.com/JamitLabs/Accio/issues/35) | PR: [#36](https://github.com/JamitLabs/Accio/pull/36) | Author: [Cihat Gündüz](https://github.com/Dschee) 75 | 76 | ## [0.6.0] - 2019-04-19 77 | ### Added 78 | - Correctly recognizes App Extensions and doesn't add build phases for them. Fixes [#25](https://github.com/JamitLabs/Accio/issues/25). 79 | - Points to detailed information about conflicting name issues with SwiftPM. Fixes [#26](https://github.com/JamitLabs/Accio/issues/26). 80 | - The `init` command now properly detects test targets and lists them as such in the created manifest file. Fixes [#23](https://github.com/JamitLabs/Accio/issues/23). 81 | ### Changed 82 | - Improves reading of supported deployment targets. 83 | - Improves init command by treating empty manifest files like non-existing ones. Fixes [#24](https://github.com/JamitLabs/Accio/issues/24). 84 | ### Fixed 85 | - Fixes an issue where Accio commands where failing when Git resets failed. 86 | - Fixes an issue where Accio didn't reset changed files untracked by Git. 87 | 88 | ## [0.5.6] - 2019-04-09 89 | ### Added 90 | - Adds support for automatically finding schemes named like 'MBProgressHUD Framework tvOS'. 91 | ### Changed 92 | - Some improvements that make the output information on the console more precise. 93 | ### Fixed 94 | - Fixes the broken cleanup command of temporary frameworks after completing install. 95 | - Fixes an issue with multiple targets linking a single framework with schemes named after their platforms. 96 | - Fixes an issue with different platform specifiers used in scheme names. 97 | 98 | ## [0.5.5] - 2019-04-05 99 | ### Changed 100 | - The framework copy build phase now optimizes "dirty" build timing by specifying the output files. [#13](https://github.com/JamitLabs/Accio/issues/13) 101 | ### Fixed 102 | - Fixes an issue where broken previous install attempt leftovers cause errors on subsequent installs. [#12](https://github.com/JamitLabs/Accio/issues/12) 103 | 104 | ## [0.5.4] - 2019-04-03 105 | ### Fixed 106 | - Fix symbolic linking of .framework and .dSYM files. 107 | 108 | ## [0.5.3] - 2019-04-01 109 | ### Fixed 110 | - Fixes an issue where recursive copies of non symbolic links could cause build errors. 111 | 112 | ## [0.5.2] - 2019-04-01 113 | ### Fixed 114 | - Keep symlinks in cached ZIP files for macOS support. 115 | 116 | ## [0.5.1] - 2019-03-31 117 | ### Changed 118 | - Check if shared cache path is available, else add new build products to local cache. 119 | ### Fixed 120 | - Fixed an issue with copying unzipped cache build products back to project. 121 | 122 | ## [0.5.0] - 2019-03-30 123 | ### Added 124 | - Demo project for integration testing with popular Swift frameworks. 125 | ### Changed 126 | - Compress cached build products in a .zip file. Old style cached build products can be deleted. 127 | ### Fixed 128 | - Multiple issues with paths, names, symbolic links & more. 129 | 130 | ## [0.4.0] - 2019-03-29 131 | ### Added 132 | - Add support for test targets. 133 | - Sort `Dependencies` group alphabetically. 134 | ### Changed 135 | - Change structure of `Dependencies` folder. 136 | - Delete unneeded groups & references from `Dependencies` group. 137 | - Delete unneeded files & folders from `Dependencies` folder. 138 | - Only link frameworks when not already linked. 139 | - Unlink frameworks that are no longer included. 140 | - Don't save build products to local cache if shared cache is available. 141 | - Cleanup Accio run script phase when target gets removed. 142 | ### Fixed 143 | - Fix typo in local cache logging. 144 | - Fix missing use of `Constants.xcodeDependenciesGroup` & `Constants.dependenciesPath`. 145 | 146 | ## [0.3.0] - 2019-03-26 147 | ### Changed 148 | - Add support for Swift 5 and Xcode 10.2. 149 | - Separate cached frameworks by Swift tools version. 150 | ### Removed 151 | - Drop support for Swift 4.2 and Xcode <=10.1. 152 | 153 | ## [0.2.2] 154 | ### Fixed 155 | - Fixed an issue with some frameworks sym-linking to themselves. 156 | 157 | ## [0.2.1] 158 | ### Added 159 | - Support for setting a default `shared-cache-path` via configuration. 160 | - New sub command `set-shared-cache` for setting the shared cache path. 161 | ### Fixed 162 | - Also correctly recognize scheme names like "SwiftyBeaver (iOS)". 163 | 164 | ## [0.2.0] 165 | ### Added 166 | - Initial working release with `init`, `install`, `update`, `clean` and `clear-cache` sub commands 167 | -------------------------------------------------------------------------------- /Sources/AccioKit/Commands/Protocols/DependencyInstaller.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | enum DependencyInstallerError: Error { 5 | case noTargetsInManifest 6 | } 7 | 8 | protocol DependencyInstaller { 9 | func loadManifest() throws -> Manifest 10 | func revertCheckoutChanges(workingDirectory: String) throws 11 | func buildFrameworksAndIntegrateWithXcode(workingDirectory: String, manifest: Manifest, dependencyGraph: DependencyGraph, sharedCachePath: String?) throws 12 | func loadRequiredFrameworksFromCache(workingDirectory: String, sharedCachePath: String?) throws -> Bool 13 | } 14 | 15 | extension DependencyInstaller { 16 | func loadManifest() throws -> Manifest { 17 | let manifest = try ManifestHandlerService.shared.loadManifest(isDependency: false) 18 | 19 | guard !manifest.targets.isEmpty else { 20 | print("No targets specified in manifest file. Please add at least one target to the 'targets' array in Package.swift.", level: .warning) 21 | throw DependencyInstallerError.noTargetsInManifest 22 | } 23 | 24 | return manifest 25 | } 26 | 27 | func revertCheckoutChanges(workingDirectory: String = GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath) throws { 28 | let workingDirectoryUrl = URL(fileURLWithPath: workingDirectory) 29 | let checkoutsDirUrl = workingDirectoryUrl.appendingPathComponent("\(Constants.buildPath)/checkouts") 30 | 31 | if FileManager.default.fileExists(atPath: checkoutsDirUrl.path) { 32 | print("Reverting any changes in the checkouts directory ...", level: .info) 33 | 34 | for fileName in try FileManager.default.contentsOfDirectory(atPath: checkoutsDirUrl.path) { 35 | let frameworkCheckoutPath: String = checkoutsDirUrl.appendingPathComponent(fileName).path 36 | 37 | if try FileManager.default.isDirectory(atPath: frameworkCheckoutPath) { 38 | do { 39 | try GitResetService.shared.resetGit(atPath: frameworkCheckoutPath) 40 | } 41 | catch { 42 | // Remove checkout if git reset fails for some reason 43 | // If the checkout is missing, SPM will automatically clone again in the next step 44 | try bash("rm -rf '\(frameworkCheckoutPath)'") 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | func buildFrameworksAndIntegrateWithXcode( 52 | workingDirectory: String = GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath, 53 | manifest: Manifest, 54 | dependencyGraph: DependencyGraph, 55 | sharedCachePath: String? 56 | ) throws { 57 | if FileManager.default.fileExists(atPath: Constants.temporaryFrameworksUrl.path) { 58 | try bash("rm -rf '\(Constants.temporaryFrameworksUrl.path)'") 59 | } 60 | 61 | if FileManager.default.fileExists(atPath: Constants.temporaryUncachingUrl.path) { 62 | try bash("rm -rf '\(Constants.temporaryUncachingUrl.path)'") 63 | } 64 | 65 | try bash("mkdir -p '\(Constants.temporaryFrameworksUrl.path)'") 66 | try bash("mkdir -p '\(Constants.temporaryUncachingUrl.path)'") 67 | 68 | let swiftVersion = try SwiftVersionDetectorService.shared.getCurrentSwiftVersion() 69 | 70 | typealias ParsingResult = (target: AppTarget, platform: Platform, frameworkProducts: [FrameworkProduct]) 71 | 72 | let appTargets: [AppTarget] = try manifest.appTargets() 73 | let parsingResults: [ParsingResult] = try appTargets.compactMap { appTarget in 74 | guard !appTarget.dependentLibraryNames.isEmpty else { 75 | print("No dependencies specified for target '\(appTarget.targetName)'. Please add at least one dependency scheme to the 'dependencies' array of the target in Package.swift.", level: .warning) 76 | return nil 77 | } 78 | 79 | let platform = try PlatformDetectorService.shared.detectPlatform(of: appTarget) 80 | print("Resolving dependencies for target '\(appTarget.targetName)' on platform '\(platform.rawValue)' ...", level: .info) 81 | 82 | let frameworkProducts = try CachedBuilderService(sharedCachePath: sharedCachePath).frameworkProducts( 83 | manifest: manifest, 84 | appTarget: appTarget, 85 | dependencyGraph: dependencyGraph, 86 | platform: platform, 87 | swiftVersion: swiftVersion 88 | ) 89 | return ParsingResult(target: appTarget, platform: platform, frameworkProducts: frameworkProducts) 90 | } 91 | 92 | try XcodeProjectIntegrationService.shared.clearDependenciesFolder() 93 | 94 | for parsingResult in parsingResults { 95 | try XcodeProjectIntegrationService.shared.updateDependencies(of: parsingResult.target, for: parsingResult.platform, with: parsingResult.frameworkProducts) 96 | } 97 | 98 | try XcodeProjectIntegrationService.shared.handleRemovedTargets(keepingTargets: appTargets) 99 | try bash("rm -rf '\(Constants.temporaryFrameworksUrl.path)'") 100 | 101 | try ResolvedManifestCachingService(sharedCachePath: sharedCachePath).cacheResolvedManifest( 102 | at: URL(fileURLWithPath: workingDirectory).appendingPathComponent("Package.resolved"), 103 | with: parsingResults.flatMap { 104 | $0.frameworkProducts.map { 105 | CachedFrameworkProduct( 106 | libraryName: $0.libraryName, 107 | commitHash: $0.commitHash, 108 | platform: $0.platformName 109 | ) 110 | } 111 | } 112 | ) 113 | } 114 | 115 | func loadRequiredFrameworksFromCache( 116 | workingDirectory: String = GlobalOptions.workingDirectory.value ?? FileManager.default.currentDirectoryPath, 117 | sharedCachePath: String? 118 | ) throws -> Bool { 119 | let cachingService = ResolvedManifestCachingService(sharedCachePath: sharedCachePath) 120 | 121 | guard let cachedFrameworkProducts = try cachingService.cachedFrameworkProducts( 122 | forResolvedManifestAt: URL(fileURLWithPath: workingDirectory).appendingPathComponent("Package.resolved") 123 | ) else { 124 | return false 125 | } 126 | 127 | let cachedFrameworkProductUrls: [URL] = cachedFrameworkProducts.compactMap { cachedFrameworkProduct in 128 | guard let cacheFileSubPath = try? cachedFrameworkProduct.getCacheFileSubPath() else { return nil } 129 | 130 | let localCacheFileUrl = URL(fileURLWithPath: Constants.localCachePath).appendingPathComponent(cacheFileSubPath) 131 | if FileManager.default.fileExists(atPath: localCacheFileUrl.path) { 132 | return localCacheFileUrl 133 | } 134 | 135 | if let sharedCachePath = sharedCachePath { 136 | let sharedCacheFileUrl = URL(fileURLWithPath: sharedCachePath).appendingPathComponent(cacheFileSubPath) 137 | 138 | if FileManager.default.fileExists(atPath: sharedCacheFileUrl.path) { 139 | return sharedCacheFileUrl 140 | } 141 | } 142 | 143 | return nil 144 | } 145 | 146 | guard cachedFrameworkProductUrls.count == cachedFrameworkProducts.count else { 147 | print("Not all required build products specified in resolved manifest are cached – unable to skip checkout / integration process ...", level: .info) 148 | return false 149 | } 150 | 151 | print("Found all required build products specified in resolved manifest in cache – skipping checkout & integration process ...", level: .info) 152 | 153 | let frameworkProducts: [FrameworkProduct] = try cachedFrameworkProductUrls.map { 154 | return try FrameworkCachingService(sharedCachePath: sharedCachePath).frameworkProduct(forCachedFileAt: $0) 155 | } 156 | 157 | try XcodeProjectIntegrationService.shared.clearDependenciesFolder() 158 | try XcodeProjectIntegrationService.shared.copy(cachedFrameworkProducts: frameworkProducts) 159 | 160 | return true 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Tests/AccioKitTests/Services/XcodeProjectIntegrationServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AccioKit 2 | import PathKit 3 | import XcodeProj 4 | import XCTest 5 | 6 | class XcodeProjectIntegrationServiceTests: XCTestCase { 7 | private let testResourcesDir: URL = FileManager.userCacheDirUrl.appendingPathComponent("AccioTestResources") 8 | private let testFrameworks: [(name: String, includeBundleVersion: Bool)] = [ 9 | ("Alamofire", true), ("HandySwift", true), ("HandyUIKit", false), ("MungoHealer", false) 10 | ] 11 | 12 | private let regularTarget: AppTarget = AppTarget(projectName: "TestProject", targetName: "TestProject-iOS", dependentLibraryNames: [], targetType: .app) 13 | private let testTarget: AppTarget = AppTarget(projectName: "TestProject", targetName: "TestProject-iOSTests", dependentLibraryNames: [], targetType: .test) 14 | 15 | private var xcodeProjectResource: Resource { 16 | return Resource( 17 | url: testResourcesDir.appendingPathComponent("TestProject.xcodeproj/project.pbxproj"), 18 | contents: ResourceData.iOSProjectFileContents 19 | ) 20 | } 21 | 22 | private var frameworkProductsResources: [Resource] { 23 | return testFrameworks.flatMap { 24 | return [ 25 | Resource(url: testResourcesDir.appendingPathComponent(Constants.buildPath).appendingPathComponent("iOS/\($0.name).framework/keep"), contents: ""), 26 | Resource(url: testResourcesDir.appendingPathComponent(Constants.buildPath).appendingPathComponent("iOS/\($0.name).framework.dSYM"), contents: "") 27 | ] 28 | } 29 | } 30 | 31 | private var frameworkProducts: [FrameworkProduct] { 32 | return testFrameworks.map { 33 | FrameworkProduct( 34 | frameworkDirPath: testResourcesDir.appendingPathComponent(Constants.buildPath).appendingPathComponent("iOS/\($0.name).framework").path, 35 | symbolsFilePath: testResourcesDir.appendingPathComponent(Constants.buildPath).appendingPathComponent("iOS/\($0.name).framework.dSYM").path, 36 | commitHash: "abc" 37 | ) 38 | } 39 | } 40 | 41 | private var copiedFrameworkProducts: [FrameworkProduct] { 42 | return testFrameworks.map { 43 | FrameworkProduct( 44 | frameworkDirPath: testResourcesDir.appendingPathComponent(Constants.dependenciesPath).appendingPathComponent("iOS/\($0.name).framework").path, 45 | symbolsFilePath: testResourcesDir.appendingPathComponent(Constants.dependenciesPath).appendingPathComponent("iOS/\($0.name).framework.dSYM").path, 46 | commitHash: "abc" 47 | ) 48 | } 49 | } 50 | 51 | override func setUp() { 52 | super.setUp() 53 | 54 | clean() 55 | } 56 | 57 | private func createInfoPlist(platform: Platform, frameworkName: String, includeBundleVersion: Bool) { 58 | let resourcesURL = testResourcesDir.appendingPathComponent(Constants.buildPath) 59 | .appendingPathComponent(platform.rawValue) 60 | .appendingPathComponent("\(frameworkName).framework") 61 | .appendingPathComponent(platform.pathToPlist) 62 | try! FileManager.default.createDirectory(atPath: resourcesURL.path, withIntermediateDirectories: true, attributes: nil) 63 | let plistURL = resourcesURL.appendingPathComponent("Info.plist") 64 | let plist = includeBundleVersion ? ["CFBundleVersion": "1"] : [:] 65 | let data = try! PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0) 66 | try! data.write(to: plistURL, options: .atomic) 67 | } 68 | 69 | private func clean() { 70 | try! bash("rm -rf '\(testResourcesDir.path)'") 71 | } 72 | 73 | func testUpdateDependencies() { 74 | 75 | let platform: Platform = .iOS 76 | 77 | let xcodeProjectIntegrationService = XcodeProjectIntegrationService(workingDirectory: testResourcesDir.path) 78 | 79 | for appTarget in [regularTarget, testTarget] { 80 | resourcesLoaded(frameworkProductsResources + [xcodeProjectResource]) { 81 | // ensure frameworks not yet copied 82 | 83 | for frameworkProduct in copiedFrameworkProducts { 84 | XCTAssert(!FileManager.default.fileExists(atPath: frameworkProduct.frameworkDirPath)) 85 | XCTAssert(!FileManager.default.fileExists(atPath: frameworkProduct.symbolsFilePath)) 86 | } 87 | 88 | // ensure frameworks not yet linked 89 | var pbxproject = readPbxproject() 90 | var targetObject: PBXTarget = pbxproject.targets(named: appTarget.targetName).first! 91 | var frameworksBuildPhase: PBXFrameworksBuildPhase = targetObject.buildPhases.first(where: { $0.buildPhase == .frameworks })! as! PBXFrameworksBuildPhase 92 | 93 | XCTAssert(frameworksBuildPhase.files!.isEmpty) 94 | 95 | // ensure build phase not yet updated 96 | XCTAssert(!targetObject.buildPhases.contains { $0.type() == .runScript && ($0 as! PBXShellScriptBuildPhase).name == Constants.copyBuildScript }) 97 | 98 | testFrameworks.forEach { frameworkName, includeBundleVersion in 99 | createInfoPlist(platform:platform, frameworkName: frameworkName, includeBundleVersion: includeBundleVersion) 100 | } 101 | 102 | try! xcodeProjectIntegrationService.updateDependencies(of: appTarget, for: platform, with: frameworkProducts) 103 | 104 | // test CFBundleVersion in Info.plist 105 | frameworkProducts.forEach { product in 106 | let frameworkPath = product.frameworkDirPath.replacingOccurrences(of: "/.accio/", with: "/Dependencies/") 107 | let plistURL = URL(fileURLWithPath: frameworkPath).appendingPathComponent(platform.pathToPlist).appendingPathComponent("Info.plist") 108 | let data = try! Data(contentsOf: plistURL) 109 | var format: PropertyListSerialization.PropertyListFormat = .binary 110 | var plist = try! PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: &format) as! [String: Any] 111 | 112 | print("\(product.platformName) \(product.libraryName) \(plist)") 113 | 114 | XCTAssertNotNil(plist["CFBundleVersion"]) 115 | } 116 | 117 | // test copyFrameworkProducts 118 | for frameworkProduct in copiedFrameworkProducts { 119 | XCTAssert(FileManager.default.fileExists(atPath: frameworkProduct.frameworkDirPath)) 120 | XCTAssert(FileManager.default.fileExists(atPath: frameworkProduct.symbolsFilePath)) 121 | } 122 | 123 | pbxproject = readPbxproject() 124 | 125 | // test linkFrameworks 126 | targetObject = pbxproject.targets(named: appTarget.targetName).first! 127 | frameworksBuildPhase = targetObject.buildPhases.first(where: { $0.buildPhase == .frameworks })! as! PBXFrameworksBuildPhase 128 | 129 | XCTAssertEqual(frameworksBuildPhase.files!.count, testFrameworks.count) 130 | XCTAssertEqual(frameworksBuildPhase.files!.map { $0.file!.name }, testFrameworks.map { "\($0.name).framework" }) 131 | 132 | // test updateBuildPhase 133 | switch appTarget.targetType { 134 | case .app: 135 | let accioBuildScript = targetObject.buildPhases.first { $0.type() == .runScript && ($0 as! PBXShellScriptBuildPhase).name == Constants.copyBuildScript } as! PBXShellScriptBuildPhase 136 | 137 | XCTAssertEqual(accioBuildScript.inputPaths.count, testFrameworks.count) 138 | XCTAssertEqual(accioBuildScript.inputPaths, testFrameworks.map { "$(SRCROOT)/\(Constants.dependenciesPath)/\(platform.rawValue)/\($0.name).framework" }) 139 | 140 | case .test, .appExtension: 141 | let accioBuildScript = targetObject.buildPhases.first { $0.type() == .runScript && ($0 as! PBXShellScriptBuildPhase).name == Constants.copyBuildScript } 142 | XCTAssertNil(accioBuildScript) 143 | } 144 | 145 | // test project navigator integration 146 | let rootGroup = try! pbxproject.rootGroup()! 147 | let dependenciesGroup = try! rootGroup.group(named: Constants.xcodeDependenciesGroup) ?? rootGroup.addGroup(named: Constants.xcodeDependenciesGroup, options: .withoutFolder)[0] 148 | let targetGroup = try! dependenciesGroup.group(named: appTarget.targetName) ?? dependenciesGroup.addGroup(named: appTarget.targetName, options: .withoutFolder)[0] 149 | 150 | XCTAssertEqual(targetGroup.name, appTarget.targetName) 151 | XCTAssertEqual(targetGroup.children.compactMap { $0.name }, testFrameworks.map { "\($0.name).framework" }) 152 | 153 | clean() 154 | } 155 | } 156 | } 157 | 158 | private func readPbxproject() -> PBXProj { 159 | let projectFile = try! XcodeProj(path: Path(xcodeProjectResource.url.deletingLastPathComponent().path)) 160 | return projectFile.pbxproj 161 | } 162 | } 163 | --------------------------------------------------------------------------------