Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 435
Stop an ordinary AndroidX dependency from failing the build on a duplicate Kotlin class#5649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
94 commits
Select commit
Hold shift + click to select a range
0dc503e
Stop an ordinary AndroidX dependency from failing the build on a dupl…
shai-almog f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog 6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog 23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog 5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog 0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog 7535785
Stop asking which Kotlin plugin is applied
shai-almog 27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog 0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog 3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog 3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog 4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog 98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog f9ae6ff
An underscore is part of an identifier
shai-almog a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog 786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog 195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog 523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog 858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog 5270ddd
Follow definitions in statement order, and through interpolation
shai-almog c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog 1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog 5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog 08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog 948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog 673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog 665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog 4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog b083292
Count braces instead of naming the constructs that open them
shai-almog f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog 0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog 3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog 72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog 0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog 507954a
Share the backward skip too, and let += assign
shai-almog c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog 25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog 61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog a58c6bb
Expand interpolations inside a stored dependency map
shai-almog 43093d0
Read repeated Gradle calls the way Gradle does
shai-almog 939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog 8bbd20c
Tell the plugin classpath from the application graph
shai-almog 0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog d356c88
Identify an artifact the same way everywhere
shai-almog 8c738ac
Scan the script the way Gradle executes it
shai-almog 7f609b3
Take the lower of two forces for one module
shai-almog 661d5b6
Keep a resolution rule together across every line break in it
shai-almog 6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog dfeb690
One reading for every spelling of "which configuration"
shai-almog 216d59d
The dependency handler has more than one adder
shai-almog daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog d3a841d
The extra properties setter binds its first argument
shai-almog 950fbbd
A custom configuration may inherit the constraint
shai-almog 3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog be23e79
Finish the carriage return, and let a ternary choose
shai-almog aff4b0e
A redundant parenthesis is still the same argument
shai-almog 5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog 1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog b3a84f8
A rejection counts only in the rule that names the family
shai-almog d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog 480af15
Four spellings that read as something they are not
shai-almog 0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog 6140dd3
Scope a destructured name, and let an output helper print
shai-almog 76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog 3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog 9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog 8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog 35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog 13d8420
Drop the Display change; it does not belong in this PR
shai-almog 7492d38
State the overlap as a capability, so no version has to move
shai-almog eb99f79
Record the Android measurement in the class
shai-almog d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog 258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23 maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsAndroid.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58 ...n/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228 .../codenameone-maven-plugin/src/main/java/com/codename1/builders/KotlinStdlibAlignment.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| /* | ||
| * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. | ||
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
| * This code is free software; you can redistribute it and/or modify it | ||
| * under the terms of the GNU General Public License version 2 only, as | ||
| * published by the Free Software Foundation. Codename One designates this | ||
| * particular file as subject to the "Classpath" exception as provided | ||
| * by Oracle in the LICENSE file that accompanied this code. | ||
| * | ||
| * This code is distributed in the hope that it will be useful, but WITHOUT | ||
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
| * version 2 for more details (a copy is included in the LICENSE file that | ||
| * accompanied this code). | ||
| * | ||
| * You should have received a copy of the GNU General Public License version | ||
| * 2 along with this work; if not, write to the Free Software Foundation, | ||
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| * | ||
| * Please contact Codename One through http://www.codenameone.com/ if you | ||
| * need additional information or have any questions. | ||
| */ | ||
| package com.codename1.builders; | ||
| /** | ||
| * The Kotlin stdlib alignment written into the generated Android | ||
| * {@code build.gradle}. | ||
| * | ||
| * <p><b>The failure it prevents.</b> Kotlin 1.8.0 folded the contents of | ||
| * {@code kotlin-stdlib-jdk7} and {@code kotlin-stdlib-jdk8} into | ||
| * {@code kotlin-stdlib} and left the two shims empty. A graph that reaches | ||
| * {@code kotlin-stdlib} 1.8 or newer through one dependency and an older | ||
| * {@code kotlin-stdlib-jdk8} through another therefore carries the same classes | ||
| * twice, and the build fails in {@code checkDuplicateClasses} naming Kotlin | ||
| * artifacts the app never asked for. The 1.8.x line ships no Gradle module | ||
| * metadata saying the two overlap; from 1.9.22 JetBrains ships it.</p> | ||
| * | ||
| * <p><b>Why a capability and not a version constraint.</b> This was a | ||
| * constraint raising both shims to the floor, and a constraint <em>raises a | ||
| * version</em> -- which is a thing an app can be holding down. Measured against | ||
| * a real Gradle, a strict pin or a {@code reject} on a shim turns into | ||
| * {@code Could not resolve ... {strictly 1.6.21}}: a build that resolved before | ||
| * the alignment and does not after it. Guarding that by reading the app's own | ||
| * Gradle for signs of a pin is an unbounded problem, and every round of review | ||
| * found another spelling it missed.</p> | ||
| * | ||
| * <p>Declaring the overlap as a <em>capability</em> has no such failure mode. It | ||
| * states a fact -- from the floor up, {@code kotlin-stdlib} provides what the | ||
| * shims provide -- and lets Gradle drop the redundant shim. No version moves, | ||
| * so there is nothing for a pin, a force, an enforced BOM, a range, a lock or a | ||
| * Kotlin compiler version to conflict with, and nothing to detect. That is why | ||
| * this class has no inputs.</p> | ||
| * | ||
| * <p><b>Measured, not reasoned.</b> The emitted script was run against real | ||
| * Gradle 6.5 (the builder's default) and 8.5 resolving from Maven Central, and | ||
| * end to end through {@code checkDebugDuplicateClasses} in a real Android | ||
| * project on AGP 8.1.4:</p> | ||
| * | ||
| * <ul> | ||
| * <li>stdlib 1.8.10 with {@code kotlin-stdlib-jdk8:1.6.21} -- the customer's | ||
| * failure, reproduced exactly: "Duplicate class | ||
| * kotlin.collections.jdk8.CollectionsJDK8Kt found in modules | ||
| * kotlin-stdlib-1.8.10 and kotlin-stdlib-jdk8-1.6.21". The task fails | ||
| * without this script and passes with it.</li> | ||
| * <li>the same, with the shim pinned {@code strictly}, or with | ||
| * {@code reject '[1.8.0,)'} -- resolves, and the Android build succeeds. | ||
| * The constraint version this replaced failed both, in the Android build | ||
| * too: "Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jdk8: | ||
| * {strictly 1.6.21}". That is the whole reason for the change.</li> | ||
| * <li>an all-1.7 project -- untouched, because the stdlib only supersedes | ||
| * from the floor up, so shims that still carry real classes stay. This is | ||
| * also why a Kotlin compiler older than the floor is not a problem: | ||
| * nothing raises the stdlib under it.</li> | ||
| * <li>stdlib 1.8.0 with a NEWER {@code kotlin-stdlib-jdk8:1.9.0} -- resolves | ||
| * to 1.9.0 throughout, exactly as the untouched graph does. An earlier | ||
| * version of this reused the shims' own capability and evicted that shim, | ||
| * taking its requirement on stdlib 1.9.0 with it and silently downgrading | ||
| * the base module to 1.8.0.</li> | ||
| * <li>stdlib 1.9.22, a graph with no Kotlin at all, and this same rule applied | ||
| * twice -- all inert or clean.</li> | ||
| * <li>{@code failOnVersionConflict} with an old shim fails identically with | ||
| * this script and with no script at all: that graph is already broken.</li> | ||
| * </ul> | ||
| */ | ||
| public class KotlinStdlibAlignment { | ||
| /** | ||
| * The version at which the shims became empty, and the version from which | ||
| * {@code kotlin-stdlib} is declared to provide their capabilities. | ||
| */ | ||
| public static final String MERGED_STDLIB_FLOOR = "1.8.0"; | ||
| /** The two shims whose classes moved into {@code kotlin-stdlib}. */ | ||
| private static final String[] ALIGNED_ARTIFACTS = { | ||
| "kotlin-stdlib-jdk7", | ||
| "kotlin-stdlib-jdk8" | ||
| }; | ||
| /** | ||
| * The group of the capability this declares, and the name suffix. | ||
| * | ||
| * <p>Ours, deliberately, rather than reusing the shims' own implicit | ||
| * capability. That one is held by EVERY version of a shim, including the | ||
| * empty ones at or above the floor -- and a conflict there has no right | ||
| * answer: dropping the shim loses its requirement on a newer stdlib and | ||
| * silently downgrades the base module, while dropping the stdlib leaves a | ||
| * graph of empty shims with no stdlib in it at all. Both were measured.</p> | ||
| * | ||
| * <p>A capability only this declares is held by exactly two things: a | ||
| * {@code kotlin-stdlib} at or above the floor, which supersedes the shims, | ||
| * and a shim below it, which is superseded. So the conflict exists where the | ||
| * duplicate exists and nowhere else. It cannot be removed from the shims | ||
| * instead -- {@code removeCapability} does not remove an implicit one, which | ||
| * was tried and measured too.</p> | ||
| */ | ||
| private static final String CAPABILITY_GROUP = "com.codenameone"; | ||
| /** @see #CAPABILITY_GROUP */ | ||
| private static final String CAPABILITY_SUFFIX = "-superseded"; | ||
| private KotlinStdlibAlignment() { | ||
| } | ||
| /** | ||
| * The alignment, as a self-contained script to append after the generated | ||
| * {@code dependencies { }} block. | ||
| * | ||
| * <p>Self-contained because it needs two different scopes: the component | ||
| * metadata rule belongs inside {@code dependencies}, the resolution | ||
| * strategy outside it. It opens its own {@code dependencies} block rather | ||
| * than making the caller splice two pieces into two places.</p> | ||
| * | ||
| * @return the script, newline terminated | ||
| */ | ||
| public static String alignmentScript() { | ||
| String major = MERGED_STDLIB_FLOOR.substring(0, MERGED_STDLIB_FLOOR.indexOf('.')); | ||
| String rest = MERGED_STDLIB_FLOOR.substring(MERGED_STDLIB_FLOOR.indexOf('.') + 1); | ||
| String minor = rest.substring(0, rest.indexOf('.')); | ||
| String atOrAbove = "major > " + major + " || (major == " + major | ||
| + " && minor >= " + minor + ")"; | ||
| String below = "major < " + major + " || (major == " + major | ||
| + " && minor < " + minor + ")"; | ||
| StringBuilder out = new StringBuilder(); | ||
| out.append("\n") | ||
| .append("// Codename One: kotlin-stdlib ").append(MERGED_STDLIB_FLOOR) | ||
| .append(" absorbed the kotlin-stdlib-jdk7 and kotlin-stdlib-jdk8\n") | ||
| .append("// classes and the 1.8.x line ships no Gradle module metadata saying so, so\n") | ||
| .append("// a graph holding stdlib at or above that and an older shim carries the same\n") | ||
| .append("// classes twice and fails checkDuplicateClasses. This states the overlap as a\n") | ||
| .append("// capability and lets Gradle drop the superseded shim. It raises no version,\n") | ||
| .append("// so it cannot conflict with a pin, a force, a BOM or the Kotlin in use.\n") | ||
| .append("// Turn it off with the build hint android.kotlinStdlibAlignment=false.\n") | ||
| .append("dependencies {\n") | ||
| .append(" components.withModule('org.jetbrains.kotlin:kotlin-stdlib') { details ->\n") | ||
| .append(versionGuard(" ", atOrAbove)) | ||
| .append(" allVariants {\n") | ||
| .append(" withCapabilities {\n"); | ||
| for (int i = 0; i < ALIGNED_ARTIFACTS.length; i++) { | ||
| out.append(" addCapability('").append(CAPABILITY_GROUP) | ||
| .append("', '").append(ALIGNED_ARTIFACTS[i]).append(CAPABILITY_SUFFIX) | ||
| .append("', details.id.version)\n"); | ||
| } | ||
| out.append(" }\n") | ||
| .append(" }\n") | ||
| .append(versionGuardEnd(" ")) | ||
| .append(" }\n"); | ||
| for (int i = 0; i < ALIGNED_ARTIFACTS.length; i++) { | ||
| out.append(" components.withModule('org.jetbrains.kotlin:") | ||
| .append(ALIGNED_ARTIFACTS[i]).append("') { details ->\n") | ||
| .append(versionGuard(" ", below)) | ||
| .append(" allVariants {\n") | ||
| .append(" withCapabilities {\n") | ||
| .append(" addCapability('").append(CAPABILITY_GROUP) | ||
| .append("', '").append(ALIGNED_ARTIFACTS[i]).append(CAPABILITY_SUFFIX) | ||
| .append("', details.id.version)\n") | ||
| .append(" }\n") | ||
| .append(" }\n") | ||
| .append(versionGuardEnd(" ")) | ||
| .append(" }\n"); | ||
| } | ||
| out.append("}\n") | ||
| .append("configurations.all {\n") | ||
| // Review asked whether selecting a stdlib that is reachable only | ||
| // THROUGH the shims makes resolution expand until the daemon runs out | ||
| // of heap. It does not: kotlin-stdlib-jdk8:1.7.0 as the only route to | ||
| // the stdlib, plus a force to 1.8.0, resolves in seconds under a | ||
| // 512MB heap on both Gradle 8.5 and 8.14.2 -- and it is a graph this | ||
| // FIXES, since the baseline there carries the duplicate. | ||
| .append(" resolutionStrategy.capabilitiesResolution {\n"); | ||
| for (int i = 0; i < ALIGNED_ARTIFACTS.length; i++) { | ||
| out.append(" withCapability('").append(CAPABILITY_GROUP).append(':') | ||
| .append(ALIGNED_ARTIFACTS[i]).append(CAPABILITY_SUFFIX).append("') {\n") | ||
| .append(" def stdlib = candidates.find {\n") | ||
| .append(" it.id instanceof org.gradle.api.artifacts.component" | ||
| + ".ModuleComponentIdentifier &&\n") | ||
| .append(" it.id.module == 'kotlin-stdlib'\n") | ||
| .append(" }\n") | ||
| .append(" if (stdlib != null) {\n") | ||
| .append(" select(stdlib)\n") | ||
| .append(" }\n") | ||
| .append(" }\n"); | ||
| } | ||
| out.append(" }\n") | ||
| .append("}\n"); | ||
| return out.toString(); | ||
| } | ||
| /** Opens a try block that reads the module version and tests {@code test}. */ | ||
| private static String versionGuard(String indent, String test) { | ||
| return indent + "try {\n" | ||
| + indent + " def parts = details.id.version.split('[.-]')\n" | ||
| + indent + " def major = parts[0].toInteger()\n" | ||
| + indent + " def minor = parts[1].toInteger()\n" | ||
| + indent + " if (" + test + ") {\n"; | ||
| } | ||
| /** | ||
| * Closes it. A version this cannot read is left alone -- doing nothing | ||
| * leaves the duplicate the app already had, and guessing could drop a shim | ||
| * whose classes are still the only copy. | ||
| */ | ||
| private static String versionGuardEnd(String indent) { | ||
| return indent + " }\n" | ||
| + indent + "} catch (Exception ignored) {\n" | ||
| + indent + "}\n"; | ||
| } | ||
| } | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.