From 903ea146e48eeb4663e00803e64bfd65a86248f8 Mon Sep 17 00:00:00 2001 From: Cengiz Date: Thu, 7 May 2026 16:34:12 +0600 Subject: [PATCH] Fixed dependency leak --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + .github/ISSUE_TEMPLATE/feature_request.yaml | 1 + .github/config/.rubocop.yml | 319 ++++++++++++++++++ .github/workflows/android-ci.yml | 9 +- .github/workflows/android-style.yml | 8 +- .github/workflows/auto-label-issues.yml | 7 +- .github/workflows/gdscript-style.yml | 6 +- .github/workflows/gradle-kts-style.yml | 6 +- .github/workflows/ios-ci.yml | 7 +- .github/workflows/ios-style.yml | 6 +- .github/workflows/label-sync.yml | 9 +- .github/workflows/properties-style.yml | 6 +- .github/workflows/release-on-milestone.yml | 9 +- .github/workflows/script-style.yml | 6 +- addon/addon-build.gradle.kts | 6 +- android/android-build.gradle.kts | 73 +++- common/build-logic/gradle.properties | 5 + common/build-logic/logic.gradle.kts | 24 +- .../build-logic/src/main/java/PluginConfig.kt | 12 +- common/build.gradle.kts | 73 +++- common/config/godot.properties | 2 +- common/gradle.properties | 1 + common/gradle/libs.versions.toml | 10 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- docs/README.md | 2 +- ios/config/spm_test_dependencies.json | 3 + ios/ios-build.gradle.kts | 185 +++++++++- ios/plugin.xcodeproj/project.pbxproj | 10 +- script/spm_manager.rb | 144 ++++++-- 29 files changed, 842 insertions(+), 110 deletions(-) create mode 100644 common/build-logic/gradle.properties create mode 100644 ios/config/spm_test_dependencies.json diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d617334..cbc5f0f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -78,6 +78,7 @@ body: label: Plugin Version description: What version of the plugin are you running? options: + - "2.0" - "1.1" - "1.0" - Other diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 1871540..d8596d3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -40,6 +40,7 @@ body: label: Plugin Version description: What version of the plugin are you running? options: + - "2.0" - "1.1" - "1.0" - Other diff --git a/.github/config/.rubocop.yml b/.github/config/.rubocop.yml index c977eb0..8ffecdc 100644 --- a/.github/config/.rubocop.yml +++ b/.github/config/.rubocop.yml @@ -31,3 +31,322 @@ Style/FrozenStringLiteralComment: Style/Documentation: Enabled: false + + AllCops: + NewCops: enable +Gemspec/AddRuntimeDependency: # new in 1.65 + Enabled: true +Gemspec/AttributeAssignment: # new in 1.77 + Enabled: true +Gemspec/DeprecatedAttributeAssignment: # new in 1.30 + Enabled: true +Gemspec/DevelopmentDependencies: # new in 1.44 + Enabled: true +Gemspec/RequireMFA: # new in 1.23 + Enabled: true +Layout/EmptyLinesAfterModuleInclusion: # new in 1.79 + Enabled: true +Layout/LineContinuationLeadingSpace: # new in 1.31 + Enabled: true +Layout/LineContinuationSpacing: # new in 1.31 + Enabled: true +Layout/LineEndStringConcatenationIndentation: # new in 1.18 + Enabled: false +Layout/SpaceBeforeBrackets: # new in 1.7 + Enabled: true +Lint/AmbiguousAssignment: # new in 1.7 + Enabled: true +Lint/AmbiguousOperatorPrecedence: # new in 1.21 + Enabled: true +Lint/AmbiguousRange: # new in 1.19 + Enabled: true +Lint/ArrayLiteralInRegexp: # new in 1.71 + Enabled: true +Lint/ConstantOverwrittenInRescue: # new in 1.31 + Enabled: true +Lint/ConstantReassignment: # new in 1.70 + Enabled: true +Lint/CopDirectiveSyntax: # new in 1.72 + Enabled: true +Lint/DataDefineOverride: # new in 1.85 + Enabled: true +Lint/DeprecatedConstants: # new in 1.8 + Enabled: true +Lint/DuplicateBranch: # new in 1.3 + Enabled: true +Lint/DuplicateMagicComment: # new in 1.37 + Enabled: true +Lint/DuplicateMatchPattern: # new in 1.50 + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 + Enabled: true +Lint/DuplicateSetElement: # new in 1.67 + Enabled: true +Lint/EmptyBlock: # new in 1.1 + Enabled: true +Lint/EmptyClass: # new in 1.3 + Enabled: true +Lint/EmptyInPattern: # new in 1.16 + Enabled: true +Lint/HashNewWithKeywordArgumentsAsDefault: # new in 1.69 + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + Enabled: true +Lint/ItWithoutArgumentsInBlock: # new in 1.59 + Enabled: true +Lint/LambdaWithoutLiteralBlock: # new in 1.8 + Enabled: true +Lint/LiteralAssignmentInCondition: # new in 1.58 + Enabled: true +Lint/MixedCaseRange: # new in 1.53 + Enabled: true +Lint/NoReturnInBeginEndBlocks: # new in 1.2 + Enabled: true +Lint/NonAtomicFileOperation: # new in 1.31 + Enabled: true +Lint/NumberedParameterAssignment: # new in 1.9 + Enabled: true +Lint/NumericOperationWithConstantResult: # new in 1.69 + Enabled: true +Lint/OrAssignmentToConstant: # new in 1.9 + Enabled: true +Lint/RedundantDirGlobSort: # new in 1.8 + Enabled: true +Lint/RedundantRegexpQuantifiers: # new in 1.53 + Enabled: true +Lint/RedundantTypeConversion: # new in 1.72 + Enabled: true +Lint/RefinementImportMethods: # new in 1.27 + Enabled: true +Lint/RequireRangeParentheses: # new in 1.32 + Enabled: true +Lint/RequireRelativeSelfPath: # new in 1.22 + Enabled: true +Lint/SharedMutableDefault: # new in 1.70 + Enabled: true +Lint/SuppressedExceptionInNumberConversion: # new in 1.72 + Enabled: true +Lint/SymbolConversion: # new in 1.9 + Enabled: true +Lint/ToEnumArguments: # new in 1.1 + Enabled: true +Lint/TripleQuotes: # new in 1.9 + Enabled: true +Lint/UnescapedBracketInRegexp: # new in 1.68 + Enabled: true +Lint/UnexpectedBlockArity: # new in 1.5 + Enabled: true +Lint/UnmodifiedReduceAccumulator: # new in 1.1 + Enabled: true +Lint/UnreachablePatternBranch: # new in 1.85 + Enabled: true +Lint/UselessConstantScoping: # new in 1.72 + Enabled: true +Lint/UselessDefaultValueArgument: # new in 1.76 + Enabled: true +Lint/UselessDefined: # new in 1.69 + Enabled: true +Lint/UselessNumericOperation: # new in 1.66 + Enabled: true +Lint/UselessOr: # new in 1.76 + Enabled: true +Lint/UselessRescue: # new in 1.43 + Enabled: true +Lint/UselessRuby2Keywords: # new in 1.23 + Enabled: true +Metrics/CollectionLiteralLength: # new in 1.47 + Enabled: true +Naming/BlockForwarding: # new in 1.24 + Enabled: true +Naming/PredicateMethod: # new in 1.76 + Enabled: true +Security/CompoundHash: # new in 1.28 + Enabled: true +Security/IoMethods: # new in 1.22 + Enabled: true +Style/AmbiguousEndlessMethodDefinition: # new in 1.68 + Enabled: true +Style/ArgumentsForwarding: # new in 1.1 + Enabled: true +Style/ArrayIntersect: # new in 1.40 + Enabled: true +Style/ArrayIntersectWithSingleElement: # new in 1.81 + Enabled: true +Style/BitwisePredicate: # new in 1.68 + Enabled: true +Style/CollectionCompact: # new in 1.2 + Enabled: true +Style/CollectionQuerying: # new in 1.77 + Enabled: true +Style/CombinableDefined: # new in 1.68 + Enabled: true +Style/ComparableBetween: # new in 1.74 + Enabled: true +Style/ComparableClamp: # new in 1.44 + Enabled: true +Style/ConcatArrayLiterals: # new in 1.41 + Enabled: true +Style/DataInheritance: # new in 1.49 + Enabled: true +Style/DigChain: # new in 1.69 + Enabled: true +Style/DirEmpty: # new in 1.48 + Enabled: true +Style/DocumentDynamicEvalDefinition: # new in 1.1 + Enabled: true +Style/EmptyClassDefinition: # new in 1.84 + Enabled: true +Style/EmptyHeredoc: # new in 1.32 + Enabled: true +Style/EmptyStringInsideInterpolation: # new in 1.76 + Enabled: true +Style/EndlessMethod: # new in 1.8 + Enabled: true +Style/EnvHome: # new in 1.29 + Enabled: true +Style/ExactRegexpMatch: # new in 1.51 + Enabled: true +Style/FetchEnvVar: # new in 1.28 + Enabled: true +Style/FileEmpty: # new in 1.48 + Enabled: true +Style/FileNull: # new in 1.69 + Enabled: true +Style/FileOpen: # new in 1.85 + Enabled: true +Style/FileRead: # new in 1.24 + Enabled: true +Style/FileTouch: # new in 1.69 + Enabled: true +Style/FileWrite: # new in 1.24 + Enabled: true +Style/HashConversion: # new in 1.10 + Enabled: true +Style/HashExcept: # new in 1.7 + Enabled: true +Style/HashFetchChain: # new in 1.75 + Enabled: true +Style/HashSlice: # new in 1.71 + Enabled: true +Style/IfWithBooleanLiteralBranches: # new in 1.9 + Enabled: true +Style/InPatternThen: # new in 1.16 + Enabled: true +Style/ItAssignment: # new in 1.70 + Enabled: true +Style/ItBlockParameter: # new in 1.75 + Enabled: true +Style/KeywordArgumentsMerging: # new in 1.68 + Enabled: true +Style/MagicCommentFormat: # new in 1.35 + Enabled: true +Style/MapCompactWithConditionalBlock: # new in 1.30 + Enabled: true +Style/MapIntoArray: # new in 1.63 + Enabled: true +Style/MapJoin: # new in 1.85 + Enabled: true +Style/MapToHash: # new in 1.24 + Enabled: true +Style/MapToSet: # new in 1.42 + Enabled: true +Style/MinMaxComparison: # new in 1.42 + Enabled: true +Style/ModuleMemberExistenceCheck: # new in 1.82 + Enabled: true +Style/MultilineInPatternThen: # new in 1.16 + Enabled: true +Style/NegatedIfElseCondition: # new in 1.2 + Enabled: true +Style/NegativeArrayIndex: # new in 1.84 + Enabled: true +Style/NestedFileDirname: # new in 1.26 + Enabled: true +Style/NilLambda: # new in 1.3 + Enabled: true +Style/NumberedParameters: # new in 1.22 + Enabled: true +Style/NumberedParametersLimit: # new in 1.22 + Enabled: true +Style/ObjectThen: # new in 1.28 + Enabled: true +Style/OneClassPerFile: # new in 1.85 + Enabled: true +Style/OpenStructUse: # new in 1.23 + Enabled: true +Style/OperatorMethodCall: # new in 1.37 + Enabled: true +Style/PartitionInsteadOfDoubleSelect: # new in 1.85 + Enabled: true +Style/PredicateWithKind: # new in 1.85 + Enabled: true +Style/QuotedSymbols: # new in 1.16 + Enabled: true +Style/ReduceToHash: # new in 1.85 + Enabled: true +Style/RedundantArgument: # new in 1.4 + Enabled: true +Style/RedundantArrayConstructor: # new in 1.52 + Enabled: true +Style/RedundantArrayFlatten: # new in 1.76 + Enabled: true +Style/RedundantConstantBase: # new in 1.40 + Enabled: true +Style/RedundantCurrentDirectoryInPath: # new in 1.53 + Enabled: true +Style/RedundantDoubleSplatHashBraces: # new in 1.41 + Enabled: true +Style/RedundantEach: # new in 1.38 + Enabled: true +Style/RedundantFilterChain: # new in 1.52 + Enabled: true +Style/RedundantFormat: # new in 1.72 + Enabled: true +Style/RedundantHeredocDelimiterQuotes: # new in 1.45 + Enabled: true +Style/RedundantInitialize: # new in 1.27 + Enabled: true +Style/RedundantInterpolationUnfreeze: # new in 1.66 + Enabled: true +Style/RedundantLineContinuation: # new in 1.49 + Enabled: true +Style/RedundantMinMaxBy: # new in 1.85 + Enabled: true +Style/RedundantRegexpArgument: # new in 1.53 + Enabled: true +Style/RedundantRegexpConstructor: # new in 1.52 + Enabled: true +Style/RedundantSelfAssignmentBranch: # new in 1.19 + Enabled: true +Style/RedundantStringEscape: # new in 1.37 + Enabled: true +Style/RedundantStructKeywordInit: # new in 1.85 + Enabled: true +Style/ReturnNilInPredicateMethodDefinition: # new in 1.53 + Enabled: true +Style/ReverseFind: # new in 1.84 + Enabled: true +Style/SafeNavigationChainLength: # new in 1.68 + Enabled: true +Style/SelectByKind: # new in 1.85 + Enabled: true +Style/SelectByRange: # new in 1.85 + Enabled: true +Style/SelectByRegexp: # new in 1.22 + Enabled: true +Style/SendWithLiteralMethodName: # new in 1.64 + Enabled: true +Style/SingleLineDoEndBlock: # new in 1.57 + Enabled: true +Style/StringChars: # new in 1.12 + Enabled: true +Style/SuperArguments: # new in 1.64 + Enabled: true +Style/SuperWithArgsParentheses: # new in 1.58 + Enabled: true +Style/SwapValues: # new in 1.1 + Enabled: true +Style/TallyMethod: # new in 1.85 + Enabled: true +Style/YAMLFileRead: # new in 1.53 + Enabled: true diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 3c88cc5..2ee4cd5 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -6,7 +6,8 @@ name: Android CI on: pull_request: - branches: [ main ] + branches: + - main paths: - 'common/config/**' - 'common/**/*.gradle.kts' @@ -21,7 +22,7 @@ jobs: steps: # -- Checkout ------------------------------------------------------------ - - name: Checkout repository + - name: Checkout uses: actions/checkout@v6 # -- Node ---------------------------------------------------------------- @@ -38,7 +39,7 @@ jobs: # -- Android SDK --------------------------------------------------------- - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@v4 # -- Gradle -------------------------------------------------------------- - name: Make Gradle wrapper executable @@ -65,7 +66,7 @@ jobs: # the step above fails (always: true keeps this step active on failure). - name: Upload Android test results if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: android-test-results path: android/build/test-results/testDebugUnitTest/ diff --git a/.github/workflows/android-style.yml b/.github/workflows/android-style.yml index c7945e9..4b5aada 100644 --- a/.github/workflows/android-style.yml +++ b/.github/workflows/android-style.yml @@ -6,7 +6,8 @@ name: Android Code Style on: pull_request: - branches: [ main ] + branches: + - main paths: - 'android/src/**/*.java' - 'android/src/**/*.kt' @@ -17,7 +18,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - name: Set up Java uses: actions/setup-java@v5 @@ -29,7 +31,7 @@ jobs: uses: ./.github/actions/install-ktlint - name: Run ktlint - run: ktlint "android/src/**/*.kt" + run: ktlint "android/src/**/*.kt" "!android/src/**/*Plugin.kt" - name: Run Checkstyle run: | diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml index 3830b19..c8979b3 100644 --- a/.github/workflows/auto-label-issues.yml +++ b/.github/workflows/auto-label-issues.yml @@ -6,7 +6,8 @@ name: Auto Label Issues on: issues: - types: [opened] + types: + - opened jobs: triage: @@ -16,13 +17,13 @@ jobs: contents: read # Required to read the labels.yml file steps: - - name: Checkout Repository + - name: Checkout uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.14' + python-version: 3.x - name: Install Dependencies run: pip install pyyaml google-genai requests diff --git a/.github/workflows/gdscript-style.yml b/.github/workflows/gdscript-style.yml index 4755b38..78ec148 100644 --- a/.github/workflows/gdscript-style.yml +++ b/.github/workflows/gdscript-style.yml @@ -6,7 +6,8 @@ name: GDScript Code Style on: pull_request: - branches: [ main ] + branches: + - main paths: - 'addon/src/**/*.gd' - 'demo/**/*.gd' @@ -16,7 +17,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - name: Install gdtoolkit run: | diff --git a/.github/workflows/gradle-kts-style.yml b/.github/workflows/gradle-kts-style.yml index 8fc54c8..694ca60 100644 --- a/.github/workflows/gradle-kts-style.yml +++ b/.github/workflows/gradle-kts-style.yml @@ -6,7 +6,8 @@ name: Gradle Kotlin DSL Style on: pull_request: - branches: [ main ] + branches: + - main paths: - '**/*.gradle.kts' @@ -15,7 +16,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - name: Install Node uses: ./.github/actions/install-node diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index b1d5862..c3a7820 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -6,7 +6,8 @@ name: iOS CI on: pull_request: - branches: [ main ] + branches: + - main paths: - 'common/config/**' - 'common/**/*.gradle.kts' @@ -24,7 +25,7 @@ jobs: steps: # -- Checkout ------------------------------------------------------------ - - name: Checkout repository + - name: Checkout uses: actions/checkout@v6 # -- Node ---------------------------------------------------------------- @@ -69,7 +70,7 @@ jobs: - name: Upload iOS test results if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ios-test-results path: ios/build/TestResults/ diff --git a/.github/workflows/ios-style.yml b/.github/workflows/ios-style.yml index 585948a..1e9824e 100644 --- a/.github/workflows/ios-style.yml +++ b/.github/workflows/ios-style.yml @@ -6,7 +6,8 @@ name: iOS Code Style on: pull_request: - branches: [ main ] + branches: + - main paths: - 'ios/src/**/*.m' - 'ios/src/**/*.mm' @@ -18,7 +19,8 @@ jobs: runs-on: macos-latest steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - name: Install SwiftLint run: brew install swiftlint diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml index 5a3ef5c..186d3ae 100644 --- a/.github/workflows/label-sync.yml +++ b/.github/workflows/label-sync.yml @@ -6,7 +6,8 @@ name: Sync Labels on: push: - branches: [ main ] + branches: + - main paths: - '.github/config/labels.yml' @@ -18,11 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 with: sparse-checkout: .github/config/labels.yml - - uses: EndBug/label-sync@v2 + - name: Label Sync + uses: EndBug/label-sync@v2 with: config-file: .github/config/labels.yml delete-other-labels: true diff --git a/.github/workflows/properties-style.yml b/.github/workflows/properties-style.yml index 492d6f3..bd1a2e5 100644 --- a/.github/workflows/properties-style.yml +++ b/.github/workflows/properties-style.yml @@ -6,7 +6,8 @@ name: Properties File Style on: pull_request: - branches: [ main ] + branches: + - main paths: - 'common/config/*.properties' - 'ios/config/*.properties' @@ -16,7 +17,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - name: Install editorconfig-checker uses: ./.github/actions/install-ec diff --git a/.github/workflows/release-on-milestone.yml b/.github/workflows/release-on-milestone.yml index 5aed4e4..e668f56 100644 --- a/.github/workflows/release-on-milestone.yml +++ b/.github/workflows/release-on-milestone.yml @@ -6,7 +6,8 @@ name: Draft Release on Milestone Close on: milestone: - types: [closed] + types: + - closed workflow_dispatch: inputs: version: @@ -29,7 +30,7 @@ jobs: steps: # -- Checkout ------------------------------------------------------------ - - name: Checkout repository + - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 @@ -82,7 +83,7 @@ jobs: # -- Prepare for Android build ------------------------------------------- - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@v4 # -- Create archives ----------------------------------------------------- - name: Run createArchives @@ -118,7 +119,7 @@ jobs: --milestone "${EXPECTED_VERSION}" \ --state closed \ --json number \ - --jq '.[].number | " - #\(.)"' || echo " - (No milestone issues linked)") + --jq '.[].number | "- #\(.)"' || echo "- (No milestone issues linked)") gh release create "$TAG_NAME" \ --draft \ diff --git a/.github/workflows/script-style.yml b/.github/workflows/script-style.yml index 61f3848..016f118 100644 --- a/.github/workflows/script-style.yml +++ b/.github/workflows/script-style.yml @@ -6,7 +6,8 @@ name: Script Code Style on: pull_request: - branches: [ main ] + branches: + - main paths: - 'script/**/*.sh' - 'script/**/*.rb' @@ -16,7 +17,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - name: Install Node uses: ./.github/actions/install-node diff --git a/addon/addon-build.gradle.kts b/addon/addon-build.gradle.kts index 7a8f12b..011339a 100644 --- a/addon/addon-build.gradle.kts +++ b/addon/addon-build.gradle.kts @@ -21,7 +21,11 @@ val androidDependencies = extensions .getByType() .named("libs") - .run { libraryAliases.map { findLibrary(it).get().get() } } + .run { + libraryAliases + .filter { it.startsWith("runtime.") } + .map { findLibrary(it).get().get() } + } // -- Helpers ------------------------------------------------------------------- diff --git a/android/android-build.gradle.kts b/android/android-build.gradle.kts index d0af75f..9ec19e3 100644 --- a/android/android-build.gradle.kts +++ b/android/android-build.gradle.kts @@ -309,13 +309,13 @@ node { // -- Dependencies -------------------------------------------------------------- -val androidDependencies = +val runtimeDependencies = extensions .getByType() .named("libs") .run { libraryAliases - .filter { it != "rewrite.static.analysis" && !it.startsWith("test.") } + .filter { it.startsWith("runtime.") } .map { findLibrary(it).get().get() } } @@ -339,11 +339,26 @@ val testRuntimeOnlyDependencies = val artifactType = Attribute.of("artifactType", String::class.java) dependencies { - "rewrite"(libs.rewrite.static.analysis) + "rewrite"(libs.style.rewrite.static.analysis) implementation("godot:godot-lib:${godotConfig.godotVersion}.${godotConfig.godotReleaseType}@aar") - androidDependencies.forEach { implementation(it) } - testDependencies.forEach { testImplementation(it) } - testRuntimeOnlyDependencies.forEach { testRuntimeOnly(it) } + + println("DEBUG: Runtime Dependencies") + runtimeDependencies.forEach { + println("DEBUG: Adding to runtime: $it") + implementation(it) + } + + println("DEBUG: Test Dependencies") + testDependencies.forEach { dependency -> + println("DEBUG: Adding to test: $dependency") + testImplementation(dependency) + } + + println("DEBUG: Test Runtime Only Dependencies") + testRuntimeOnlyDependencies.forEach { + println("DEBUG: Adding to testRuntimeOnly: $it") + testRuntimeOnly(it) + } attributesSchema { attribute(artifactType) { @@ -559,6 +574,50 @@ tasks { overwrite(false) } + val kotlinSourceFiles = + fileTree("src") { + include("**/*.kt") + exclude("**/*Plugin.kt") + }.files + .map { it.relativeTo(projectDir).path } + .sorted() + + register("checkKotlinFormat") { + description = "Checks ktlint compliance of Kotlin source files under \$projectDir/src" + group = "verification" + workingDir = projectDir + + doFirst { + if (kotlinSourceFiles.isNotEmpty()) { + commandLine(listOf("ktlint") + kotlinSourceFiles) + } else { + logger.lifecycle("checkKotlinFormat: No source files found to format.") + + // Set commandLine to something harmless so Exec doesn't fail + // if it expects a command to be set + commandLine("true") + } + } + } + + register("formatKotlinSource") { + description = "Formats Kotlin source files under $projectDir/src in-place using ktlint" + group = "formatting" + workingDir = projectDir + + doFirst { + if (kotlinSourceFiles.isNotEmpty()) { + commandLine(listOf("ktlint", "--format") + kotlinSourceFiles) + } else { + logger.lifecycle("formatKotlinSource: No source files found to format.") + + // Set commandLine to something harmless so Exec doesn't fail + // if it expects a command to be set + commandLine("true") + } + } + } + named("preBuild") { dependsOn("downloadGodotAar") } @@ -605,7 +664,7 @@ tasks.withType { ignoreFailures = true } -// 🔥 Final fix: afterEvaluate guarantees the test task exists when we configure it +// afterEvaluate guarantees the test task exists when configured afterEvaluate { tasks.named("testDebugUnitTest") { // This tells Gradle: coverage report MUST run AFTER the test task finishes diff --git a/common/build-logic/gradle.properties b/common/build-logic/gradle.properties new file mode 100644 index 0000000..f79abf4 --- /dev/null +++ b/common/build-logic/gradle.properties @@ -0,0 +1,5 @@ +# +# © 2026-present https://github.com/cengiz-pz +# + +kotlin.incremental=false diff --git a/common/build-logic/logic.gradle.kts b/common/build-logic/logic.gradle.kts index d464869..f586109 100644 --- a/common/build-logic/logic.gradle.kts +++ b/common/build-logic/logic.gradle.kts @@ -17,18 +17,38 @@ plugins { alias(libs.plugins.kotlin.serialization) } +val buildLogicDependencies = + extensions + .getByType() + .named("libs") + .run { + libraryAliases + .filter { it.startsWith("build.logic.") } + .map { findLibrary(it).get().get() } + } + dependencies { - implementation(libs.kotlinx.serialization.json) + println("DEBUG: BUILD LOGIC IMPLEMENTATION Dependencies") + buildLogicDependencies.forEach { + println("DEBUG: Adding to runtime: $it") + implementation(it) + } } sourceSets { main { - java.srcDirs("src/main/kotlin") + java.srcDirs("src/main/java") } } kotlin { jvmToolchain(17) + + sourceSets { + getByName("main") { + kotlin.srcDir("src/main/java") + } + } } tasks.withType().configureEach { diff --git a/common/build-logic/src/main/java/PluginConfig.kt b/common/build-logic/src/main/java/PluginConfig.kt index e239a67..496b960 100644 --- a/common/build-logic/src/main/java/PluginConfig.kt +++ b/common/build-logic/src/main/java/PluginConfig.kt @@ -23,24 +23,24 @@ import java.util.Properties * are computed from the raw values and are never stored in the `.properties` file. */ data class PluginConfig( - /** Human-readable node name, e.g. `NativeCamera`. */ + /** Human-readable node name, e.g. `MyNode`. */ val pluginNodeName: String, - /** Snake-case native module name, e.g. `native_camera`. */ + /** Snake-case native module name, e.g. `my_node`. */ val pluginModuleName: String, - /** Fully-qualified Java/Kotlin package, e.g. `org.godotengine.plugin.nativecamera`. */ + /** Fully-qualified Java/Kotlin package, e.g. `org.godotengine.plugin.mynode`. */ val pluginPackageName: String, /** Semantic version string, e.g. `1.0`. */ val pluginVersion: String, ) { - /** Full plugin name used for AAR filenames and GDScript class names, e.g. `NativeCameraPlugin`. */ + /** Full plugin name used for AAR filenames and GDScript class names, e.g. `MyPlugin`. */ val pluginName: String get() = "${pluginNodeName}Plugin" - /** iOS gdnlib initialisation symbol, e.g. `native_camera_plugin_init`. */ + /** iOS gdnlib initialisation symbol, e.g. `my_node_plugin_init`. */ val iosInitializationMethod: String get() = "${pluginModuleName}_plugin_init" - /** iOS gdnlib de-initialisation symbol, e.g. `native_camera_plugin_deinit`. */ + /** iOS gdnlib de-initialisation symbol, e.g. `my_node_plugin_deinit`. */ val iosDeinitializationMethod: String get() = "${pluginModuleName}_plugin_deinit" diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 2db1091..a49f42f 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -9,7 +9,6 @@ plugins { alias(libs.plugins.undercouch.download) apply false alias(libs.plugins.openrewrite) apply false alias(libs.plugins.node) apply false - alias(libs.plugins.kotlin.serialization) apply false } // -- Load config data class ---------------------------------------------------- @@ -154,7 +153,7 @@ tasks { ).joinToString(" -o ") { "-name \"$it\"" } val excludePatterns = - listOf("node_modules", ".git", "build", ".gradle", ".idea", "bin", "release") + listOf("node_modules", ".git", "build", ".gradle", ".idea", "bin", "release", "framework") .joinToString(" ") { "-not -path \"*/$it/*\"" } commandLine( @@ -269,17 +268,85 @@ tasks { } } + val rubySourceFiles = + fileTree("$repositoryRootDir/script") { include("**/*.rb") } + .files + .map { it.absolutePath } + .sorted() + + fun checkRubocop(project: Project) { + val rubocopAvailable = + project + .exec { + commandLine("which", "rubocop") + isIgnoreExitValue = true + }.exitValue == 0 + if (!rubocopAvailable) { + throw GradleException("rubocop is not installed or not on PATH.") + } + } + + register("checkRubyScriptFormat") { + description = "Checks Rubocop compliance of all Ruby scripts under script/" + group = "verification" + + doLast { + checkRubocop(project) + if (rubySourceFiles.isEmpty()) { + throw GradleException("checkRubyScriptFormat: no *.rb files found.") + } + + project.exec { + workingDir = file(repositoryRootDir) + commandLine( + listOf( + "rubocop", + "--config", + "$repositoryRootDir/.github/config/.rubocop.yml", + ) + rubySourceFiles, + ) + } + } + } + + register("applyRubyScriptFormat") { + description = "Applies Rubocop suggested fixes to Ruby scripts under script/" + group = "formatting" + + doLast { + checkRubocop(project) + if (rubySourceFiles.isEmpty()) { + throw GradleException("applyRubyScriptFormat: no *.rb files found.") + } + + project.exec { + workingDir = file(repositoryRootDir) + isIgnoreExitValue = true + commandLine( + listOf( + "rubocop", + "--config", + "$repositoryRootDir/.github/config/.rubocop.yml", + "--autocorrect", + ) + rubySourceFiles, + ) + } + } + } + register("checkFormat") { description = "Validates format in all source code" group = "verification" dependsOn( project(":addon").tasks.named("checkGdscriptFormat"), project(":android").tasks.named("checkJavaFormat"), + project(":android").tasks.named("checkKotlinFormat"), project(":android").tasks.named("checkXmlFormat"), project(":ios").tasks.named("checkObjCFormat"), project(":ios").tasks.named("checkSwiftFormat"), "checkKtsFormat", "checkBashScriptFormat", + "checkRubyScriptFormat", "checkEditorConfig", ) } @@ -290,11 +357,13 @@ tasks { dependsOn( project(":addon").tasks.named("formatGdscriptSource"), project(":android").tasks.named("rewriteRun"), + project(":android").tasks.named("formatKotlinSource"), project(":android").tasks.named("formatXml"), project(":ios").tasks.named("formatObjCSource"), project(":ios").tasks.named("formatSwiftSource"), "formatKtsSource", "applyBashScriptFormat", + "applyRubyScriptFormat", ) } } diff --git a/common/config/godot.properties b/common/config/godot.properties index 344713d..e98282b 100644 --- a/common/config/godot.properties +++ b/common/config/godot.properties @@ -3,4 +3,4 @@ # godotVersion=4.7 -godotReleaseType=dev5 +godotReleaseType=beta1 diff --git a/common/gradle.properties b/common/gradle.properties index f1ef47c..99cdcc9 100644 --- a/common/gradle.properties +++ b/common/gradle.properties @@ -7,3 +7,4 @@ org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 android.useAndroidX=true android.nonTransitiveRClass=true kotlin.code.style=official +kotlin.incremental=false diff --git a/common/gradle/libs.versions.toml b/common/gradle/libs.versions.toml index fda4c1a..5d88683 100644 --- a/common/gradle/libs.versions.toml +++ b/common/gradle/libs.versions.toml @@ -23,6 +23,8 @@ kotlinx-serialization = "1.6.3" # -- Test dependencies --------------------------------------------------------- junit-jupiter = "6.0.3" mockito = "5.23.0" +mockk = "1.13.17" +opengl-api = "gl1.1-android-2.1_r1" # -- Godot plugin dependencies ------------------------------------------------- appcompat = "1.7.1" @@ -30,19 +32,21 @@ appcompat = "1.7.1" [libraries] # -- Build tool ---------------------------------------------------------------- -rewrite-static-analysis = { module = "org.openrewrite.recipe:rewrite-static-analysis", version.ref = "rewrite-static-analysis" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +style-rewrite-static-analysis = { module = "org.openrewrite.recipe:rewrite-static-analysis", version.ref = "rewrite-static-analysis" } +build-logic-kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } # -- Test libraries (aliases starting with "test-" are loaded as testImplementation) -- test-junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } test-mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } test-mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } # -- Runtime-only test libraries (aliases starting with "test-runtime-" -> testRuntimeOnly) -- test-runtime-junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-jupiter" } +test-runtime-opengl-api = { module = "org.khronos:opengl-api", version.ref = "opengl-api" } # -- Godot plugin -------------------------------------------------------------- -androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +runtime-androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } [plugins] diff --git a/common/gradle/wrapper/gradle-wrapper.properties b/common/gradle/wrapper/gradle-wrapper.properties index 89ff833..70cb0a6 100644 --- a/common/gradle/wrapper/gradle-wrapper.properties +++ b/common/gradle/wrapper/gradle-wrapper.properties @@ -3,7 +3,7 @@ # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/docs/README.md b/docs/README.md index 72bcd74..1196b16 100644 --- a/docs/README.md +++ b/docs/README.md @@ -296,7 +296,7 @@ Helpful resources: | | [Admob](https://github.com/godot-sdk-integrations/godot-admob) | ✅ | ✅ | | | | | | [Connection State](https://github.com/godot-mobile-plugins/godot-connection-state) | ✅ | ✅ | | | | | | [Deeplink](https://github.com/godot-mobile-plugins/godot-deeplink) | ✅ | ✅ | | | | -| | [Firebase](https://github.com/godot-mobile-plugins/godot-firebase) | ⏰ | ⏰ | 🔜 | - | | +| | [Firebase](https://github.com/godot-mobile-plugins/godot-firebase) | ✅ | ✅ | | | | | | [In-App Review](https://github.com/godot-mobile-plugins/godot-inapp-review) | ✅ | ✅ | | | | | | [Native Camera](https://github.com/godot-mobile-plugins/godot-native-camera) | ✅ | ✅ | | | | | | [Notification Scheduler](https://github.com/godot-mobile-plugins/godot-notification-scheduler) | ✅ | ✅ | | | | diff --git a/ios/config/spm_test_dependencies.json b/ios/config/spm_test_dependencies.json new file mode 100644 index 0000000..41b42e6 --- /dev/null +++ b/ios/config/spm_test_dependencies.json @@ -0,0 +1,3 @@ +[ + +] diff --git a/ios/ios-build.gradle.kts b/ios/ios-build.gradle.kts index de2c2d4..b61545f 100644 --- a/ios/ios-build.gradle.kts +++ b/ios/ios-build.gradle.kts @@ -136,6 +136,91 @@ fun TaskContainerScope.registerIosBuildTask( "Failed to rename ${builtLib.absolutePath} to ${renamedLib.absolutePath}", ) } + + // Inject ExecOperations using your predefined Injected interface + val execOps = project.objects.newInstance().execOps + + // -- Dynamically strip SPM dependency objects from the static library -- + // + // xcodebuild archives every compiled SPM package object into the .a via + // libtool, even when --no-link is used (that only prevents Frameworks-phase + // linking). We enumerate the .a members with `ar -t`, identify the ones + // that belong to Firebase / SPM packages (i.e. anything NOT in the plugin's + // own src/ directory), and delete them one-by-one with `ar -d`. + // + // macOS `ar -t` prints one member name per line with no path prefix. + // macOS `ar -d` accepts exactly one member name per invocation — passing + // multiple names in a single call silently processes only the first. + println("Inspecting archive for SPM dependency objects to strip...") + + val listOutput = java.io.ByteArrayOutputStream() + execOps.exec { + commandLine("xcrun", "ar", "-t", renamedLib.absolutePath) + standardOutput = listOutput + } + + // `ar -t` gives one bare filename per line, e.g. "FirebaseAuth.o" + val allObjects: List = + listOutput + .toString("UTF-8") + .lines() + .map { it.trim() } + .filter { it.endsWith(".o") } + + // Build the set of object names that belong to the plugin's own sources. + // Each source file compiles to .o in the archive. + val pluginSrcDir = file("$projectDir/src") + val pluginObjectNames: Set = + fileTree(pluginSrcDir) { + include("**/*.mm", "**/*.m", "**/*.swift", "**/*.cpp") + }.files.map { it.nameWithoutExtension + ".o" }.toSet() + + // In Release (WMO) builds Xcode merges all Swift sources into a single + // object named .o. Preserve it unconditionally. + val wmoObjectName = "${pluginConfig.pluginModuleName}_plugin.o" + + // Everything not produced by the plugin's own source files is a foreign + // SPM dependency that must be stripped. + val objectsToStrip: List = + allObjects.filter { obj -> + obj !in pluginObjectNames && obj != wmoObjectName + } + + if (objectsToStrip.isEmpty()) { + println("No SPM dependency objects found — archive is already clean.") + } else { + println("Stripping ${objectsToStrip.size} SPM object(s) from ${renamedLib.name}:") + var strippedCount = 0 + var failedObjects = mutableListOf() + + // macOS ar -d requires one member per invocation. + objectsToStrip.forEach { obj -> + val result = + execOps.exec { + commandLine("xcrun", "ar", "-d", renamedLib.absolutePath, obj) + isIgnoreExitValue = true + } + if (result.exitValue == 0) { + println(" Stripped: $obj") + strippedCount++ + } else { + // Member may be absent in some build variants — not fatal. + logger.debug(" $obj not present in ${renamedLib.name} (skipped)") + failedObjects.add(obj) + } + } + + if (failedObjects.isNotEmpty()) { + logger.warn( + "Warning: {} object(s) could not be stripped: {}. " + + "Verify with: xcrun ar -t {}", + failedObjects.size, + failedObjects, + renamedLib.absolutePath, + ) + } + println("Stripped $strippedCount/${objectsToStrip.size} SPM object(s) from ${renamedLib.name}") + } println("iOS build completed at: ${buildTimestamp()}") } } @@ -183,6 +268,12 @@ fun TaskContainerScope.registerIosTestTask( } doFirst { + // Delete the existing result bundle to avoid xcodebuild errors + val resultBundle = testResultsDir.resolve("$name.xcresult") + if (resultBundle.exists()) { + resultBundle.deleteRecursively() + } + testResultsDir.mkdirs() // Try pulling from Gradle extra properties first (set by bootiOSSimulator), then fallback to env @@ -214,7 +305,7 @@ fun TaskContainerScope.registerIosTestTask( "-derivedDataPath", derivedDataDir.absolutePath, "-resultBundlePath", - testResultsDir.resolve("$name.xcresult").absolutePath, + resultBundle.absolutePath, "-enableCodeCoverage", "YES", "GODOT_DIR=$godotDir", @@ -353,10 +444,11 @@ tasks { val godotDirectory = file(godotDir) val versionFile = godotDirectory.resolve("GODOT_VERSION") - val filename = "godot-headers-${godotConfig.godotVersion}-${godotConfig.godotReleaseType}.zip" + val expectedVersionString = "${godotConfig.godotVersion}-${godotConfig.godotReleaseType}" + val filename = "godot-headers-$expectedVersionString.zip" val releaseUrl = "https://github.com/godot-mobile-plugins/godot-headers/releases/download/" + - "${godotConfig.godotVersion}-${godotConfig.godotReleaseType}/$filename" + "$expectedVersionString/$filename" val archiveFile = file("$godotDir.zip") inputs.property("godotVersion", godotConfig.godotVersion) @@ -364,10 +456,10 @@ tasks { inputs.property("godotDir", godotDir) onlyIf { - if (versionFile.exists() && versionFile.readText().trim() == godotConfig.godotVersion) { + if (versionFile.exists() && versionFile.readText().trim() == expectedVersionString) { logger.info( "Godot {} already present in {}. Skipping download.", - godotConfig.godotVersion, + expectedVersionString, godotDirectory.absolutePath, ) return@onlyIf false @@ -387,9 +479,9 @@ tasks { throw GradleException( "ERROR: Godot directory '${godotDirectory.absolutePath}' already exists but " + "contains version '$existingVersion', which does not match the " + - "configured version '${godotConfig.godotVersion}'. " + + "configured version '$expectedVersionString'. " + "Remove the directory (or run 'removeGodotDirectory') before downloading again, " + - "or update 'godotVersion' in config/godot.properties.", + "or update 'godotVersion' or 'godotReleaseType' in config/godot.properties.", ) } } @@ -408,10 +500,10 @@ tasks { } archiveFile.delete() - versionFile.writeText(godotConfig.godotVersion) + versionFile.writeText(expectedVersionString) println( - "Godot headers ${godotConfig.godotVersion}-${godotConfig.godotReleaseType} successfully " + + "Godot headers $expectedVersionString successfully " + "downloaded and extracted to ${godotDirectory.absolutePath}", ) } @@ -497,13 +589,33 @@ tasks { println("Warning: No dependencies found for plugin. Skipping SPM dependency removal.") } else { println("Removing SPM dependencies from project...") + val moduleName = "${pluginConfig.pluginModuleName}_plugin" + val testTargetName = "${pluginConfig.pluginModuleName}_plugin_tests" + deps.forEach { dep -> dep.products.forEach { product -> + // Remove from module target + execOps.exec { + commandLine( + "ruby", + "$scriptDir/spm_manager.rb", + "-d", + "--target", + moduleName, + xcodeproj, + dep.url, + dep.version, + product, + ) + } + // Remove from test target execOps.exec { commandLine( "ruby", "$scriptDir/spm_manager.rb", "-d", + "--target", + testTargetName, xcodeproj, dep.url, dep.version, @@ -595,14 +707,28 @@ tasks { val xcodeproj = "$projectDir/plugin.xcodeproj" val scriptDir = file("$repositoryRootDir/script") + // -- Module target: compile-only (--no-link) ---------------------- + // Firebase frameworks must be in packageProductDependencies so the + // Swift compiler can resolve their modules (Authentication.swift, + // AuthProviding.swift etc. import them). However they must NOT be + // linked into FirebasePlugin.a — the consuming Godot app links them + // independently, and duplicate symbols would cause export failure. + val moduleName = "${pluginConfig.pluginModuleName}_plugin" + val testTargetName = "${pluginConfig.pluginModuleName}_plugin_tests" + println("Updating Xcode project with SPM dependencies...") + println(" - Module target '$moduleName' (compile-only, not linked):") deps.forEach { dep -> dep.products.forEach { product -> + println(" • $product") execOps.exec { commandLine( "ruby", "$scriptDir/spm_manager.rb", "-a", + "--target", + moduleName, + "--no-link", xcodeproj, dep.url, dep.version, @@ -611,6 +737,33 @@ tasks { } } } + + // -- Test target: compile + link (normal) -------------------------- + // The test target compiles Swift files directly (not via the .a) and + // must link Firebase frameworks itself. + val spmTestConfigFile = file("$projectDir/config/spm_test_dependencies.json") + val testDeps = if (spmTestConfigFile.exists()) readSpmDependencies(spmTestConfigFile) else deps + + println(" - Test target '$testTargetName' (compile + link):") + testDeps.forEach { dep -> + dep.products.forEach { product -> + println(" • $product") + execOps.exec { + commandLine( + "ruby", + "$scriptDir/spm_manager.rb", + "-a", + "--target", + testTargetName, + xcodeproj, + dep.url, + dep.version, + product, + ) + } + } + } + println("SPM update completed.") } } @@ -624,6 +777,7 @@ tasks { val xcodeproj = "$projectDir/plugin.xcodeproj" inputs.file("$projectDir/config/spm_dependencies.json") + inputs.file("$projectDir/config/spm_test_dependencies.json") inputs.files(fileTree(xcodeproj) { include("**/*.pbxproj", "**/project.pbxproj") }) outputs.file("$xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved") @@ -698,17 +852,20 @@ tasks { } register("validateGodotVersion") { - description = "Validates that the Godot version in godotDir matches the configured godotVersion" + description = "Validates that the Godot version in godotDir matches the configured godotVersion and releaseType" group = "verification" dependsOn("downloadGodotHeaders") + val expectedVersionString = "${godotConfig.godotVersion}-${godotConfig.godotReleaseType}" + inputs.property("godotVersion", godotConfig.godotVersion) + inputs.property("godotReleaseType", godotConfig.godotReleaseType) inputs.property("godotDirPath", godotDir) outputs.upToDateWhen { val vf = java.io.File("$godotDir/GODOT_VERSION") - vf.exists() && vf.readText().trim() == godotConfig.godotVersion + vf.exists() && vf.readText().trim() == expectedVersionString } doLast { @@ -722,16 +879,16 @@ tasks { } val downloadedVersion = versionFile.readText().trim() - if (downloadedVersion != godotConfig.godotVersion) { + if (downloadedVersion != expectedVersionString) { throw GradleException( "Godot version mismatch!\n" + - " Expected (config/godot.properties): ${godotConfig.godotVersion}\n" + + " Expected (config/godot.properties): $expectedVersionString\n" + " Found (${versionFile.absolutePath}): $downloadedVersion\n" + "Ensure they match, or run 'removeGodotDirectory' then 'downloadGodotHeaders'.", ) } - logger.lifecycle("Godot version validation passed: {}", godotConfig.godotVersion) + logger.lifecycle("Godot version validation passed: {}", expectedVersionString) } } diff --git a/ios/plugin.xcodeproj/project.pbxproj b/ios/plugin.xcodeproj/project.pbxproj index a14763a..f28cd41 100644 --- a/ios/plugin.xcodeproj/project.pbxproj +++ b/ios/plugin.xcodeproj/project.pbxproj @@ -101,14 +101,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 07964BC82F8F1CF100C0CAC1 /* Recovered References */ = { - isa = PBXGroup; - children = ( - 0721C6C02F82639B00A9BA75 /* XCTest.framework */, - ); - name = "Recovered References"; - sourceTree = ""; - }; 07A6C6792F8F73FE00F484DA /* fixtures */ = { isa = PBXGroup; children = ( @@ -178,7 +170,6 @@ 07DF21702F3EE0E500E2725C /* src */, 90CAAA9524E71FF10013969F /* Products */, 90CAAAA824E72A8A0013969F /* Frameworks */, - 07964BC82F8F1CF100C0CAC1 /* Recovered References */, ); sourceTree = ""; }; @@ -194,6 +185,7 @@ 90CAAAA824E72A8A0013969F /* Frameworks */ = { isa = PBXGroup; children = ( + 0721C6C02F82639B00A9BA75 /* XCTest.framework */, 072C41E02F8F2AA200BA4867 /* AVFoundation.framework */, ); name = Frameworks; diff --git a/script/spm_manager.rb b/script/spm_manager.rb index a80b46d..8e307df 100644 --- a/script/spm_manager.rb +++ b/script/spm_manager.rb @@ -7,28 +7,77 @@ require 'xcodeproj' def print_usage - puts 'Usage: ruby spm_manager.rb -a|-d ' - puts '' - puts 'Options:' - puts ' -a Add the specified SPM dependency to the Xcode project' - puts ' -d Remove the specified SPM dependency from the Xcode project' - puts '' - puts 'Examples:' - puts ' ruby spm_manager.rb -a MyProject.xcodeproj https://github.com/URL 1.0.0 ProductName' - puts ' ruby spm_manager.rb -d MyProject.xcodeproj https://github.com/URL 1.0.0 ProductName' + puts <<~USAGE + Usage: ruby spm_manager.rb -a|-d [--target ] [--no-link] + + Options: + -a Add the specified SPM dependency to the Xcode project + -d Remove the specified SPM dependency from the Xcode project + --target Xcode target to modify. Defaults to the first target. + --no-link Add to packageProductDependencies (compile) but NOT to the + Frameworks build phase (link). Use this for static library + targets that need to compile against a framework without + embedding it — prevents duplicate symbols at link time when + the consuming app also links the same framework. + + Examples: + # Add to first target, linked (original behaviour): + ruby spm_manager.rb -a MyApp.xcodeproj https://github.com/firebase-ios-sdk 11.0.0 FirebaseAuth + + # Add to module target, compile-only (no link) — for static library targets: + ruby spm_manager.rb -a --target firebase_plugin --no-link \\ + MyApp.xcodeproj https://github.com/firebase-ios-sdk 11.0.0 FirebaseAuth + + # Add to test target, linked: + ruby spm_manager.rb -a --target firebase_plugin_tests \\ + MyApp.xcodeproj https://github.com/firebase-ios-sdk 11.0.0 FirebaseAuth + USAGE +end + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +if ARGV.length < 5 + print_usage + exit 1 +end + +option = ARGV[0] +target_name = nil +no_link = false + +remaining = ARGV[1..] + +# Consume flags +loop do + case remaining[0] + when '--target' + if remaining[1].nil? || remaining[1].start_with?('-') + puts 'Error: --target requires a target name argument.' + puts '' + print_usage + exit 1 + end + target_name = remaining[1] + remaining = remaining[2..] + when '--no-link' + no_link = true + remaining = remaining[1..] + else + break + end end -# Argument Validation -if ARGV.length != 5 +if remaining.length != 4 print_usage exit 1 end -option = ARGV[0] -project_path = ARGV[1] -url = ARGV[2].strip -version = ARGV[3].strip -product_name = ARGV[4].strip +project_path, url, version, product_name = remaining +url = url.strip +version = version.strip +product_name = product_name.strip unless ['-a', '-d'].include?(option) puts "Error: Unknown option '#{option}'. Must be -a (add) or -d (remove)." @@ -47,10 +96,25 @@ def print_usage exit 1 end +# --------------------------------------------------------------------------- # Xcode Project Manipulation +# --------------------------------------------------------------------------- begin project = Xcodeproj::Project.open(project_path) - target = project.targets.first + + # Resolve the target by name when --target is given, else use first target. + target = + if target_name + found = project.targets.find { |t| t.name == target_name } + unless found + available = project.targets.map(&:name).join(', ') + puts "Error: Target '#{target_name}' not found. Available targets: #{available}" + exit 1 + end + found + else + project.targets.first + end if target.nil? puts 'Error: No targets found in the Xcode project.' @@ -58,15 +122,15 @@ def print_usage end if option == '-a' - # Check for an existing product dependency with the same name to avoid duplicates existing_dep = target.package_product_dependencies.find do |dep| dep.product_name == product_name end if existing_dep - puts "Warning: Product dependency '#{product_name}' already exists in the project. Skipping add.\n\n" + puts "Warning: Product dependency '#{product_name}' already exists in " \ + "target '#{target.name}'. Skipping add.\n\n" else - # Reuse an existing package reference for the same URL, or create a new one + # Reuse an existing XCRemoteSwiftPackageReference or create one. pkg = project.root_object.package_references.find do |p| p.repositoryURL == url end @@ -80,39 +144,52 @@ def print_usage project.root_object.package_references << pkg end - # Create the product dependency and link it to the shared package reference + # Always add to packageProductDependencies ref = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency) ref.product_name = product_name ref.package = pkg target.package_product_dependencies << ref - filename = File.basename(project_path) - puts "Successfully added SPM dependency '#{product_name}' " \ - "(#{url} @ #{version}) to #{filename}\n\n" + if no_link + puts "Successfully added SPM dependency '#{product_name}' " \ + "(#{url} @ #{version}) to target '#{target.name}' " \ + "[compile-only, not linked] in #{File.basename(project_path)}\n\n" + else + frameworks_phase = target.frameworks_build_phase + frameworks_phase.add_file_reference(ref) + puts "Successfully added SPM dependency '#{product_name}' " \ + "(#{url} @ #{version}) to target '#{target.name}' " \ + "[compile + link] in #{File.basename(project_path)}\n\n" + end end elsif option == '-d' - # Remove the product dependency from the target dep_to_remove = target.package_product_dependencies.find do |dep| dep.product_name == product_name end if dep_to_remove + frameworks_phase = target.frameworks_build_phase + bf = frameworks_phase.files.find do |f| + f.product_ref == dep_to_remove + end + frameworks_phase.remove_file_reference(bf.file_ref) if bf + target.package_product_dependencies.delete(dep_to_remove) dep_to_remove.remove_from_project - puts "Removed product dependency '#{product_name}'." + puts "Removed product dependency '#{product_name}' from target '#{target.name}'." else - puts "Warning: Product dependency '#{product_name}' not found in target. Skipping.\n\n" + puts "Warning: Product dependency '#{product_name}' not found in " \ + "target '#{target.name}'. Skipping.\n\n" end - # Only remove the package reference if no remaining product dependencies still point to it - pkg_to_remove = project.root_object.package_references.find do |pkg| - pkg.repositoryURL == url + pkg_to_remove = project.root_object.package_references.find do |p| + p.repositoryURL == url end if pkg_to_remove - still_in_use = target.package_product_dependencies.any? do |dep| - dep.package == pkg_to_remove + still_in_use = project.targets.any? do |t| + t.package_product_dependencies.any? { |dep| dep.package == pkg_to_remove } end if still_in_use @@ -126,7 +203,8 @@ def print_usage puts "Warning: Package reference '#{url}' not found in project. Skipping.\n\n" end - puts "Successfully removed SPM dependency '#{product_name}' from #{File.basename(project_path)}\n\n" + puts "Successfully removed SPM dependency '#{product_name}' from " \ + "target '#{target.name}' in #{File.basename(project_path)}\n\n" end project.save