diff --git a/.gitignore b/.gitignore index cd00854..3f83ea6 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,10 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + +# Editor and IDE specific +.vscode/ +.claude/ + +# Demo project +demo_project/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..174e9b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +# Makefile for Jungle + +PREFIX ?= $(HOME)/.local +INSTALL_PATH = $(PREFIX)/bin +BUILD_PATH = .build/release +EXECUTABLE_NAME = jungle + +.PHONY: all build install uninstall clean test help + +# Default target +all: build + +# Build the executable in release mode +build: + @echo "Building $(EXECUTABLE_NAME) in release mode..." + swift build -c release --disable-sandbox + +# Install the executable to user directory +install: build + @echo "Installing $(EXECUTABLE_NAME) to $(INSTALL_PATH)..." + @mkdir -p $(INSTALL_PATH) + @install -m 755 $(BUILD_PATH)/$(EXECUTABLE_NAME) $(INSTALL_PATH)/$(EXECUTABLE_NAME) + @echo "✓ $(EXECUTABLE_NAME) installed successfully at $(INSTALL_PATH)/$(EXECUTABLE_NAME)" + @if ! echo "$$PATH" | grep -q "$(INSTALL_PATH)"; then \ + echo ""; \ + echo "⚠️ Note: $(INSTALL_PATH) is not in your PATH"; \ + echo " Add this to your shell profile (~/.zshrc or ~/.bashrc):"; \ + echo " export PATH=\"$(INSTALL_PATH):\$$PATH\""; \ + fi + +# Uninstall the executable from user directory +uninstall: + @echo "Uninstalling $(EXECUTABLE_NAME) from $(INSTALL_PATH)..." + @rm -f $(INSTALL_PATH)/$(EXECUTABLE_NAME) + @echo "✓ $(EXECUTABLE_NAME) uninstalled successfully" + +# Clean build artifacts +clean: + @echo "Cleaning build artifacts..." + @rm -rf .build + @echo "✓ Build artifacts cleaned" + +# Run tests +test: + @echo "Running tests..." + swift test + +# Show help +help: + @echo "Jungle Makefile Commands:" + @echo "" + @echo " make build - Build the executable in release mode" + @echo " make install - Build and install to $(INSTALL_PATH) (default: ~/.local/bin)" + @echo " make uninstall - Remove the executable from $(INSTALL_PATH)" + @echo " make clean - Remove build artifacts" + @echo " make test - Run tests" + @echo " make help - Show this help message" + @echo "" + @echo "Custom Installation Path:" + @echo " make install PREFIX=/custom/path" + @echo " This will install to /custom/path/bin/jungle" + @echo "" + @echo "Note: The default installation uses ~/.local/bin" + @echo " Make sure this directory is in your PATH" diff --git a/Package.swift b/Package.swift index 86a7f6e..b5bc714 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -79,5 +79,6 @@ let package = Package( .target( name: "Shell" ) - ] + ], + swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index a39440b..a4e9e37 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ You can read more information about dependency complexity in our Technical artic - [Installation](#installation) * [Mint](#mint) * [Manual](#manual) +- [Sample Projects](#sample-projects) - [Usage](#usage) * [Fetch Historic Complexities](#fetch-historic-complexities) * [Compare Complexity Graphs](#compare-complexity-graphs) @@ -35,6 +36,27 @@ mint install xing/jungle mint run jungle help ``` +### Makefile (Recommended) + +```bash +git clone https://github.com/xing/jungle +cd jungle +make install +``` + +This will install jungle to `~/.local/bin`. Make sure this directory is in your `PATH`: + +```bash +# Add to your ~/.zshrc or ~/.bashrc +export PATH="$HOME/.local/bin:$PATH" +``` + +Other available commands: +- `make build` - Build the executable +- `make clean` - Clean build artifacts +- `make uninstall` - Remove the installed executable +- `make help` - Show all available commands + ### Manual ```bash @@ -44,6 +66,31 @@ swift build -c release .build/release/jungle help ``` +## Sample Projects + +The `Samples/` directory contains example projects to help you get started with Jungle: + +### SPMSample +A Swift Package Manager project with popular dependencies (Alamofire, RealmSwift, SwiftyJSON, Kingfisher). + +**Try it:** +```bash +cd Samples/SPMSample +jungle modules --target SPMSample +``` + +### CocoaPodsSample +An iOS project using CocoaPods with dependencies like Moya, RxSwift, and SnapKit. + +**Setup and try:** +```bash +cd Samples/CocoaPodsSample +pod install +jungle modules --target CocoaPodsSample +``` + +See [Samples/README.md](Samples/README.md) for more details about each sample project. + ## Usage ### Fetch Historic Complexities diff --git a/Samples/CocoaPodsSample/.gitignore b/Samples/CocoaPodsSample/.gitignore new file mode 100644 index 0000000..d419aba --- /dev/null +++ b/Samples/CocoaPodsSample/.gitignore @@ -0,0 +1,6 @@ +# CocoaPods +Pods/ +*.xcworkspace + +# Xcode +xcuserdata/ diff --git a/Samples/CocoaPodsSample/CocoaPodsSample.xcodeproj/project.pbxproj b/Samples/CocoaPodsSample/CocoaPodsSample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..98ece19 --- /dev/null +++ b/Samples/CocoaPodsSample/CocoaPodsSample.xcodeproj/project.pbxproj @@ -0,0 +1,359 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 54C280AE07D87589A2F580BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B61101AF65E260F9BADA139 /* AppDelegate.swift */; }; + 74C66065555FB629F727483C /* Pods_CocoaPodsSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2D0DA4CCB77970154FBCC36 /* Pods_CocoaPodsSample.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 5653DF33309A954D3FF03627 /* Pods-CocoaPodsSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPodsSample.debug.xcconfig"; path = "Target Support Files/Pods-CocoaPodsSample/Pods-CocoaPodsSample.debug.xcconfig"; sourceTree = ""; }; + 6B61101AF65E260F9BADA139 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7CC13D77AAA1D7BA7FB000AB /* CocoaPodsSample.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = CocoaPodsSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D30B2B1191E353231DBB12B /* Pods-CocoaPodsSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPodsSample.release.xcconfig"; path = "Target Support Files/Pods-CocoaPodsSample/Pods-CocoaPodsSample.release.xcconfig"; sourceTree = ""; }; + B2D0DA4CCB77970154FBCC36 /* Pods_CocoaPodsSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CocoaPodsSample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + EEF661CC971D388A48920883 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 74C66065555FB629F727483C /* Pods_CocoaPodsSample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3EB6D18563ECF0A7B6A0ABA3 /* Products */ = { + isa = PBXGroup; + children = ( + 7CC13D77AAA1D7BA7FB000AB /* CocoaPodsSample.app */, + ); + name = Products; + sourceTree = ""; + }; + 50F499507ACC7496DA7B1219 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B2D0DA4CCB77970154FBCC36 /* Pods_CocoaPodsSample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 536692CDDAC281C567A166AB /* CocoaPodsSample */ = { + isa = PBXGroup; + children = ( + 6B61101AF65E260F9BADA139 /* AppDelegate.swift */, + ); + path = CocoaPodsSample; + sourceTree = ""; + }; + 83B14D9353013C7B4AE9FAC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 5653DF33309A954D3FF03627 /* Pods-CocoaPodsSample.debug.xcconfig */, + 7D30B2B1191E353231DBB12B /* Pods-CocoaPodsSample.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + AF7E0FF6A308B3F7B1A6A479 = { + isa = PBXGroup; + children = ( + 536692CDDAC281C567A166AB /* CocoaPodsSample */, + 3EB6D18563ECF0A7B6A0ABA3 /* Products */, + 83B14D9353013C7B4AE9FAC1 /* Pods */, + 50F499507ACC7496DA7B1219 /* Frameworks */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 245CAD9F3964737E6C773D82 /* CocoaPodsSample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4E41CC94FA177DFCA91ADFA3 /* Build configuration list for PBXNativeTarget "CocoaPodsSample" */; + buildPhases = ( + 2890D6350275F10090EBE1BE /* [CP] Check Pods Manifest.lock */, + 89B091C4E4FA74E879CA3ECA /* Sources */, + EEF661CC971D388A48920883 /* Frameworks */, + B11A05C7F6D5883C8E8B1969 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CocoaPodsSample; + productName = CocoaPodsSample; + productReference = 7CC13D77AAA1D7BA7FB000AB /* CocoaPodsSample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A3D1E7084E80EDB2CB2D78DA /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + }; + }; + buildConfigurationList = 436D44B293380B95B868A659 /* Build configuration list for PBXProject "CocoaPodsSample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = AF7E0FF6A308B3F7B1A6A479; + productRefGroup = 3EB6D18563ECF0A7B6A0ABA3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 245CAD9F3964737E6C773D82 /* CocoaPodsSample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2890D6350275F10090EBE1BE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CocoaPodsSample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B11A05C7F6D5883C8E8B1969 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CocoaPodsSample/Pods-CocoaPodsSample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CocoaPodsSample/Pods-CocoaPodsSample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CocoaPodsSample/Pods-CocoaPodsSample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 89B091C4E4FA74E879CA3ECA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 54C280AE07D87589A2F580BC /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 22C0550B4A6DE7E315271D75 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7D30B2B1191E353231DBB12B /* Pods-CocoaPodsSample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.sample.CocoaPodsSample; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + A64178699855E49810B57046 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + F6F2FF85804301B1932C21CF /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5653DF33309A954D3FF03627 /* Pods-CocoaPodsSample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.sample.CocoaPodsSample; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F86C4E901F8C6E5A2BABAC02 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 436D44B293380B95B868A659 /* Build configuration list for PBXProject "CocoaPodsSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A64178699855E49810B57046 /* Debug */, + F86C4E901F8C6E5A2BABAC02 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 4E41CC94FA177DFCA91ADFA3 /* Build configuration list for PBXNativeTarget "CocoaPodsSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F6F2FF85804301B1932C21CF /* Debug */, + 22C0550B4A6DE7E315271D75 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = A3D1E7084E80EDB2CB2D78DA /* Project object */; +} diff --git a/Samples/CocoaPodsSample/CocoaPodsSample/AppDelegate.swift b/Samples/CocoaPodsSample/CocoaPodsSample/AppDelegate.swift new file mode 100644 index 0000000..896a8b6 --- /dev/null +++ b/Samples/CocoaPodsSample/CocoaPodsSample/AppDelegate.swift @@ -0,0 +1,14 @@ +import UIKit +import Alamofire +import RxSwift +import Kingfisher + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + print("CocoaPods Sample App with popular dependencies") + return true + } +} diff --git a/Samples/CocoaPodsSample/Podfile b/Samples/CocoaPodsSample/Podfile new file mode 100644 index 0000000..6f465c5 --- /dev/null +++ b/Samples/CocoaPodsSample/Podfile @@ -0,0 +1,21 @@ +platform :ios, '13.0' +use_frameworks! + +target 'CocoaPodsSample' do + # Networking + pod 'Alamofire', '~> 5.8' + pod 'Moya', '~> 15.0' + + # JSON Parsing + pod 'SwiftyJSON', '~> 5.0' + + # Image Loading + pod 'Kingfisher', '~> 7.10' + + # Reactive Programming + pod 'RxSwift', '~> 6.6' + pod 'RxCocoa', '~> 6.6' + + # UI + pod 'SnapKit', '~> 5.6' +end diff --git a/Samples/CocoaPodsSample/Podfile.lock b/Samples/CocoaPodsSample/Podfile.lock new file mode 100644 index 0000000..fb8cfa9 --- /dev/null +++ b/Samples/CocoaPodsSample/Podfile.lock @@ -0,0 +1,49 @@ +PODS: + - Alamofire (5.10.2) + - Kingfisher (7.12.0) + - Moya (15.0.0): + - Moya/Core (= 15.0.0) + - Moya/Core (15.0.0): + - Alamofire (~> 5.0) + - RxCocoa (6.9.0): + - RxRelay (= 6.9.0) + - RxSwift (= 6.9.0) + - RxRelay (6.9.0): + - RxSwift (= 6.9.0) + - RxSwift (6.9.0) + - SnapKit (5.7.1) + - SwiftyJSON (5.0.2) + +DEPENDENCIES: + - Alamofire (~> 5.8) + - Kingfisher (~> 7.10) + - Moya (~> 15.0) + - RxCocoa (~> 6.6) + - RxSwift (~> 6.6) + - SnapKit (~> 5.6) + - SwiftyJSON (~> 5.0) + +SPEC REPOS: + trunk: + - Alamofire + - Kingfisher + - Moya + - RxCocoa + - RxRelay + - RxSwift + - SnapKit + - SwiftyJSON + +SPEC CHECKSUMS: + Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 + Kingfisher: 53a10ea35051a436b5fb626ca2dd8f3144d755e9 + Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee + RxCocoa: ac16414696ae706516be3e1ab00fcce5bdc9be8a + RxRelay: 6b0c930e5cef57d5fe2032571e5e65b78e3cbdb1 + RxSwift: 31649ace6aceeb422e16ff71c60804f9c3281ed9 + SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a + SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a + +PODFILE CHECKSUM: 138a12e14797ce3c5cc5a496d9d472ec428bfc16 + +COCOAPODS: 1.16.2 diff --git a/Samples/CocoaPodsSample/README.md b/Samples/CocoaPodsSample/README.md new file mode 100644 index 0000000..6adf131 --- /dev/null +++ b/Samples/CocoaPodsSample/README.md @@ -0,0 +1,38 @@ +# CocoaPodsSample + +A CocoaPods sample project demonstrating Jungle's analysis capabilities with iOS projects. + +## Dependencies + +This project includes popular CocoaPods: +- **Alamofire** (~> 5.8) - HTTP networking library +- **Moya** (~> 15.0) - Network abstraction layer +- **SwiftyJSON** (~> 5.0) - JSON parsing library +- **Kingfisher** (~> 7.10) - Image downloading and caching +- **RxSwift** (~> 6.6) - Reactive programming framework +- **RxCocoa** (~> 6.6) - Reactive extensions for Cocoa +- **SnapKit** (~> 5.6) - Auto Layout DSL + +## Setup + +1. Install dependencies: +```bash +pod install +``` + +2. Open the workspace: +```bash +open CocoaPodsSample.xcworkspace +``` + +## Usage + +Run Jungle to analyze module dependencies: + +```bash +jungle modules --target CocoaPodsSample +``` + +## Note + +This sample project has a git repository initialized with Podfile.lock committed, which allows Jungle to analyze the dependency graph. diff --git a/Samples/CocoaPodsSample/project.yml b/Samples/CocoaPodsSample/project.yml new file mode 100644 index 0000000..26dade3 --- /dev/null +++ b/Samples/CocoaPodsSample/project.yml @@ -0,0 +1,13 @@ +name: CocoaPodsSample +options: + bundleIdPrefix: com.sample +targets: + CocoaPodsSample: + type: application + platform: iOS + deploymentTarget: "13.0" + sources: + - CocoaPodsSample + settings: + PRODUCT_BUNDLE_IDENTIFIER: com.sample.CocoaPodsSample + SWIFT_VERSION: "5.0" diff --git a/Samples/README.md b/Samples/README.md new file mode 100644 index 0000000..4f10ce8 --- /dev/null +++ b/Samples/README.md @@ -0,0 +1,37 @@ +# Jungle Sample Projects + +This directory contains sample projects demonstrating how to use Jungle with different dependency management systems. + +## Available Samples + +### SPMSample +A Swift Package Manager project with popular dependencies including: +- Alamofire (networking) +- RealmSwift (database) +- SwiftyJSON (JSON parsing) +- Kingfisher (image loading) + +### CocoaPodsSample +A CocoaPods-based iOS project with popular dependencies including: +- Alamofire (networking) +- Moya (network abstraction) +- SwiftyJSON (JSON parsing) +- Kingfisher (image loading) +- RxSwift & RxCocoa (reactive programming) +- SnapKit (Auto Layout) + +## Usage + +Navigate to any sample directory and run Jungle commands to analyze dependencies. + +### For SPMSample: +```bash +cd SPMSample +jungle modules --target SPMSample +``` + +### For CocoaPodsSample: +```bash +cd CocoaPodsSample +jungle modules --target CocoaPodsSample +``` diff --git a/Samples/SPMSample/Package.swift b/Samples/SPMSample/Package.swift new file mode 100644 index 0000000..3ab73b1 --- /dev/null +++ b/Samples/SPMSample/Package.swift @@ -0,0 +1,108 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "SPMSample", + platforms: [ + .iOS(.v13), + .macOS(.v10_15) + ], + products: [ + .library( + name: "SPMSample", + targets: ["SPMSample"]), + .library( + name: "Networking", + targets: ["Networking"]), + .library( + name: "DataLayer", + targets: ["DataLayer"]), + ], + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"), + .package(url: "https://github.com/realm/realm-swift.git", from: "10.45.0"), + .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), + .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.10.0"), + ], + targets: [ + // Core utilities module (no dependencies) + .target( + name: "Core", + dependencies: []), + + // Networking layer (depends on Core and Alamofire) + .target( + name: "Networking", + dependencies: [ + "Core", + "Alamofire", + "SwiftyJSON", + ]), + + // Data persistence layer (depends on Core and RealmSwift) + .target( + name: "DataLayer", + dependencies: [ + "Core", + .product(name: "RealmSwift", package: "realm-swift"), + ]), + + // API client (depends on Networking and DataLayer) + .target( + name: "APIClient", + dependencies: [ + "Networking", + "DataLayer", + ]), + + // Image loading service (depends on Core and Kingfisher) + .target( + name: "ImageService", + dependencies: [ + "Core", + "Kingfisher", + ]), + + // Feature 1 (depends on APIClient and ImageService) + .target( + name: "Feature1", + dependencies: [ + "Core", + "APIClient", + "ImageService", + ]), + + // Feature 2 (depends on Networking and DataLayer) + .target( + name: "Feature2", + dependencies: [ + "Core", + "Networking", + "DataLayer", + ]), + + // Main app module (depends on everything) + .target( + name: "SPMSample", + dependencies: [ + "Core", + "Networking", + "DataLayer", + "APIClient", + "ImageService", + "Feature1", + "Feature2", + ]), + + // Tests + .testTarget( + name: "CoreTests", + dependencies: ["Core"]), + .testTarget( + name: "NetworkingTests", + dependencies: ["Networking"]), + .testTarget( + name: "SPMSampleTests", + dependencies: ["SPMSample"]), + ] +) diff --git a/Samples/SPMSample/README.md b/Samples/SPMSample/README.md new file mode 100644 index 0000000..913c5ba --- /dev/null +++ b/Samples/SPMSample/README.md @@ -0,0 +1,87 @@ +# SPMSample + +A complex Swift Package Manager sample project demonstrating Jungle's analysis capabilities with multiple internal modules and dependencies. + +## Architecture + +This project demonstrates a multi-module architecture with internal dependencies: + +### Internal Modules + +- **Core** - Base utilities (no dependencies) +- **Networking** - HTTP client layer (depends on Core, Alamofire, SwiftyJSON) +- **DataLayer** - Data persistence (depends on Core, RealmSwift) +- **APIClient** - API integration (depends on Networking, DataLayer) +- **ImageService** - Image loading (depends on Core, Kingfisher) +- **Feature1** - User profile feature (depends on Core, APIClient, ImageService) +- **Feature2** - Data sync feature (depends on Core, Networking, DataLayer) +- **SPMSample** - Main module (depends on all modules) + +### Dependency Graph + +``` +SPMSample +├── Core +├── Networking +│ ├── Core +│ ├── Alamofire +│ └── SwiftyJSON +├── DataLayer +│ ├── Core +│ └── RealmSwift +├── APIClient +│ ├── Networking +│ └── DataLayer +├── ImageService +│ ├── Core +│ └── Kingfisher +├── Feature1 +│ ├── Core +│ ├── APIClient +│ └── ImageService +└── Feature2 + ├── Core + ├── Networking + └── DataLayer +``` + +## External Dependencies + +- **Alamofire** (5.8.0+) - HTTP networking library +- **RealmSwift** (10.45.0+) - Mobile database +- **SwiftyJSON** (5.0.0+) - JSON parsing library +- **Kingfisher** (7.10.0+) - Image downloading and caching + +## Usage with Jungle + +Analyze the main module dependencies: +```bash +jungle modules --target SPMSample +``` + +Analyze a specific internal module: +```bash +jungle modules --target Networking +``` + +Find dependants of a module: +```bash +jungle dependant --target Core +``` + +Generate dependency graph: +```bash +jungle graph --target SPMSample +``` + +## Building + +```bash +swift build +``` + +## Testing + +```bash +swift test +``` diff --git a/Samples/SPMSample/Sources/APIClient/APIClient.swift b/Samples/SPMSample/Sources/APIClient/APIClient.swift new file mode 100644 index 0000000..0bb57fd --- /dev/null +++ b/Samples/SPMSample/Sources/APIClient/APIClient.swift @@ -0,0 +1,16 @@ +import Foundation +import Networking +import DataLayer + +public class APIClient { + private let networkClient = NetworkClient() + private let databaseManager = DatabaseManager() + + public init() {} + + public func fetchAndStore(endpoint: String) async throws { + let data = try await networkClient.request(endpoint) + // Process and store data + print("Fetched and stored data from \(endpoint)") + } +} diff --git a/Samples/SPMSample/Sources/Core/Extensions.swift b/Samples/SPMSample/Sources/Core/Extensions.swift new file mode 100644 index 0000000..8ade9d5 --- /dev/null +++ b/Samples/SPMSample/Sources/Core/Extensions.swift @@ -0,0 +1,13 @@ +import Foundation + +public extension String { + var trimmed: String { + trimmingCharacters(in: .whitespacesAndNewlines) + } +} + +public extension Date { + var timestamp: Int64 { + Int64(timeIntervalSince1970) + } +} diff --git a/Samples/SPMSample/Sources/Core/Logger.swift b/Samples/SPMSample/Sources/Core/Logger.swift new file mode 100644 index 0000000..87f5f23 --- /dev/null +++ b/Samples/SPMSample/Sources/Core/Logger.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct Logger { + public init() {} + + public func log(_ message: String) { + print("[LOG] \(message)") + } + + public func error(_ message: String) { + print("[ERROR] \(message)") + } +} diff --git a/Samples/SPMSample/Sources/DataLayer/DatabaseManager.swift b/Samples/SPMSample/Sources/DataLayer/DatabaseManager.swift new file mode 100644 index 0000000..4b6e712 --- /dev/null +++ b/Samples/SPMSample/Sources/DataLayer/DatabaseManager.swift @@ -0,0 +1,19 @@ +import Foundation +import RealmSwift +import Core + +public class DatabaseManager { + private let logger = Logger() + + public init() {} + + public func save(_ object: T) throws { + logger.log("Saving object to database") + // Simplified example + } + + public func fetch(_ type: T.Type) -> [T] { + logger.log("Fetching objects from database") + return [] + } +} diff --git a/Samples/SPMSample/Sources/Feature1/Feature1.swift b/Samples/SPMSample/Sources/Feature1/Feature1.swift new file mode 100644 index 0000000..c5dfa0b --- /dev/null +++ b/Samples/SPMSample/Sources/Feature1/Feature1.swift @@ -0,0 +1,23 @@ +import Foundation +import Core +import APIClient +import ImageService + +public class Feature1 { + private let apiClient = APIClient() + private let imageLoader = ImageLoader() + private let logger = Logger() + + public init() {} + + public func execute() async { + logger.log("Feature1: Executing user profile feature") + do { + try await apiClient.fetchAndStore(endpoint: "/api/user/profile") + imageLoader.loadImage(from: URL(string: "https://example.com/avatar.png")!) + logger.log("Feature1: User profile loaded successfully") + } catch { + logger.error("Feature1: Error - \(error)") + } + } +} diff --git a/Samples/SPMSample/Sources/Feature2/Feature2.swift b/Samples/SPMSample/Sources/Feature2/Feature2.swift new file mode 100644 index 0000000..277359f --- /dev/null +++ b/Samples/SPMSample/Sources/Feature2/Feature2.swift @@ -0,0 +1,23 @@ +import Foundation +import Core +import Networking +import DataLayer + +public class Feature2 { + private let networkClient = NetworkClient() + private let databaseManager = DatabaseManager() + private let logger = Logger() + + public init() {} + + public func execute() async { + logger.log("Feature2: Executing data sync feature") + do { + let data = try await networkClient.request("/api/sync") + // Process and store sync data + logger.log("Feature2: Data synced successfully") + } catch { + logger.error("Feature2: Error - \(error)") + } + } +} diff --git a/Samples/SPMSample/Sources/ImageService/ImageLoader.swift b/Samples/SPMSample/Sources/ImageService/ImageLoader.swift new file mode 100644 index 0000000..8828129 --- /dev/null +++ b/Samples/SPMSample/Sources/ImageService/ImageLoader.swift @@ -0,0 +1,14 @@ +import Foundation +import Kingfisher +import Core + +public class ImageLoader { + private let logger = Logger() + + public init() {} + + public func loadImage(from url: URL) { + logger.log("Loading image from: \(url)") + // Simplified example using Kingfisher + } +} diff --git a/Samples/SPMSample/Sources/Networking/NetworkClient.swift b/Samples/SPMSample/Sources/Networking/NetworkClient.swift new file mode 100644 index 0000000..7f7023c --- /dev/null +++ b/Samples/SPMSample/Sources/Networking/NetworkClient.swift @@ -0,0 +1,16 @@ +import Foundation +import Alamofire +import SwiftyJSON +import Core + +public class NetworkClient { + private let logger = Logger() + + public init() {} + + public func request(_ endpoint: String) async throws -> JSON { + logger.log("Making request to: \(endpoint)") + // Simplified example + return JSON([:]) + } +} diff --git a/Samples/SPMSample/Sources/SPMSample/SPMSample.swift b/Samples/SPMSample/Sources/SPMSample/SPMSample.swift new file mode 100644 index 0000000..280fe45 --- /dev/null +++ b/Samples/SPMSample/Sources/SPMSample/SPMSample.swift @@ -0,0 +1,33 @@ +import Foundation +import Core +import Networking +import DataLayer +import APIClient +import ImageService +import Feature1 +import Feature2 + +public struct SPMSample { + private let apiClient = APIClient() + private let imageLoader = ImageLoader() + private let feature1 = Feature1() + private let feature2 = Feature2() + private let logger = Logger() + + public init() {} + + public func run() async { + logger.log("SPM Sample - Complex multi-module architecture") + do { + try await apiClient.fetchAndStore(endpoint: "/api/data") + imageLoader.loadImage(from: URL(string: "https://example.com/image.png")!) + + await feature1.execute() + await feature2.execute() + + logger.log("Application running successfully") + } catch { + logger.error("Error: \(error)") + } + } +} diff --git a/Samples/SPMSample/Tests/CoreTests/LoggerTests.swift b/Samples/SPMSample/Tests/CoreTests/LoggerTests.swift new file mode 100644 index 0000000..1f6fd36 --- /dev/null +++ b/Samples/SPMSample/Tests/CoreTests/LoggerTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import Core + +final class LoggerTests: XCTestCase { + func testLogger() { + let logger = Logger() + logger.log("Test message") + logger.error("Test error") + } +} diff --git a/Samples/SPMSample/Tests/NetworkingTests/NetworkClientTests.swift b/Samples/SPMSample/Tests/NetworkingTests/NetworkClientTests.swift new file mode 100644 index 0000000..effd755 --- /dev/null +++ b/Samples/SPMSample/Tests/NetworkingTests/NetworkClientTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import Networking + +final class NetworkClientTests: XCTestCase { + func testNetworkClient() async throws { + let client = NetworkClient() + let result = try await client.request("/test") + XCTAssertNotNil(result) + } +} diff --git a/Samples/SPMSample/Tests/SPMSampleTests/SPMSampleTests.swift b/Samples/SPMSample/Tests/SPMSampleTests/SPMSampleTests.swift new file mode 100644 index 0000000..8d453cb --- /dev/null +++ b/Samples/SPMSample/Tests/SPMSampleTests/SPMSampleTests.swift @@ -0,0 +1,9 @@ +import XCTest +@testable import SPMSample + +final class SPMSampleTests: XCTestCase { + func testSample() async throws { + let sample = SPMSample() + await sample.run() + } +} diff --git a/Sources/jungle/Commands/CompareCommand.swift b/Sources/jungle/Commands/CompareCommand.swift index b0b1db4..90b16de 100644 --- a/Sources/jungle/Commands/CompareCommand.swift +++ b/Sources/jungle/Commands/CompareCommand.swift @@ -1,4 +1,4 @@ -import ArgumentParser +@preconcurrency import ArgumentParser import Foundation import DependencyGraph import PodExtractor @@ -20,7 +20,7 @@ extension CompareError: CustomStringConvertible { } struct CompareCommand: ParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "compare", abstract: "Compares the current complexity of the dependency graph to others versions in git" ) diff --git a/Sources/jungle/Commands/DependantCommand.swift b/Sources/jungle/Commands/DependantCommand.swift index 9cf84a6..18ab5f2 100644 --- a/Sources/jungle/Commands/DependantCommand.swift +++ b/Sources/jungle/Commands/DependantCommand.swift @@ -1,12 +1,12 @@ import Foundation -import ArgumentParser +@preconcurrency import ArgumentParser import Shell import SPMExtractor import DependencyModule import PodExtractor struct DependantCommand: ParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "dependant", abstract: "Outputs a sorted list of targets that depends on the specified one in target" ) @@ -39,7 +39,8 @@ struct DependantCommand: ParsableCommand { } private func processPodfile(at directoryURL: URL) throws { - let podfileLock = try shell("git show HEAD:Podfile.lock", at: directoryURL) + let podfileLockURL = directoryURL.appendingPathComponent("Podfile.lock") + let podfileLock = try String(contentsOf: podfileLockURL, encoding: .utf8) let allPodfileModules = try extractModulesFromPodfileLock(podfileLock, excludeTests: false) let targets = try extractDependantTargets(from: allPodfileModules, for: target) processOutput(for: targets) diff --git a/Sources/jungle/Commands/GraphCommand.swift b/Sources/jungle/Commands/GraphCommand.swift index 68c973f..18fdbb7 100644 --- a/Sources/jungle/Commands/GraphCommand.swift +++ b/Sources/jungle/Commands/GraphCommand.swift @@ -1,4 +1,4 @@ -import ArgumentParser +@preconcurrency import ArgumentParser import Foundation import DependencyGraph import PodExtractor @@ -7,7 +7,7 @@ import Shell import SPMExtractor struct GraphCommand: ParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "graph", abstract: "Outputs the dependency graph in DOT format" ) diff --git a/Sources/jungle/Commands/HistoryCommand.swift b/Sources/jungle/Commands/HistoryCommand.swift index 1c1fc9b..d54989a 100644 --- a/Sources/jungle/Commands/HistoryCommand.swift +++ b/Sources/jungle/Commands/HistoryCommand.swift @@ -1,4 +1,4 @@ -import ArgumentParser +@preconcurrency import ArgumentParser import Foundation import DependencyGraph import PodExtractor @@ -7,13 +7,13 @@ import DependencyModule import Shell struct HistoryCommand: AsyncParsableCommand { - + enum OutputFormat: String, ExpressibleByArgument { case json case csv } - - static var configuration = CommandConfiguration( + + static let configuration = CommandConfiguration( commandName: "history", abstract: "Displays historic complexity of the dependency graph" ) diff --git a/Sources/jungle/Commands/Main.swift b/Sources/jungle/Commands/Main.swift index 38b68b9..e00d55e 100644 --- a/Sources/jungle/Commands/Main.swift +++ b/Sources/jungle/Commands/Main.swift @@ -1,8 +1,8 @@ -import ArgumentParser +@preconcurrency import ArgumentParser @main struct Jungle: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "jungle", abstract: "SwiftPM and Cocoapods based projects complexity analyzer.", version: "2.3.2", diff --git a/Sources/jungle/Commands/ModulesCommand.swift b/Sources/jungle/Commands/ModulesCommand.swift index 0cb6be5..be7895f 100644 --- a/Sources/jungle/Commands/ModulesCommand.swift +++ b/Sources/jungle/Commands/ModulesCommand.swift @@ -1,5 +1,5 @@ import Foundation -import ArgumentParser +@preconcurrency import ArgumentParser import Shell import PodExtractor import DependencyGraph @@ -8,7 +8,7 @@ import DependencyModule struct ModulesCommand: ParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "modules", abstract: "Outputs a sorted list of modules dependencies count of your project" ) @@ -41,13 +41,14 @@ struct ModulesCommand: ParsableCommand { private func processPodfile(at directoryURL: URL) throws { // Choose the target to analyze let podfileJSON = try shell("pod ipc podfile-json Podfile --silent", at: directoryURL) - + guard let currentTargetDependencies = try moduleFromJSONPodfile(podfileJSON, onTarget: target) else { throw CompareError.targetNotFound(target: target) } let targetDependencies = currentTargetDependencies.dependencies - - let podfileLock = try shell("git show HEAD:Podfile.lock", at: directoryURL) + + let podfileLockURL = directoryURL.appendingPathComponent("Podfile.lock") + let podfileLock = try String(contentsOf: podfileLockURL, encoding: .utf8) let allPodfileModules = try extractModulesFromPodfileLock(podfileLock) let realTargetDependencies = allPodfileModules.filter { targetDependencies.contains($0.name) }