1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:Suppress("UnstableApiUsage") // b/393137152
18 
19 package androidx.build
20 
21 import androidx.build.clang.AndroidXClang
22 import androidx.build.clang.MultiTargetNativeCompilation
23 import androidx.build.clang.NativeLibraryBundler
24 import androidx.build.clang.configureCinterop
25 import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
26 import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin
27 import groovy.lang.Closure
28 import java.io.File
29 import javax.inject.Inject
30 import org.gradle.api.Action
31 import org.gradle.api.GradleException
32 import org.gradle.api.NamedDomainObjectCollection
33 import org.gradle.api.Project
34 import org.gradle.api.artifacts.Configuration
35 import org.gradle.api.configuration.BuildFeatures
36 import org.gradle.api.plugins.ExtensionAware
37 import org.gradle.kotlin.dsl.findByType
38 import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
39 import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
40 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
41 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
42 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
43 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
44 import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
45 import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
46 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
47 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation
48 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
49 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithHostTests
50 import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenRootExtension
51 import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
52 import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmTargetDsl
53 import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask
54 import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec
55 import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
56 import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport
57 import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
58 import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
59 import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
60 
61 /**
62  * [AndroidXMultiplatformExtension] is an extension that wraps specific functionality of the Kotlin
63  * multiplatform extension, and applies the Kotlin multiplatform plugin when it is used. The purpose
64  * of wrapping is to prevent targets from being added when the platform has not been enabled. e.g.
65  * the `macosX64` target is gated on a `project.enableMac` check.
66  */
67 abstract class AndroidXMultiplatformExtension(val project: Project) {
68 
69     @get:Inject abstract val buildFeatures: BuildFeatures
70 
71     var enableBinaryCompatibilityValidator = true
72 
73     // Kotlin multiplatform plugin is only applied if at least one target / sourceset is added.
74     private val kotlinExtensionDelegate = lazy {
75         project.validateMultiplatformPluginHasNotBeenApplied()
76         project.plugins.apply(KotlinMultiplatformPluginWrapper::class.java)
77         project.multiplatformExtension!!
78     }
79     private val kotlinExtension: KotlinMultiplatformExtension by kotlinExtensionDelegate
80     private val agpKmpExtensionDelegate = lazy {
81         // make sure to initialize the kotlin extension by accessing the property
82         val extension = (kotlinExtension as ExtensionAware)
83         project.plugins.apply(KotlinMultiplatformAndroidPlugin::class.java)
84         extension.extensions.getByType(KotlinMultiplatformAndroidLibraryTarget::class.java)
85     }
86 
87     val agpKmpExtension: KotlinMultiplatformAndroidLibraryTarget by agpKmpExtensionDelegate
88 
89     /**
90      * The list of platforms that have been declared as supported in the build configuration.
91      *
92      * This may be a superset of the currently enabled platforms in [targetPlatforms].
93      */
94     val supportedPlatforms: MutableSet<PlatformIdentifier> = mutableSetOf()
95 
96     /**
97      * The list of platforms that are currently enabled.
98      *
99      * This will vary across build environments. For example, a project's build configuration may
100      * have requested `mac()` but this is not available when building on Linux.
101      */
102     val targetPlatforms: List<String>
103         get() =
104             if (kotlinExtensionDelegate.isInitialized()) {
105                 kotlinExtension.targets.mapNotNull {
106                     if (it.targetName != "metadata") {
107                         it.targetName
108                     } else {
109                         null
110                     }
111                 }
112             } else {
113                 throw GradleException("Kotlin multi-platform extension has not been initialized")
114             }
115 
116     /**
117      * Default platform identifier used for specifying POM dependencies.
118      *
119      * This platform will be added as a dependency to the multi-platform anchor artifact's POM
120      * publication. For example, if the anchor artifact is `collection` and the default platform is
121      * `jvm`, then the POM for `collection` will express a dependency on `collection-jvm`. This
122      * ensures that developers who are silently upgrade to KMP artifacts but are not using Gradle
123      * still see working artifacts.
124      *
125      * If no default was specified and a single platform is requested (ex. using [jvm]), returns the
126      * identifier for that platform.
127      */
128     var defaultPlatform: String? = null
129         get() = field ?: supportedPlatforms.singleOrNull()?.id
130         set(value) {
131             if (value != null) {
132                 if (supportedPlatforms.none { it.id == value }) {
133                     throw GradleException(
134                         "Platform $value has not been requested as a target. " +
135                             "Available platforms are: " +
136                             supportedPlatforms.joinToString(", ") { it.id }
137                     )
138                 }
139                 if (targetPlatforms.none { it == value }) {
140                     throw GradleException(
141                         "Platform $value is not available in this build " +
142                             "environment. Available platforms are: " +
143                             targetPlatforms.joinToString(", ")
144                     )
145                 }
146             }
147             field = value
148         }
149 
150     val targets: NamedDomainObjectCollection<KotlinTarget>
151         get() = kotlinExtension.targets
152 
153     /** Helper class to access Clang functionality. */
154     private val clang = AndroidXClang(project)
155 
156     /** Helper class to bundle outputs of clang compilation into an AAR / JAR. */
157     private val nativeLibraryBundler = NativeLibraryBundler(project)
158 
159     internal fun hasNativeTarget(): Boolean {
160         // it is important to check initialized here not to trigger initialization
161         return kotlinExtensionDelegate.isInitialized() &&
162             targets.any { it.platformType == KotlinPlatformType.native }
163     }
164 
165     internal fun hasAndroidMultiplatform(): Boolean {
166         return agpKmpExtensionDelegate.isInitialized()
167     }
168 
169     fun sourceSets(closure: Closure<*>) {
170         if (kotlinExtensionDelegate.isInitialized()) {
171             kotlinExtension.sourceSets.configure(closure).also {
172                 kotlinExtension.sourceSets.configureEach { sourceSet ->
173                     if (sourceSet.name == "main" || sourceSet.name == "test") {
174                         throw Exception(
175                             "KMP-enabled projects must use target-prefixed " +
176                                 "source sets, e.g. androidMain or commonTest, rather than main or test"
177                         )
178                     }
179                 }
180             }
181         }
182     }
183 
184     /**
185      * Creates a multi-target native compilation with the given [archiveName].
186      *
187      * The given [configure] action can be used to add targets, sources, includes etc.
188      *
189      * The outputs of this compilation is not added to any artifact by default.
190      * * To use the outputs via cinterop (kotlin native), use the [createCinterop] function.
191      * * To bundle the outputs inside a JAR (to be loaded at runtime), use the
192      *   [addNativeLibrariesToResources] function.
193      * * To bundle the outputs inside an AAR (to be loaded at runtime), use the
194      *   [addNativeLibrariesToJniLibs] function.
195      *
196      * @param archiveName The archive file name for the native artifacts (.so, .a or .o)
197      * @param configure Action block to configure the compilation.
198      */
199     fun createNativeCompilation(
200         archiveName: String,
201         configure: Action<MultiTargetNativeCompilation>
202     ): MultiTargetNativeCompilation {
203         return clang.createNativeCompilation(archiveName = archiveName, configure = configure)
204     }
205 
206     /**
207      * Creates a Kotlin Native cinterop configuration for the given [nativeTarget] main compilation
208      * from the outputs of [nativeCompilation].
209      *
210      * @param nativeTarget The kotlin native target for which a new cinterop will be added on the
211      *   main compilation.
212      * @param nativeCompilation The [MultiTargetNativeCompilation] which will be embedded into the
213      *   generated cinterop klib.
214      * @param cinteropName The name of the cinterop definition. A matching "<cinteropName.def>" file
215      *   needs to be present in the default cinterop location
216      *   (src/nativeInterop/cinterop/<cinteropName.def>).
217      */
218     @JvmOverloads
219     fun createCinterop(
220         nativeTarget: KotlinNativeTarget,
221         nativeCompilation: MultiTargetNativeCompilation,
222         cinteropName: String = nativeCompilation.archiveName
223     ) {
224         createCinterop(
225             kotlinNativeCompilation =
226                 nativeTarget.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME)
227                     as KotlinNativeCompilation,
228             nativeCompilation = nativeCompilation,
229             cinteropName = cinteropName
230         )
231     }
232 
233     /**
234      * Creates a Kotlin Native cinterop configuration for the given [kotlinNativeCompilation] from
235      * the outputs of [nativeCompilation].
236      *
237      * @param kotlinNativeCompilation The kotlin native compilation for which a new cinterop will be
238      *   added
239      * @param nativeCompilation The [MultiTargetNativeCompilation] which will be embedded into the
240      *   generated cinterop klib.
241      * @param cinteropName The name of the cinterop definition. A matching "<cinteropName.def>" file
242      *   needs to be present in the default cinterop location
243      *   (src/nativeInterop/cinterop/<cinteropName.def>).
244      */
245     @JvmOverloads
246     fun createCinterop(
247         kotlinNativeCompilation: KotlinNativeCompilation,
248         nativeCompilation: MultiTargetNativeCompilation,
249         cinteropName: String = nativeCompilation.archiveName
250     ) {
251         nativeCompilation.configureCinterop(
252             kotlinNativeCompilation = kotlinNativeCompilation,
253             cinteropName = cinteropName
254         )
255     }
256 
257     /**
258      * Creates a Kotlin Native cinterop configuration for the given [kotlinNativeCompilation] from
259      * the single output of a configuration.
260      *
261      * @param kotlinNativeCompilation The kotlin native compilation for which a new cinterop will be
262      *   added
263      * @param configuration The configuration to resolve. It is expected for the configuration to
264      *   contain a single file of the archive file to be referenced in the C interop definition
265      *   file.
266      */
267     fun createCinteropFromArchiveConfiguration(
268         kotlinNativeCompilation: KotlinNativeCompilation,
269         configuration: Configuration
270     ) {
271         configureCinterop(project, kotlinNativeCompilation, configuration)
272     }
273 
274     /** @see NativeLibraryBundler.addNativeLibrariesToJniLibs */
275     @JvmOverloads
276     fun addNativeLibrariesToJniLibs(
277         androidTarget: KotlinAndroidTarget,
278         nativeCompilation: MultiTargetNativeCompilation,
279         forTest: Boolean = false
280     ) =
281         nativeLibraryBundler.addNativeLibrariesToJniLibs(
282             androidTarget = androidTarget,
283             nativeCompilation = nativeCompilation,
284             forTest = forTest
285         )
286 
287     /**
288      * Convenience method to add bundle native libraries with a test jar.
289      *
290      * @see addNativeLibrariesToResources
291      */
292     fun addNativeLibrariesToTestResources(
293         jvmTarget: KotlinJvmTarget,
294         nativeCompilation: MultiTargetNativeCompilation
295     ) =
296         addNativeLibrariesToResources(
297             jvmTarget = jvmTarget,
298             nativeCompilation = nativeCompilation,
299             compilationName = KotlinCompilation.TEST_COMPILATION_NAME
300         )
301 
302     /** @see NativeLibraryBundler.addNativeLibrariesToResources */
303     @JvmOverloads
304     fun addNativeLibrariesToResources(
305         jvmTarget: KotlinJvmTarget,
306         nativeCompilation: MultiTargetNativeCompilation,
307         compilationName: String = KotlinCompilation.MAIN_COMPILATION_NAME
308     ) =
309         nativeLibraryBundler.addNativeLibrariesToResources(
310             jvmTarget = jvmTarget,
311             nativeCompilation = nativeCompilation,
312             compilationName = compilationName
313         )
314 
315     /**
316      * Sets the default target platform.
317      *
318      * The default target platform *must* be enabled in all build environments. For projects which
319      * request multiple target platforms, this method *must* be called to explicitly specify a
320      * default target platform.
321      *
322      * See [defaultPlatform] for details on how the value is used.
323      */
324     fun defaultPlatform(value: PlatformIdentifier) {
325         defaultPlatform = value.id
326     }
327 
328     @JvmOverloads
329     fun jvm(block: Action<KotlinJvmTarget>? = null): KotlinJvmTarget? {
330         supportedPlatforms.add(PlatformIdentifier.JVM)
331         return if (project.enableJvm()) {
332             kotlinExtension.jvm { block?.execute(this) }
333         } else {
334             null
335         }
336     }
337 
338     @JvmOverloads
339     fun jvmStubs(
340         runTests: Boolean = false,
341         block: Action<KotlinJvmTarget>? = null
342     ): KotlinJvmTarget? {
343         supportedPlatforms.add(PlatformIdentifier.JVM_STUBS)
344         return if (project.enableJvm()) {
345             kotlinExtension.jvm("jvmStubs") {
346                 block?.execute(this)
347                 project.tasks.named("jvmStubsTest").configure {
348                     // don't try running common tests for stubs target if disabled
349                     it.enabled = runTests
350                 }
351             }
352         } else {
353             null
354         }
355     }
356 
357     @OptIn(ExperimentalKotlinGradlePluginApi::class)
358     @JvmOverloads
359     fun androidTarget(block: Action<KotlinAndroidTarget>? = null): KotlinAndroidTarget? {
360         supportedPlatforms.add(PlatformIdentifier.ANDROID)
361         return if (project.enableJvm()) {
362             kotlinExtension.androidTarget {
363                 // we need to allow instrumented test to depend on commonTest/jvmTest, which is not
364                 // default.
365                 // see https://youtrack.jetbrains.com/issue/KT-62594
366                 instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
367                 block?.execute(this)
368             }
369         } else {
370             null
371         }
372     }
373 
374     @JvmOverloads
375     fun androidNative(block: Action<KotlinNativeTarget>? = null): List<KotlinNativeTarget> {
376         return listOfNotNull(
377             androidNativeX86(block),
378             androidNativeX64(block),
379             androidNativeArm64(block),
380             androidNativeArm32(block)
381         )
382     }
383 
384     @JvmOverloads
385     fun androidNativeX86(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
386         supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_X86)
387         return if (project.enableAndroidNative()) {
388             kotlinExtension.androidNativeX86 { block?.execute(this) }
389         } else {
390             null
391         }
392     }
393 
394     @JvmOverloads
395     fun androidNativeX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
396         supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_X64)
397         return if (project.enableAndroidNative()) {
398             kotlinExtension.androidNativeX64 { block?.execute(this) }
399         } else {
400             null
401         }
402     }
403 
404     @JvmOverloads
405     fun androidNativeArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
406         supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_ARM64)
407         return if (project.enableAndroidNative()) {
408             kotlinExtension.androidNativeArm64 { block?.execute(this) }
409         } else {
410             null
411         }
412     }
413 
414     @JvmOverloads
415     fun androidNativeArm32(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
416         supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_ARM32)
417         return if (project.enableAndroidNative()) {
418             kotlinExtension.androidNativeArm32 { block?.execute(this) }
419         } else {
420             null
421         }
422     }
423 
424     @JvmOverloads
425     fun androidLibrary(
426         block: Action<KotlinMultiplatformAndroidLibraryTarget>? = null
427     ): KotlinMultiplatformAndroidLibraryTarget? {
428         supportedPlatforms.add(PlatformIdentifier.ANDROID)
429         return if (project.enableJvm()) {
430             agpKmpExtension.also { block?.execute(it) }
431         } else {
432             null
433         }
434     }
435 
436     @JvmOverloads
437     fun desktop(block: Action<KotlinJvmTarget>? = null): KotlinJvmTarget? {
438         supportedPlatforms.add(PlatformIdentifier.DESKTOP)
439         return if (project.enableDesktop()) {
440             kotlinExtension.jvm("desktop") { block?.execute(this) }
441         } else {
442             null
443         }
444     }
445 
446     @JvmOverloads
447     fun mingwX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTargetWithHostTests? {
448         supportedPlatforms.add(PlatformIdentifier.MINGW_X_64)
449         return if (project.enableWindows()) {
450             kotlinExtension.mingwX64 { block?.execute(this) }
451         } else {
452             null
453         }
454     }
455 
456     /** Configures all mac targets supported by AndroidX. */
457     @JvmOverloads
458     fun mac(block: Action<KotlinNativeTarget>? = null): List<KotlinNativeTarget> {
459         return listOfNotNull(macosX64(block), macosArm64(block))
460     }
461 
462     @JvmOverloads
463     fun macosX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTargetWithHostTests? {
464         supportedPlatforms.add(PlatformIdentifier.MAC_OSX_64)
465         return if (project.enableMac()) {
466             kotlinExtension.macosX64 { block?.execute(this) }
467         } else {
468             null
469         }
470     }
471 
472     @JvmOverloads
473     fun macosArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTargetWithHostTests? {
474         supportedPlatforms.add(PlatformIdentifier.MAC_ARM_64)
475         return if (project.enableMac()) {
476             kotlinExtension.macosArm64 { block?.execute(this) }
477         } else {
478             null
479         }
480     }
481 
482     /** Configures all ios targets supported by AndroidX. */
483     @JvmOverloads
484     fun ios(block: Action<KotlinNativeTarget>? = null): List<KotlinNativeTarget> {
485         return listOfNotNull(iosX64(block), iosArm64(block), iosSimulatorArm64(block))
486     }
487 
488     @JvmOverloads
489     fun iosArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
490         supportedPlatforms.add(PlatformIdentifier.IOS_ARM_64)
491         return if (project.enableMac()) {
492             kotlinExtension.iosArm64 { block?.execute(this) }
493         } else {
494             null
495         }
496     }
497 
498     @JvmOverloads
499     fun iosX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
500         supportedPlatforms.add(PlatformIdentifier.IOS_X_64)
501         return if (project.enableMac()) {
502             kotlinExtension.iosX64 { block?.execute(this) }
503         } else {
504             null
505         }
506     }
507 
508     @JvmOverloads
509     fun iosSimulatorArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
510         supportedPlatforms.add(PlatformIdentifier.IOS_SIMULATOR_ARM_64)
511         return if (project.enableMac()) {
512             kotlinExtension.iosSimulatorArm64 { block?.execute(this) }
513         } else {
514             null
515         }
516     }
517 
518     /** Configures all watchos targets supported by AndroidX. */
519     @JvmOverloads
520     fun watchos(block: Action<KotlinNativeTarget>? = null): List<KotlinNativeTarget> {
521         return listOfNotNull(
522             watchosX64(block),
523             watchosArm32(block),
524             watchosArm64(block),
525             watchosDeviceArm64(block),
526             watchosSimulatorArm64(block)
527         )
528     }
529 
530     @JvmOverloads
531     fun watchosArm32(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
532         supportedPlatforms.add(PlatformIdentifier.WATCHOS_ARM_32)
533         return if (project.enableMac()) {
534             kotlinExtension.watchosArm32 { block?.execute(this) }
535         } else {
536             null
537         }
538     }
539 
540     @JvmOverloads
541     fun watchosArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
542         supportedPlatforms.add(PlatformIdentifier.WATCHOS_ARM_64)
543         return if (project.enableMac()) {
544             kotlinExtension.watchosArm64 { block?.execute(this) }
545         } else {
546             null
547         }
548     }
549 
550     @JvmOverloads
551     fun watchosDeviceArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
552         supportedPlatforms.add(PlatformIdentifier.WATCHOS_DEVICE_ARM_64)
553         return if (project.enableMac()) {
554             kotlinExtension.watchosDeviceArm64 { block?.execute(this) }
555         } else {
556             null
557         }
558     }
559 
560     @JvmOverloads
561     fun watchosX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
562         supportedPlatforms.add(PlatformIdentifier.WATCHOS_X_64)
563         return if (project.enableMac()) {
564             kotlinExtension.watchosX64 { block?.execute(this) }
565         } else {
566             null
567         }
568     }
569 
570     @JvmOverloads
571     fun watchosSimulatorArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
572         supportedPlatforms.add(PlatformIdentifier.WATCHOS_SIMULATOR_ARM_64)
573         return if (project.enableMac()) {
574             kotlinExtension.watchosSimulatorArm64 { block?.execute(this) }
575         } else {
576             null
577         }
578     }
579 
580     /** Configures all tvos targets supported by AndroidX. */
581     @JvmOverloads
582     fun tvos(block: Action<KotlinNativeTarget>? = null): List<KotlinNativeTarget> {
583         return listOfNotNull(tvosX64(block), tvosArm64(block), tvosSimulatorArm64(block))
584     }
585 
586     @JvmOverloads
587     fun tvosArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
588         supportedPlatforms.add(PlatformIdentifier.TVOS_ARM_64)
589         return if (project.enableMac()) {
590             kotlinExtension.tvosArm64 { block?.execute(this) }
591         } else {
592             null
593         }
594     }
595 
596     @JvmOverloads
597     fun tvosX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
598         supportedPlatforms.add(PlatformIdentifier.TVOS_X_64)
599         return if (project.enableMac()) {
600             kotlinExtension.tvosX64 { block?.execute(this) }
601         } else {
602             null
603         }
604     }
605 
606     @JvmOverloads
607     fun tvosSimulatorArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
608         supportedPlatforms.add(PlatformIdentifier.TVOS_SIMULATOR_ARM_64)
609         return if (project.enableMac()) {
610             kotlinExtension.tvosSimulatorArm64 { block?.execute(this) }
611         } else {
612             null
613         }
614     }
615 
616     @JvmOverloads
617     fun linux(block: Action<KotlinNativeTarget>? = null): List<KotlinNativeTarget> {
618         return listOfNotNull(
619             linuxArm64(block),
620             linuxX64(block),
621         )
622     }
623 
624     @JvmOverloads
625     fun linuxArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
626         supportedPlatforms.add(PlatformIdentifier.LINUX_ARM_64)
627         return if (project.enableLinux()) {
628             kotlinExtension.linuxArm64 { block?.execute(this) }
629         } else {
630             null
631         }
632     }
633 
634     @JvmOverloads
635     fun linuxX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
636         supportedPlatforms.add(PlatformIdentifier.LINUX_X_64)
637         return if (project.enableLinux()) {
638             kotlinExtension.linuxX64 { block?.execute(this) }
639         } else {
640             null
641         }
642     }
643 
644     @JvmOverloads
645     fun linuxX64Stubs(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
646         supportedPlatforms.add(PlatformIdentifier.LINUX_X_64_STUBS)
647         return if (project.enableLinux()) {
648             kotlinExtension.linuxX64("linuxx64Stubs") {
649                 block?.execute(this)
650                 project.tasks.named("linuxx64StubsTest").configure {
651                     // don't try running common tests for stubs target
652                     it.enabled = false
653                 }
654             }
655         } else {
656             null
657         }
658     }
659 
660     @JvmOverloads
661     fun js(block: Action<KotlinJsTargetDsl>? = null): KotlinJsTargetDsl? {
662         if (buildFeatures.isIsolatedProjectsEnabled()) return null
663         supportedPlatforms.add(PlatformIdentifier.JS)
664         return if (project.enableJs()) {
665             kotlinExtension.js() {
666                 block?.execute(this)
667                 binaries.library()
668                 project.configureJs()
669                 project.configureKotlinJsTests()
670                 configureBrowserForTests(project)
671             }
672         } else {
673             null
674         }
675     }
676 
677     @OptIn(ExperimentalWasmDsl::class)
678     @JvmOverloads
679     fun wasmJs(block: Action<KotlinJsTargetDsl>? = null): KotlinWasmTargetDsl? {
680         if (buildFeatures.isIsolatedProjectsEnabled()) return null
681         supportedPlatforms.add(PlatformIdentifier.WASM_JS)
682         return if (project.enableWasmJs()) {
683             kotlinExtension.wasmJs("wasmJs") {
684                 block?.execute(this)
685                 binaries.library()
686                 project.configureWasm()
687                 project.configureKotlinJsTests()
688                 configureBrowserForTests(project)
689             }
690         } else {
691             null
692         }
693     }
694 
695     private fun KotlinJsTargetDsl.configureBrowserForTests(project: Project) {
696         browser {
697             testTask {
698                 it.useKarma {
699                     useChromeHeadless()
700                     useConfigDirectory(File(project.getSupportRootFolder(), "buildSrc/karmaconfig"))
701                 }
702             }
703         }
704     }
705 
706     /** Locates a project by path. */
707     // This method is needed for Gradle project isolation to avoid calls to parent projects due to
708     // androidx { samples(project(":foo")) }
709     // Without this method, the call above results into a call to the parent object, because
710     // AndroidXExtension has `val project: Project`, which from groovy `project` call within
711     // `androidx` block tries retrieves that project object and calls to look for :foo property
712     // on it, then checking all the parents for it.
713     fun project(name: String): Project = project.project(name)
714 
715     companion object {
716         const val EXTENSION_NAME = "androidXMultiplatform"
717     }
718 }
719 
Projectnull720 private fun Project.configureJs() {
721     configureNode()
722     configureBinaryen()
723     // Use DSL API when https://youtrack.jetbrains.com/issue/KT-70029 is closed for all tasks below
724     tasks.named("jsDevelopmentLibraryCompileSync", DefaultIncrementalSyncTask::class.java) {
725         it.destinationDirectory.set(file(layout.buildDirectory.dir("js/packages/js/dev/kotlin")))
726     }
727     tasks.named("jsProductionLibraryCompileSync", DefaultIncrementalSyncTask::class.java) {
728         it.destinationDirectory.set(file(layout.buildDirectory.dir("js/packages/js/prod/kotlin")))
729     }
730 }
731 
Projectnull732 private fun Project.configureWasm() {
733     configureNode()
734     configureBinaryen()
735     // Use DSL API when https://youtrack.jetbrains.com/issue/KT-70029 is closed for all tasks below
736     tasks.named("wasmJsDevelopmentLibraryCompileSync", DefaultIncrementalSyncTask::class.java) {
737         it.destinationDirectory.set(
738             file(layout.buildDirectory.dir("js/packages/wasm-js/dev/kotlin"))
739         )
740     }
741     tasks.named("wasmJsProductionLibraryCompileSync", DefaultIncrementalSyncTask::class.java) {
742         it.destinationDirectory.set(
743             file(layout.buildDirectory.dir("js/packages/wasm-js/prod/kotlin"))
744         )
745     }
746 
747     // Compiler Arg needed for tests only: https://youtrack.jetbrains.com/issue/KT-59081
748     tasks.withType(Kotlin2JsCompile::class.java).configureEach { task ->
749         if (task.name.lowercase().contains("test")) {
750             task.compilerOptions.freeCompilerArgs.add("-Xwasm-enable-array-range-checks")
751         }
752     }
753 }
754 
Projectnull755 private fun Project.configureNode() {
756     extensions.findByType<NodeJsEnvSpec>()?.let { nodeJs ->
757         nodeJs.version.set(getVersionByName("node"))
758         if (!ProjectLayoutType.isPlayground(this)) {
759             nodeJs.downloadBaseUrl.set(
760                 File(project.getPrebuiltsRoot(), "androidx/external/org/nodejs/node")
761                     .toURI()
762                     .toString()
763             )
764         }
765     }
766 
767     // https://youtrack.jetbrains.com/issue/KT-73913/K-Wasm-yarn-version-per-project
768     rootProject.extensions.findByType(YarnRootExtension::class.java)?.let { yarn ->
769         @Suppress("DEPRECATION")
770         yarn.version = getVersionByName("yarn")
771         yarn.yarnLockMismatchReport = YarnLockMismatchReport.FAIL
772         if (!ProjectLayoutType.isPlayground(this)) {
773             yarn.lockFileDirectory =
774                 File(project.getPrebuiltsRoot(), "androidx/javascript-for-kotlin")
775         }
776     }
777 }
778 
Projectnull779 private fun Project.configureBinaryen() {
780     // https://youtrack.jetbrains.com/issue/KT-74840
781     rootProject.extensions.findByType<BinaryenRootExtension>()?.let { binaryen ->
782         @Suppress("DEPRECATION")
783         binaryen.downloadBaseUrl =
784             File(project.getPrebuiltsRoot(), "androidx/javascript-for-kotlin/binaryen")
785                 .toURI()
786                 .toString()
787     }
788 }
789 
Projectnull790 private fun Project.configureKotlinJsTests() =
791     tasks.withType(KotlinJsTest::class.java).configureEach { task ->
792         if (!ProjectLayoutType.isPlayground(this)) {
793             val unzipChromeBuildServiceProvider =
794                 gradle.sharedServices.registrations.getByName("unzipChrome").service
795             task.usesService(unzipChromeBuildServiceProvider)
796             // Remove doFirst and switch to FileProperty property to set browser path when issue
797             // https://youtrack.jetbrains.com/issue/KT-72514 is resolved
798             task.doFirst {
799                 task.environment(
800                     "CHROME_BIN",
801                     (unzipChromeBuildServiceProvider.get() as UnzipChromeBuildService).chromePath
802                 )
803             }
804         }
805         task.testLogging.showStandardStreams = true
806         // From: https://nodejs.org/api/cli.html
807         task.nodeJsArgs.addAll(listOf("--trace-warnings", "--trace-uncaught", "--trace-sigint"))
808     }
809 
Projectnull810 fun Project.validatePublishedMultiplatformHasDefault() {
811     val extension = project.extensions.getByType(AndroidXMultiplatformExtension::class.java)
812     if (extension.defaultPlatform == null && extension.supportedPlatforms.isNotEmpty()) {
813         throw GradleException(
814             "Project is published and multiple platforms are requested. You " +
815                 "must explicitly specify androidXMultiplatform.defaultPlatform as one of: " +
816                 extension.targetPlatforms.joinToString(", ") {
817                     "PlatformIdentifier.${PlatformIdentifier.fromId(it)!!.name}"
818                 }
819         )
820     }
821 }
822