Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
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 Sep 1, 2026
f800035
Say the hint's rationale without the contraction Vale rejects
shai-almog Sep 1, 2026
6bd4562
Decide the Kotlin plugin skip on its version, and suppress per artifact
shai-almog Sep 1, 2026
23ff233
Read the Kotlin BOM by version too, not by presence
shai-almog Sep 1, 2026
e1f4b14
Suppress on an active declaration, not on the artifact name appearing…
shai-almog Sep 1, 2026
a1d2089
Hand the alignment every app-controlled fragment, from the tree's own…
shai-almog Sep 1, 2026
5a67ee4
Read declarations on the configuration being constrained, and past co…
shai-almog Sep 1, 2026
ae92d41
Group physical lines into statements before deciding what the app dec…
shai-almog Sep 1, 2026
c20b8e9
Accept every main-variant configuration, and end a statement at a sem…
shai-almog Sep 1, 2026
0256d52
Honour a strict pin anywhere, and stop treating compileOnly as runtim…
shai-almog Sep 1, 2026
7535785
Stop asking which Kotlin plugin is applied
shai-almog Sep 1, 2026
27f413f
Delete the exclusion cut, and let the comment stripper see strings
shai-almog Sep 1, 2026
0726d9b
Delete the Kotlin BOM case, and teach the statement scanner about esc…
shai-almog Sep 1, 2026
3d40900
Tell Gradle syntax from English, and let a map entry breathe
shai-almog Sep 1, 2026
3b26c9d
Honour a strict pin on the merged stdlib, and say what is not being f…
shai-almog Sep 1, 2026
4ac657c
A prerelease of the floor is below it, and a reason string is not a d…
shai-almog Sep 1, 2026
98d5769
Read the declaration Gradle reads: next-line closures, prose, qualifi…
shai-almog Sep 1, 2026
f9ae6ff
An underscore is part of an identifier
shai-almog Sep 1, 2026
a141d81
One rule for walking a string literal, since several had drifted apart
shai-almog Sep 1, 2026
786d1bc
Scan the plugin fragment, join comma-continued maps, and stop at the …
shai-almog Sep 1, 2026
aea0eae
Follow one hop through a def, and stop calling prose a coordinate
shai-almog Sep 1, 2026
195576d
Do not manufacture the duplicate: a pre-merge shim pin takes its sibl…
shai-almog Sep 1, 2026
c756160
Read the strict shorthand, and stop reading map notation out of prose
shai-almog Sep 1, 2026
523e93f
Take the version from a rich-version closure, not just from the coord…
shai-almog Sep 1, 2026
cfcef06
Ask whether a pin is strict separately from what it is strict at
shai-almog Sep 1, 2026
858c49a
Read a version selector by its lower bound, the declared group, and a…
shai-almog Sep 1, 2026
5270ddd
Follow definitions in statement order, and through interpolation
shai-almog Sep 1, 2026
c7e94ed
Give the last two scanners the shared string rule, and read !! wherev…
shai-almog Sep 1, 2026
1d2df8a
Read a literal's real delimiters, its position, and the script it bel…
shai-almog Sep 1, 2026
ca4de85
Sweep the spelling space, and stop the alignment from ever failing a …
shai-almog Sep 1, 2026
c0513a7
Give the last scanner the shared rule, and stop reading a strict pin …
shai-almog Sep 1, 2026
5dd0ba2
Ask one question about where a literal starts, and change position on…
shai-almog Sep 1, 2026
08ffcf3
Scan every fragment the generated block is built from, and enumerate …
shai-almog Sep 1, 2026
e663c8d
Read a force the way Gradle applies one, and let a slashy literal be …
shai-almog Sep 1, 2026
cf45c7a
Ask which half is closed, and let a removed comment still separate it…
shai-almog Sep 1, 2026
d095935
Let a line ending, a blank line and an escaped delimiter all mean not…
shai-almog Sep 1, 2026
948257b
Read every spelling of a force, and expand a definition that refers t…
shai-almog Sep 1, 2026
673e167
Tell a call from an assignment, and interpolate everything Groovy int…
shai-almog Sep 1, 2026
665cde2
Ask whether a selector can reach the floor, not where it starts
shai-almog Sep 1, 2026
4055a52
Read a partial coordinate, a status selector and a conditional for wh…
shai-almog Sep 1, 2026
b083292
Count braces instead of naming the constructs that open them
shai-almog Sep 1, 2026
f0a5ee4
Read a resolution rule, an empty declaration and a qualified type
shai-almog Sep 1, 2026
0bd8939
Read useTarget as a force, and a quoted map key as a key
shai-almog Sep 1, 2026
bd4b2b6
Scan the whole generated script, and read a substitution from its rep…
shai-almog Sep 1, 2026
fd5aff3
Stop a timeout test from asserting the speed of a JVM launch
shai-almog Sep 1, 2026
3498e3e
Pass every fragment that carries app text, and read a substitution's …
shai-almog Sep 1, 2026
72ea31e
Give a scope its names back, and let skipBlanks know what whitespace is
shai-almog Sep 1, 2026
0026dff
Read a declaration past its annotations, and to the end of its declar…
shai-almog Sep 1, 2026
507954a
Share the backward skip too, and let += assign
shai-almog Sep 1, 2026
c38efbd
Tell a named argument from a declaration, and count a closure opened …
shai-almog Sep 1, 2026
ddd90c0
Let a bracket hold a statement together the way a parenthesis does
shai-almog Sep 1, 2026
25e8fae
Scan each fragment inside the closure that holds it, and read what a …
shai-almog Sep 1, 2026
61c9fce
Stand down where a conflict is fatal, and read declarations only wher…
shai-almog Sep 2, 2026
a44efac
Narrow both of last commit's stand-downs to what they actually govern
shai-almog Sep 2, 2026
b20ef8a
Record a map factored into a variable, and read a rejection for what …
shai-almog Sep 2, 2026
a58c6bb
Expand interpolations inside a stored dependency map
shai-almog Sep 2, 2026
43093d0
Read repeated Gradle calls the way Gradle does
shai-almog Sep 2, 2026
939aa0c
A type is a type however it is spelled, and a rejected floor is gone
shai-almog Sep 2, 2026
8bbd20c
Tell the plugin classpath from the application graph
shai-almog Sep 2, 2026
0462194
Quoted syntax is not syntax, and an enforced BOM is not an ordinary one
shai-almog Sep 2, 2026
d356c88
Identify an artifact the same way everywhere
shai-almog Sep 2, 2026
8c738ac
Scan the script the way Gradle executes it
shai-almog Sep 2, 2026
7f609b3
Take the lower of two forces for one module
shai-almog Sep 2, 2026
661d5b6
Keep a resolution rule together across every line break in it
shai-almog Sep 2, 2026
6333590
A soft requirement is raised by the constraint, not honoured as a pin
shai-almog Sep 2, 2026
dabc1af
Read a coordinate that is not the first argument, and not another han…
shai-almog Sep 2, 2026
dfeb690
One reading for every spelling of "which configuration"
shai-almog Sep 2, 2026
216d59d
The dependency handler has more than one adder
shai-almog Sep 2, 2026
daa14f3
Four readings that were confidently wrong rather than unreadable
shai-almog Sep 2, 2026
d3a841d
The extra properties setter binds its first argument
shai-almog Sep 2, 2026
950fbbd
A custom configuration may inherit the constraint
shai-almog Sep 2, 2026
3d4a0a1
Copy a binding, end a comment, and name the adders
shai-almog Sep 2, 2026
be23e79
Finish the carriage return, and let a ternary choose
shai-almog Sep 2, 2026
aff4b0e
A redundant parenthesis is still the same argument
shai-almog Sep 2, 2026
5dc2843
A soft pre-merge declaration is raised, not honoured
shai-almog Sep 2, 2026
a034590
Read the two value shapes the ext closure missed, and correct the hint
shai-almog Sep 2, 2026
d41c825
Keep a command call from clearing its argument, and honour a rejected…
shai-almog Sep 2, 2026
1463f7e
Read a selection rule across its body, and an unreadable arm as an arm
shai-almog Sep 2, 2026
b3a84f8
A rejection counts only in the rule that names the family
shai-almog Sep 2, 2026
d9d9919
Keep the lower coordinate, and read a module named by coordinate
shai-almog Sep 2, 2026
480af15
Four spellings that read as something they are not
shai-almog Sep 2, 2026
0f46dae
A called keyword, a wrapped value, and one android closure
shai-almog Sep 2, 2026
6140dd3
Scope a destructured name, and let an output helper print
shai-almog Sep 2, 2026
76cb5e1
Answer the pin question with a token check, not a Groovy parser
shai-almog Sep 2, 2026
f26e00e
Say out loud when the alignment stands down, and ask both questions o…
shai-almog Sep 2, 2026
3b7e769
Do not adopt an EDT that has stopped dispatching
shai-almog Sep 2, 2026
9fcadc1
Leave a Kotlin project's own toolchain alone, and find a pin in any case
shai-almog Sep 2, 2026
8735b1f
Close the handover window rather than narrowing it, and read a range …
shai-almog Sep 2, 2026
35d818a
The BOM that exists, the plugin wherever it came from, and a range wi…
shai-almog Sep 2, 2026
13d8420
Drop the Display change; it does not belong in this PR
shai-almog Sep 2, 2026
7492d38
State the overlap as a capability, so no version has to move
shai-almog Sep 2, 2026
eb99f79
Record the Android measurement in the class
shai-almog Sep 2, 2026
d75fe8b
Declare a capability of our own, so only the overlapping pair conflicts
shai-almog Sep 2, 2026
258947b
The duplicate is not an AndroidX problem, so stop gating on AndroidX
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -638,6 +638,29 @@ static void register(List<Hint> h) {
.doc("Boolean true/false defaults to true. Toggles the new async keyboard mode that leaves the "
+ "keyboard open while you move between text components"));

h.add(new Hint("android.kotlinStdlibAlignment")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
.def("true")
.platform("android")
.doc("Boolean true/false defaults to true. Kotlin 1.8.0 moved the contents of "
+ "`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` into `kotlin-stdlib` "
+ "and left the two shims empty. A build that reaches `kotlin-stdlib` "
+ "1.8 or newer through one dependency and an older "
+ "`kotlin-stdlib-jdk8` through another then carries the same classes "
+ "twice and fails in `checkReleaseDuplicateClasses`, naming Kotlin "
+ "artifacts you never asked for. The 1.8.x line ships no Gradle "
+ "module metadata to say the two overlap; from 1.9.22 JetBrains "
+ "ships it. This adds that missing statement, as a Gradle "
+ "capability: from 1.8.0 up, `kotlin-stdlib` provides what the shims "
+ "provide, so Gradle drops the redundant shim. It moves no version, "
+ "which is what keeps it out of your way -- a version pin, a force, "
+ "an enforced BOM, a range or a Kotlin compiler older than 1.8 all "
+ "resolve exactly as they did without it. Below 1.8.0 nothing "
+ "happens at all, because there the shims still hold the only copy "
+ "of their classes. Set to false to manage these coordinates "
+ "yourself."));

h.add(new Hint("android.largeScreens")
.group(HintGroup.ANDROID)
.type(HintType.BOOLEAN)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7270,6 +7270,58 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
namespace = "namespace '"+request.getPackageName()+"'\n";
}

// Kotlin stdlib alignment, emitted for every AndroidX build rather than
// for Kotlin-shaped apps: the duplicate class it prevents is produced by
// ordinary AndroidX and Play dependencies, not by anything the app wrote.
// See KotlinStdlibAlignment for the mechanism and for why Gradle cannot
// work it out for itself on the kotlin-stdlib 1.8.x line. A constraint
// adds nothing to a graph that has no Kotlin in it, so an app that could
// never hit the clash resolves exactly as it did before.
//
// Gated on AndroidX because that is what decides the configuration name a few
// lines below: `compile` is only "implementation" when useAndroidX or the aar
// implementation flag is set, so a useAndroidX=false build would take this
// block on the legacy `compile` configuration. Reviewed as an unrelated flag
// to gate on -- it is not, and the failing case it is meant to protect needs
// a modern AndroidX dependency in a project that has AndroidX turned off,
// which AGP refuses for its own reasons before this could matter. That
// whole line of reasoning turned out not to matter either: see the
// useAndroidX note on the gate below.
//
// On Gradle 6 rather than on 4.6 where the constraints
// DSL first appeared. That is deliberate, and it has been questioned in
// review, so: 4.6 selects AGP 3.2.0, which cannot compile against a
// compileSdk the current AndroidX releases require, and the builder gives
// that path appcompat 1.0.0, whose graph contains no Kotlin at all. A graph
// that reaches a merged kotlin-stdlib cannot occur there. Widening the gate
// would put an untested constraints block into AGP 3.x builds that work
// today, to fix a clash they cannot have -- and the two failure directions
// are not symmetrical: too narrow leaves an ancient build with a failure it
// already had, too wide breaks a build that currently succeeds. Raise this
// gate only with a reproduction on that path.
// No inputs. This used to collect every Gradle fragment the app
// controls and search it for signs that the app was holding a stdlib
// version down, because the alignment RAISED one and could then break a
// build that resolved. It declares a capability now, which raises
// nothing, so there is nothing to search for -- see KotlinStdlibAlignment.
//
// Not gated on useAndroidX any more. It was, on the reasoning above that
// a non-AndroidX graph cannot reach a merged kotlin-stdlib -- and that
// reasoning is wrong, because the duplicate has nothing to do with
// AndroidX. Reproduced with android.useAndroidX=false explicitly set,
// AGP 8.1.4, kotlin-stdlib 1.8.10 beside kotlin-stdlib-jdk8 1.6.21:
// checkDebugDuplicateClasses fails exactly as it does with AndroidX on,
// and passes with this script. The old gate left those builds broken.
//
// The Gradle 6 floor stays, and for a reason that did survive
// measurement: capabilitiesResolution is the mechanism here, and AGP 3.x
// on Gradle 4.6 is a different world. Turning it off is the hint.
String kotlinStdlibAlignment = "";
if (gradleVersionInt >= 6
&& request.getArg("android.kotlinStdlibAlignment", "true").equals("true")) {
kotlinStdlibAlignment = KotlinStdlibAlignment.alignmentScript();
}

String gradleProps = "apply plugin: 'com.android.application'\n"
+ kotlinPluginApply
+ request.getArg("android.gradlePlugin", "")
Expand DownExpand Up@@ -7362,6 +7414,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
+ addNewlineIfMissing(aarDependencies)
+ "}\n"
// After the dependencies block, not inside it: the alignment
// needs a component metadata rule (which lives in dependencies)
// AND a resolution strategy (which does not), so it brings its
// own dependencies block rather than being spliced into two
// places.
+ kotlinStdlibAlignment
+ request.getArg("android.xgradle", "");

debug("Gradle File start\n-------\n");
Expand Down
Original file line numberDiff line numberDiff 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")
Comment thread
shai-almog marked this conversation as resolved.
.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";
}
}
Loading
Loading