Skip to content

Latest commit

 

History

History
2243 lines (1793 loc) · 64.8 KB

Documentation.md

File metadata and controls

2243 lines (1793 loc) · 64.8 KB

Before you start

Check the setup instructions

Basics

Amper exists as a standalone build tool and also as a Gradle plugin. For Gradle-based projects Amper supports full Gradle interop, Gradle plugins and custom tasks. Certain functionality and behavior might differ between the standalone and Gradle-based Amper versions.

See the usage instructions for the standalone Amper version and for the Gradle-based version.

An Amper project is defined by a project.yaml file. This file contains the list of modules and the project-wide configuration. The folder with the project.yaml file is the project root. Modules can only be located under the project root. If there is only one module in the project, a project.yaml file is not required.

In Gradle-based projects, a settings.gradle.kts file is used instead of a project.yaml file.

An Amper module is a directory with a module.yaml configuration file, module sources and resources. A module configuration file describes what to produce: e.g. a reusable library or a platform-specific application. Each module describes a single product. Several modules can't share same sources or resources.

To get familiar with YAML, see the brief intro.

How to produce the desired product, that is, the build rules, is the responsibility of the Amper build engine and extensions. In Gradle-based Amper projects it's possible to use plugins and write custom Gradle tasks.

Amper supports Kotlin Multiplatform as a core concept and offers special syntax to deal with multiplatform configuration. There is a dedicated @platform-qualifier used to mark platform-specific code, dependencies, settings, etc. You'll see it in the examples below.

Project layout

A basic single-module Amper project looks like this:

|-src/             
|  |-main.kt      
|-test/       
|  |-MainTest.kt 
|-module.yaml

If there are multiple modules, the project.yaml file specifies the list of modules:

|-app/
|  |-src/             
|  |  |-main.kt
|  |-...      
|  |-module.yaml
|-lib/
|  |-src/             
|  |  |-util.kt      
|  |-module.yaml
|-project.yaml

The project.yaml file could look like this:

modules:
  - ./app
  - ./lib

Check the reference for more options to define the list of modules in the project.yaml file.

In Gradle-based projects the settings.gradle.kts is expected instead of a project.yaml file, and it's required even for single-module projects. Read more in the Gradle-based projects section.

|-src/             
|  |-main.kt      
|-test/       
|  |-MainTest.kt 
|-module.yaml
|-settings.gradle.kts

Source code

Source files are located in the src folder:

|-src/             
|  |-main.kt      
|-module.yaml

By convention, a main.kt file if present is a default entry point for the application. Read more on configuring the application entry points.

In a JVM module, you can mix Kotlin and Java source files:

|-src/             
|  |-main.kt      
|  |-Util.java      
|-module.yaml

In a multiplatform module, platform-specific code is located in the folders with @platform-qualifiers:

|-src/             # common code
|  |-main.kt      
|  |-util.kt       #  API with ‘expect’ part
|-src@ios/         # code to be compiled only for iOS targets
|  |-util.kt       #  API implementation with ‘actual’ part for iOS
|-src@jvm/         # code to be compiled only for JVM targets
|  |-util.kt       #  API implementation with ‘actual’ part for JVM
|-module.yaml

In the future, we plan to also support a more lightweight multiplatform layout like the one below:

|-src/             # common and platform-specific code
|  |-main.kt      
|  |-util.kt       #  API with ‘expect’ part
|  |-util@ios.kt   #  API implementation with ‘actual’ part for iOS
|  |-util@jvm.kt   #  API implementation with ‘actual’ part for JVM
|-module.yaml

Sources and resources can't be shared by multiple modules. This ensures that the IDE always knows how to analyze and refactor the code, as it always exists in the scope of a single module, has a well-defined list of dependencies, etc.

See also info on resource management.

Amper also supports Gradle-compatible layouts for Gradle-based projects:

|-src/
|  |-main/
|  |  |-kotlin
|  |  |  |-main.kt
|  |  |-resources
|  |  |  |-...
|  |-test/
|  |  |-kotlin
|  |  |  |-test.kt
|  |  |-resources
|  |  |  |-...
|-module.yaml

Read more about Gradle-compatible project layouts.

Module file anatomy

A module.yaml file has several main sections: product:, dependencies: and settings:. A module could produce a single product, such as a reusable library or an application. Read more on the supported product types

Here is an example of a JVM console application with a single dependency and a specified Kotlin language version:

product: jvm/app

dependencies:
  - io.ktor:ktor-client-core:2.3.0

settings:
  kotlin:
    languageVersion: 1.9

Example of a KMP library:

product: 
  type: lib
  platforms: [android, iosArm64]

settings:
  kotlin:
    languageVersion: 1.9

Product types

Product type describes the target platform and the type of the project at the same time. Below is the list of supported product types:

  • lib - a reusable Amper library which could be used as a dependency by other modules in the Amper project.
  • jvm/app - a JVM console or desktop application
  • windows/app - a mingw64 app
  • linux/app - a native linux application
  • macos/app - a native macOS application
  • android/app - an Android VM application
  • ios/app - an iOS/iPadOS application

Other product types what we plan to support in the future:

  • watchos/app - an Apple Watch application (not yet implemented)
  • windows/dll
  • linux/so
  • macos/framework
  • etc.

Packaging

Each product has an associated packaging, defined by the target OS and product type. E.g. macos/app are packaged as so-called bundles, android/app as APKs, and jvm/app as jars. By default, packages are generated according to platform conventions. For custom packaging configuration Amper will offer a separate packaging: section.

Packaging configuration is not yet implemented. Meanwhile, you can use Gradle interop for custom packaging.

product: jvm/app

packaging:
  - type: fatJar            # specify a type of the package 
    content: # specify how to lay out the final artifact
      include: licenses/**    

Publishing

Publishing means preparing the resulting package for external use, and optionally uploading or deploying it. Here are a few examples of publishing:

  • Preparing a JVM jar or a KLIB with sources and docs and uploading them Maven Central or to another Maven repository.
  • Creating a CocoaPods package and publishing it for use in Xcode projects.
  • Preparing Android AABs or Apple IPAs for publishing into App Stores.
  • Preparing and signing an MSI, DMG, or DEB distributions

Publishing configuration is not yet implemented. Meanwhile, you can use Gradle interop for custom publishing.

Here is a very rough approximation, how publishing could look like in Amper:

product:
  type: lib
  platforms: [android, iosArm64, iosSimulatorArm64]

publishing:
  - type: maven        
    groupId: ...
    artifactId: ...
    repository:
      url: ...
      credentials: ...

  - type: cocoapods
    name: MyCocoaPod
    version: 1.0
    summary: ...
    homepage: ...

Multiplatform configuration

dependencies: and setting: sections could be specialized for each platform using the @platform-qualifier. An example of a multiplatform library with some common and platform-specific code:

product:
  type: lib
  platforms: [iosArm64, android]

# These dependencies are available in common code.
# They are also propagated to iOS and Android code, along with their platform-specific counterparts 
dependencies:
  - io.ktor:ktor-client-core:2.3.0

# These dependencies are available in Android code only
dependencies@android:
  - com.google.android.material:material:1.5.0

# These dependencies are available in iOS code only
dependencies@ios:
  - pod: 'Alamofire'
    version: '~> 2.0.1'

# These settings are for common code.
# They are also propagated to iOS and Android code 
settings:
  kotlin:
    languageVersion: 1.8
  android:
    compileSdk: 33

# We can add or override settings for specific platforms. 
# Let's override the Kotlin language version for iOS code: 
settings@ios:
  kotlin:
    languageVersion: 1.9 

See details on multiplatform configuration for more information.

Dependencies

External Maven dependencies

For Maven dependencies, simply specify their coordinates:

dependencies:
  - org.jetbrains.kotlin:kotlin-serialization:1.8.0
  - io.ktor:ktor-client-core:2.2.0

Module dependencies

To depend on another module, use a relative path to the folder which contains the corresponding module.yaml. Module dependency should start either with ./ or ../.

Dependencies between modules are only allowed within the project scope. That is, they must be listed in the project.yaml or settings.gradle.kts file.

Example: given the project layout

root/
  |-app/
  |  |-src/
  |  |-module.yaml
  |-ui/
  |  |-utils/
  |  |  |-src/
  |  |  |-module.yaml

The app/module.yaml could declare a dependency on ui/utils as follows:

dependencies:
  - ../ui/utils

Other examples of the internal dependencies:

dependencies:
  - ./nested-folder-with-module-yaml
  - ../sibling-folder-with-module-yaml

Scopes and visibility

There are three dependency scopes:

  • all - (default) the dependency is available during compilation and runtime.
  • compile-only - the dependency is only available during compilation. This is a 'provided' dependency in Maven terminology.
  • runtime-only - the dependency is not available during compilation, but available during testing and running

In a full form you can declare scope as follows:

dependencies:
  - io.ktor:ktor-client-core:2.2.0:
      scope: compile-only 
  - ../ui/utils:
      scope: runtime-only 

There is also an inline form:

dependencies:
  - io.ktor:ktor-client-core:2.2.0: compile-only  
  - ../ui/utils: runtime-only

All dependencies by default are not accessible from the dependent code.
In order to make a dependency visible to a dependent module, you need to explicitly mark it as exported (aka Gradle API-dependency).

dependencies:
  - io.ktor:ktor-client-core:2.2.0:
      exported: true 
  - ../ui/utils:
      exported: true 

There is also an inline form:

dependencies:
  - io.ktor:ktor-client-core:2.2.0: exported
  - ../ui/utils: exported

Here is an example of a compile-only and exported dependency:

dependencies:
  - io.ktor:ktor-client-core:2.2.0:
      scope: compile-only
      exported: true

Native dependencies

Native dependencies are not yet implemented.

To depend on a npm, CocoaPods, or a Swift package, use the following format:

dependencies:
  - npm: "react"
    version: "^17.0.2"
dependencies:
  - pod: 'Alamofire'
    version: '~> 2.0.1'
dependencies:
  - pod: 'Alamofire'
    git: 'https://github.com/Alamofire/Alamofire.git'
    tag: '3.1.1'
dependencies:
  - swift-package:
      url: "https://github.com/.../some-package.git"
      from: "2.0.0"
      target: "SomePackageTarget"

Managing Maven repositories

By default, Maven Central and Google Android repositories are pre-configured. To add extra repositories, use the following options:

repositories:
  - https://repo.spring.io/ui/native/release
  - url: https://dl.google.com/dl/android/maven2/
  - id: jitpack
    url: https://jitpack.io

To configure repository credentials, use the following snippet:

repositories:
  - url: https://my.private.repository/
    credentials:
      file: ../local.properties # relative path to the file with credentials
      usernameKey: my.private.repository.username
      passwordKey: my.private.repository.password

Here is the file ../local.properties:

my.private.repository.username=...
my.private.repository.password=...

Currently only *.properties files with credentials are supported.

Note on Gradle interop

If some repositories are defined in settings.gradle.kts using a dependencyResolutionManagement block, they are only taken into account by pure Gradle subprojects, and don't affect Amper modules. If you want to define repositories in a central place for Amper modules, you can use a repositories list in a template file and apply this template to your modules.

Technical explanation: in Gradle, adding any repository at the subproject level will by default discard the repositories configured in the settings (unless a different Gradle RepositoriesMode is used). Default repositories provided by Amper is an equivalent to adding a repositories section in the build.gradle.kts file of each individual Amper module.

Dependency/Version Catalogs

There are several types of dependency catalogs that are available in Amper:

  • Dependency catalogs provided by toolchains (such as Kotlin, Compose Multiplatform etc.). The toolchain catalog names correspond to the names of the toolchains in the settings section. E.g. dependencies for the Compose Multiplatform frameworks are accessible using the $compose catalog, and its settings using the compose: section.
  • Gradle-based Amper supports Gradle version catalog in the default gradle/libs.versions.toml file. Dependencies from this catalog can be accessed via $libs. catalog name according to the Gradle name mapping rules.
  • User-defined dependency catalogs (not yet implemented).

All supported catalogs could be accessed via a $<catalog-name.key> reference, for example:

dependencies:
  - $compose.material3    # dependency from a Compose Multiplatform catalog
  - $my-catalog.ktor      # dependency from a custom project catalog with a name 'my-catalog' 
  - $libs.commons.lang3   # dependency from a Gradle default libs.versions.toml catalog

Dependencies from catalogs may have scope and visibility:

dependencies:
  - $compose.foundation: exported
  - $my-catalog.db-engine: runtime-only 

Settings

The settings: section contains toolchains settings. A toolchain is an SDK (Kotlin, Java, Android, iOS) or a simpler tool (linter, code generator). Currently, the following toolchains are supported: kotlin:, java:, android:, compose:.

Toolchains are supposed to be extensible in the future.

All toolchain settings are specified in the dedicated groups in the settings: section:

settings:
  kotlin:
    languageVersion: 1.8
  android:
    compileSdk: 31

Here is the list of currently supported toolchains and their settings.

See multiplatform settings configuration for more details.

Configuring Kotlin Serialization

Kotlin Serialization is the official multiplatform and multi-format serialization library for Kotlin.

If you need to (de)serialize Kotlin classes to/from JSON, you can enable Kotlin Serialization it in its simplest form:

settings:
  kotlin:
    serialization: json  # JSON or other format

This snippet configures the compiler to process @Serializable classes, and adds dependencies on the serialization runtime and JSON format libraries.

You can also customize the version of the Kotlin Serialization libraries using the full form of the configuration:

settings:
  kotlin:
    serialization:
      format: json
      version: 1.7.3
More control over serialization formats

If you don't need serialization format dependencies or if you need more control over them, you can use the following:

settings:
  kotlin:
    serialization: enabled # configures the compiler and serialization runtime library

This snippet on its own only configures the compiler and the serialization runtime library, but doesn't add any format dependency. However, it adds a built-in catalog with official serialization formats libraries, which you can use in your dependencies section. This is useful in multiple cases:

  • if you need a format dependency only in tests:

    settings:
      kotlin:
        serialization: enabled
    
    test-dependencies:
      - $kotlin.serialization.json
  • if you need to customize the scope of the format dependencies:

    settings:
      kotlin:
        serialization: enabled
    
    dependencies:
      - $kotlin.serialization.json: compile-only
  • if you need to expose format dependencies transitively:

    settings:
      kotlin:
        serialization: enabled
    
    dependencies:
      - $kotlin.serialization.json: exported
  • if you need multiple formats:

    settings:
      kotlin:
        serialization: enabled
    
    dependencies:
      - $kotlin.serialization.json
      - $kotlin.serialization.protobuf

Configuring Compose Multiplatform

In order to enable Compose (with a compiler plugin and required dependencies), add the following configuration:

JVM Desktop:

product: jvm/app

dependencies:
  # add Compose dependencies using a dependency catalog:
  - $compose.desktop.currentOs
    
settings: 
  # enable Compose toolchain
  compose: enabled

Android:

product: android/app

dependencies:
  # add Compose dependencies using a dependency catalog:
  - $compose.foundation
  - $compose.material3

settings: 
  # enable Compose toolchain
  compose: enabled

There is also a full form for enabling or disabling the Compose toolchain:

...
settings: 
  compose:
    enabled: true

Also, you can specify the exact version of the Compose framework to use:

...
settings:
  compose:
    version: 1.5.10

In a Gradle-based project you also need to set a couple of flags in the gradle.properties file:

|-...
|-settings.gradle.kts
|-gradle.properties    # create this file if it doesn't exist 
# Compose requires AndroidX
android.useAndroidX=true

# Android and iOS build require more memory
org.gradle.jvmargs=-Xmx4g
Using multiplatform resources

Amper supports Compose Multiplatform resources.

Current limitations:

The file layout is:

|-my-kmp-module/
|  |-module.yaml
|  |-src/
|  |  |-commonMain/
|  |  |  |-kotlin # your code is here
|  |  |  |  |-...
|  |  |  |-composeResources # place your multiplatform resources in this folder
|  |  |  |  |-values/
|  |  |  |  |  |-strings.xml
|  |  |  |  |-drawable/
|  |  |  |  |  |-image.jpg
|  |  |-...

Configure the module.yaml to use gradle-kmp file layout:

product: 
  type: lib
  platforms: [jvm, android]

module:
  layout: gradle-kmp 

Amper automatically generates the accessors for resources during build and when working with code in the IDE. Accessors are generated in a package that corresponds to the module name. All non-letter symbols are replaced with _. In the given example where the module name is my-kmp-module, the package name for the generated resources will be my_kmp_module.

Here is how to use the resources in the code:

import my_kmp_module.generated.resources.Res
import my_kmp_module.generated.resources.hello
// other imports

@Composable
private fun displayHelloText() {
    BasicText(stringResource(Res.string.hello))
}

Read more about setting up and using compose resources in the documentation.

Configuring entry points

JVM

By convention a single main.kt file (case-insensitive) in the source folder is a default entry point for the application.

Here is how to specify the entry point explicitly for JVM:

product: jvm/app

settings:
  jvm:
    mainClass: org.example.myapp.MyMainKt
Native

By convention a single main.kt file (case-insensitive) in the source folder is a default entry point for the application.

Android

See the dedicated Android section

iOS

Currently, there should be a swift file in the src/ folder with the @main struct:

|-src/ 
|  |-main.swift
|  |-... 
|-module.yaml

src/main.swift:

...
@main
struct iosApp: App {
   ...
}

Configuring Android

With the standalone version of Amper, you can use the ./amper task bundleAndroid command to create a release build.

In Gradle-based Amper projects, you can use the Gradle tasks provided with the release build type.

Entry point

The application's entry point is specified in the AndroidManifest.xml file according to the official Android documentation.

|-src/ 
|  |-MyActivity.kt
|  |-AndroidManifest.xml
|  |-... 
|-module.yaml

src/AndroidManifest.xml:

<manifest ... >
  <application ... >
    <activity android:name="com.example.myapp.MainActivity" ... >
    </activity>
  </application>
</manifest>
Signing

In a module containing an Android application (using the android/app product type) you can enable signing under settings:

settings:
  android:
    signing: enabled

This will use a keystore.properties file located in the module folder for the signing details by default. This properties file must contain the following signing details. Remember that these details should usually not be added to version control.

storeFile=/Users/example/.keystores/release.keystore
storePassword=store_password
keyAlias=alias
keyPassword=key_password

To customize the path to this file, you can use the propertiesFile option:

settings:
  android:
    signing:
      enabled: true
      propertiesFile: ./keystore.properties # default value

With the standalone version of Amper, you can use ./amper tool generate-keystore to generate a new keystore if you don't have one yet. This will create a new self-signed certificate, using the details in the keystore.properties file.

You can also pass in these details to generate-keystore as command line arguments. Invoke the tool with --help to learn more.

Parcelize

If you want to automatically generate your Parcelable implementations, you can enable Parcelize as follows:

settings:
  android:
    parcelize: enabled

With this simple toggle, the following class gets its Parcelable implementation automatically without spelling it out in the code, just thanks to the @Parcelize annotation:

import kotlinx.parcelize.Parcelize

@Parcelize
class User(val firstName: String, val lastName: String, val age: Int): Parcelable

While this is only relevant on Android, sometimes you need to share your data model between multiple platforms. However, the Parcelable interface and @Parcelize annotation are only present on Android. But fear not, there is a solution described in the official documentation. In short:

  • For android.os.Parcelable, you can use the expect/actual mechanism to define your own interface as typealias of android.os.Parcelable (for Android), and as an empty interface for other platforms.
  • For @Parcelize, you can simply define your own annotation instead, and then tell Parcelize about it (see below).

For example, in common code:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class MyParcelize

expect interface MyParcelable

Then in Android code:

actual typealias MyParcelable = android.os.Parcelable

And in other platforms:

// empty because nothing is generated on non-Android platforms
actual interface MyParcelable

You can then make Parcelize recognize this custom annotation using the additionalAnnotations option:

settings:
  kotlin:
    # for the expect/actual MyParcelable interface
    freeCompilerArgs: [ -Xexpect-actual-classes ]
  android:
    parcelize:
      enabled: true
      additionalAnnotations: [ com.example.MyParcelize ]

Code shrinking

When creating a release build with Amper, R8 will be used automatically, with minification and shrinking enabled. This is an equivalent of:

// in Gradle
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))

You can create a proguard-rules.pro file in the module folder to add custom rules for R8.

|-src/
|  ...      
|-test/
|  ...
|-proguard-rules.pro
|-module.yaml

It is automatically used by Amper if found.

The example of how to add custom R8 rules could be found at compose-multiplatform example in android-app module.

Configuring Kotlin Symbol Processing (KSP)

Kotlin Symbol Processing is a tool that allows feeding Kotlin source code to processors, which can in turn use this information to generate code, classes, or resources, for instance. Amper provides built-in support for KSP.

Some popular libraries also include a KSP processor to enhance their capabilities, such as Room or Moshi.

Note: Amper works with KSP2, so any processors used must be compatible with KSP2. We’re expecting most processors to make this upgrade soon, as KSP1 is deprecated and will not support Kotlin 2.1. However, at the moment, you might still see some gaps in support, such as issues with native targets.

To add KSP processors to your module, add their maven coordinates to the settings.kotlin.ksp.processors list:

settings:
  kotlin:
    ksp:
      processors:
        - androidx.room:room-compiler:2.7.0-alpha09

In multiplatform modules, all settings from the settings section apply to all platforms by default, including KSP processors. If you only want to add KSP processors for a specific platform, use a settings block with a platform qualifier:

# the Room processor will only process code that compiles to the Android platform
settings@android:
  kotlin:
    ksp:
      processors:
        - androidx.room:room-compiler:2.7.0-alpha09

Some processors can be customized by passing options. You can pass these options using the processorOptions section:

settings:
  kotlin:
    ksp:
      processors:
        - androidx.room:room-compiler:2.7.0-alpha09
      processorOptions:
        room.schemaLocation: ./schema

Note: all options are passed to all processors by KSP. It's the processor's responsibility to use unique option names to avoid clashes with other processor options.

Using your own local KSP processor

You can implement your own processor in an Amper module as a regular JVM library, and then use it to process code from other modules in your project.

Usually, 3 modules are involved:

  • The processor module, with the actual processor implementation
  • The annotations module (optional), which contains annotations that the processor looks for in the consumer code
  • The consumer module, which uses KSP with the custom processor

The annotations module is a very simple JVM library module without any required dependencies (it's just here to provide some annotations to work with, if necessary):

# my-processor-annotations/module.yaml
product:
  type: lib
  platforms: [ jvm ]

The processor module is a JVM library with a compile-only dependency on KSP facilities, and on the custom annotations module:

# my-processor/module.yaml
product:
  type: lib
  platforms: [ jvm ]

dependencies:
  - ../my-processor-annotations
  - com.google.devtools.ksp:symbol-processing-api:2.0.21-1.0.25: compile-only

The consumer module adds a regular dependency on the annotations module, and a reference to the processor module:

# my-consumer/module.yaml
product: jvm/app

dependencies:
  - ../my-processor-annotations # to be able to annotate the consumer code

settings:
  kotlin:
    ksp:
      processors:
        - ../my-processor # path to the module implementing the KSP processor

For more information about how to write your own processor, check out the KSP documentation.

Tests

Test code is located in the test/ folder:

|-src/            # production code
|  ...      
|-test/           # test code 
|  |-MainTest.kt
|  |-... 
|-module.yaml

By default, the Kotlin test framework is preconfigured for each platform. Additional test-only dependencies should be added to the test-dependencies: section of your module configuration file:

product: jvm/app

# these dependencies are available in main and test code
dependencies:
  - io.ktor:ktor-client-core:2.2.0

# additional dependencies for test code
test-dependencies:
  - io.ktor:ktor-server-test-host:2.2.0

To add or override toolchain settings in tests, use test-settings: section:

# these dependencies are available in main and test code
setting:
  kotlin:
    ...

# additional test-specific setting 
test-settings:
  kotlin:
    ...

Test settings and dependencies by default are inherited from the main configuration according to the configuration propagation rules. Example:

|-src/             
|  ...      
|-src@ios/             
|  ...      
|-test/           # Sees declarations from src/. Executed on all platforms. 
|  |-MainTest.kt
|  |-... 
|-test@ios/       # Sees declarations from test/ and src@ios/. Executed on iOS platforms only.  
|  |-IOSTest.kt 
|  |-... 
|-module.yaml
product:
  type: lib
  platforms: [android, iosArm64]

# these dependencies are available in main and test code
dependencies:
  - io.ktor:ktor-client-core:2.2.0

# dependencies for test code
test-dependencies:
  - org.jetbrains.kotlin:kotlin-test:1.8.10
  
# these settings affect the main and test code
settings: 
  kotlin:
    languageVersion: 1.8

# these settings affect tests only
test-settings:
  kotlin:
    languageVersion: 1.9 # overrides `settings:kotlin:languageVersion: 1.8`

Special types of tests

Unit tests are an integral part of a module. In addition to unit tests, some platforms have additional types of tests, such as Android instrumented tests or iOS UI Test. Also, a project might need dedicated benchmarking, performance or integration tests.

To keep module configuration files simple and at the same to offer flexibility for a different type of tests, Amper has a concept of auxiliary modules.

Auxiliary modules are not yet implemented.

They differ from the regular modules in some important aspects:

  • Auxiliary module is located inside its main module.
  • There may be multiple Auxiliary modules for a single main module.
  • Auxiliary module have an implicit dependency on its main module.
  • Auxiliary module is a friend to its main module and can see the internal declarations which are often needed for white-box or grey-box testing.
  • Auxiliary module inherits settings from its main module.
  • Main module cannot depend on its Auxiliary module in dependencies: section, but can in test-dependencies: section.
  • Auxiliary module is not accessible from outside its main module, so other modules can't depend on Auxiliary modules.

You may think of module's unit tests which are located in test/ folder and have dedicated test-dependencies: and test-settings: as auxiliary modules, which are embedded directly into the main module for the convenience.

Android Instrumented tests

Here is how Android Instrumented tests could be added as an Auxiliary module:

Main module.yaml:

product: android/app

dependencies:
  - io.ktor:ktor-client-core:2.2.0

test-dependencies:
  - org.jetbrains.kotlin:kotlin-test:1.8.10
  
settings: 
  kotlin:
    languageVersion: 1.8

Auxiliary instrumented-tests/module.yaml:

product: android/instrumented-tests

dependencies:
  - org.jetbrains.kotlin:kotlin-test:1.8.10
  - androidx.test.uiautomator:uiautomator:2.3.0
  
settings: 
  android:
    testInstrumentationRunnerArguments:
      clearPackageData: true

And organize files as following:

|-src/             
|  |-MyMainActivity.kt      
|-test/   
|  |-MyUnitTest.kt
|-instrumented-tests
|  |-src/
|  |  |-MyInstrumentedTest.kt 
|  |-module.yaml 
|-module.yaml

Sharing test utilities

The test utility code (such as test fixtures) could be shared between Unit and Instrumented tests.

Main module.yaml:

product: android/app

dependencies:
  - io.ktor:ktor-client-core:2.2.0

test-dependencies:
  - ./test-utils

settings:
  kotlin:
    languageVersion: 1.8

Auxiliary test-utils/module.yaml

product: lib

dependencies:
  - org.jetbrains.kotlin:kotlin-test:1.8.10: exported

Auxiliary instrumented-tests/module.yaml:

product: android/instrumented-tests

dependencies:
  - ../test-utils
  - androidx.test.uiautomator:uiautomator:2.3.0

settings:
  android:
    testInstrumentationRunnerArguments:
      clearPackageData: true

Here is the file layout:

|-src/             
|  |-MyMainActivity.kt      
|-test/   
|  |-MyUnitTest.kt
|-test-utils/
|  |-src/
|  |  |-MyAssertions.kt 
|  |-module.yaml 
|-instrumented-tests/
|  |-src/
|  |  |-MyInstrumentedTest.kt 
|  |-module.yaml 
|-module.yaml

Resources

Files placed into the resources folder are copied to the resulting products:

|-src/             
|  |-...
|-resources/     # These files are copied into the final products
|  |-...

In multiplatform modules resources are merged from the common folders and corresponding platform-specific folders:

|-src/             
|  |-...
|-resources/          # these resources are copied into the Android and JVM artifact
|  |-...
|-resources@android/  # these resources are copied into the Android artifact
|  |-...
|-resources@jvm/      # these resources are copied into the JVM artifact
|  |-...

In case of duplicating names, the common resources are overwritten by the more specific ones. That is resources/foo.txt will be overwritten by resources@android/foo.txt.

Android modules also have res and assets folders:

|-src/             
|  |-...
|-res/
|  |-drawable/
|  |  |-...
|  |-layout/
|  |  |-...
|  |-...
|-assets/
|  |-...
|-module.yaml

Interop between languages

Kotlin Multiplatform implies smooth interop with platform languages, APIs, and frameworks. There are three distinct scenarios where such interoperability is needed:

  • Consuming: Kotlin code can use APIs from existing platform libraries, e.g. jars on JVM or CocoaPods on iOS.
  • Publishing: Kotlin code can be compiled and published as platform libraries to be consumed by the target platform's tooling; such as jars on JVM, *.so on linux or frameworks on iOS.
  • Joint compilation: Kotlin code be compiled and linked into a final product together with the platform languages, like JVM, C, Objective-C and Swift.

Kotlin JVM supported all these scenarios from the beginning. However, full interoperability is currently not supported in the Kotlin Native.

Here is how the interop is designed to work in the current Amper design:

Consuming: Platform libraries and package managers could be consumed using a dedicated (and extensible) dependency notation:

dependencies:
  # Kotlin or JVM dependency
  - io.ktor:ktor-client-core:2.2.0

  # JS npm dependency
  - npm: "react"
    version: "^17.0.2"

  # iOS CocoaPods dependency
  - pod: 'Alamofire'
    version: '~> 2.0.1'

Publishing: In order to create a platform library or a package different packaging types are supported (also extensible):

publishing:
  # publish as JVM library 
  - maven: lib        
    groupId: ...
    artifactId: ...

  # publish as iOS CocoaPods framework 
  - cocoapods: ios/framework
    name: MyCocoaPod
    version: 1.0
    summary: ...
    homepage: ...

Joint compilation is already supported for Java and Kotlin, and in the future Kotlin Native will also support joint Kotlin+Swift compilation.

From the user's point of view, the joint compilation is transparent; they could simply place the code written in different languages into the same source folder:

|-src/             
|  |-main.kt      
|-src@jvm/             
|  |-KotlinCode.kt      
|  |-JavaCode.java      
|-src@ios/             
|  |-KotlinCode.kt 
|  |-SwiftCore.swift
|-module.yaml

Multiplatform projects

Platform qualifier

Use the @platform-qualifier to mark platform-specific source folders and sections in the module.yaml files. You can use Kotlin Multiplatform platform names and families as @platform-qualifier.

dependencies:               # common dependencies for all platforms
dependencies@ios:           # ios is a platform family name  
dependencies@iosArm64:      # iosArm64 is a KMP platform name
settings:                   # common settings for all platforms
settings@ios:               # ios is a platform family name  
settings@iosArm64:          # iosArm64 is a KMP platform name
|-src/                      # common code for all platforms
|-src@ios/                  # sees declarations from src/ 
|-src@iosArm64/             # sees declarations from src/ and from src@ios/ 

See also how the resources are handled in the multiplatform projects.

Only the platform names (but not the platform family names) can be currently used in the platforms: list:

product:
  type: lib
  platforms: [iosArm64, android, jvm]

Platforms hierarchy

Some target platforms belong to the same family and share some common APIs. They form a hierarchy as follows:

common  # corresponds to src directories or configuration sections without @platform suffix
  jvm
  android  
  native
    linux
      linuxX64
      linuxArm64
    mingw
      mingwX64
    apple
      macos
        macosX64
        macosArm64
      ios
        iosArm64
        iosSimulatorArm64
        iosX64            # iOS Simulator for Intel Mac
      watchos
        watchosArm32
        watchosArm64
        watchosDeviceArm64
        watchosSimulatorArm64
        watchosX64
      tvos
        tvosArm64
        tvosSimulatorArm64
        tvosX64
  ...

Note: not all platforms listed here are equally supported or tested. Additional platforms may also exist in addition to the ones listed here, but are also untested/highly experimental.

Based on this hierarchy, common code is visible from more @platform-specific code, but not vice versa:

|-src/             
|  |-...      
|-src@ios/                  # sees declarations from src/ 
|  |-...      
|-src@iosArm64/             # sees declarations from src/ and from src@ios/ 
|  |-...      
|-src@iosSimulatorArm64/    # sees declarations from src/ and from src@ios/ 
|  |-...      
|-src@jvm/                  # sees declarations from src/
|  |-...      
|-module.yaml

You can therefore share code between platforms by placing it in a common ancestor in the hierarchy: code placed in src@ios is shared between iosArm64 and iosSimulatorArm64, for instance.

For Kotlin Multiplatform expect/actual declarations, put your expected declarations into the src/ folder, and actual declarations into the corresponding src@<platform>/ folders.

This hierarchy applies to @platform-qualified sections in the configuration files as well. We'll see how this works more precisely in the Multiplatform Dependencies and Multiplatform Settings sections.

Aliases

If the default hierarchy is not enough, you can create custom aliases, each corresponding to a group of target platforms. You can then use the alias in places where @platform suffixes usually appear to share code or configuration:

product:
  type: lib
  platforms: [iosArm64, android, jvm]

aliases:
  - jvmAndAndroid: [jvm, android] # defines a custom alias for this group of platforms

# these dependencies will be visible in jvm and android code
dependencies@jvmAndAndroid:
  - org.lighthousegames:logging:1.3.0

# these dependencies will be visible in jvm code only
dependencies@jvm:
  - org.lighthousegames:logging:1.3.0
|-src/             
|-src@jvmAndAndroid/ # sees declarations from src/ 
|-src@jvm/           # sees declarations from src/ and src@jvmAndAndroid/              
|-src@android/       # sees declarations from src/ and src@jvmAndAndroid/             

Multiplatform dependencies

When you use a Kotlin Multiplatform library, its platforms-specific parts are automatically configured for each module platform.

Example: To add a KmLogging library to a multiplatform module, simply write

product:
  type: lib
  platforms: [android, iosArm64, jvm]

dependencies:
  - org.lighthousegames:logging:1.3.0

The effective dependency lists are:

dependencies@android:
  - org.lighthousegames:logging:1.3.0
  - org.lighthousegames:logging-android:1.3.0
dependencies@iosArm64:
  - org.lighthousegames:logging:1.3.0
  - org.lighthousegames:logging-iosarm64:1.3.0
dependencies@jvm:
  - org.lighthousegames:logging:1.3.0
  - org.lighthousegames:logging-jvm:1.3.0

For the explicitly specified dependencies in the @platform-sections the general propagation rules apply. That is, for the given configuration:

product:
  type: lib
  platforms: [android, iosArm64, iosSimulatorArm64]
  
dependencies:
  - ../foo
dependencies@ios:
  - ../bar
dependencies@iosArm64:
  - ../baz

The effective dependency lists are:

dependencies@android:
  ../foo
dependencies@iosSimulatorArm64:
  ../foo
  ../bar
dependencies@iosArm64:
  ../foo
  ../bar
  ../baz

Multiplatform settings

All toolchain settings, even platform-specific could be placed in the settings: section:

product:
  type: lib
  platforms: [android, iosArm64]

settings:
  # Kotlin toolchain settings that are used for both platforms
  kotlin:
    languageVersion: 1.8

  # Android-specific settings are used only when building for android
  android:
    compileSdk: 33

  # iOS-specific settings are used only when building for iosArm64
  ios:
    deploymentTarget: 17

There are situations, when you need to override certain settings in for a specific platform only. You can use @platform-qualifier.

Note that certain platform names match the toolchain names, e.g. iOS and Android:

  • settings@ios qualifier specifies settings for all iOS target platforms
  • settings:ios: is an iOS toolchain settings

This could lead to confusion in cases like:

product: ios/app

settings@ios:    # settings to be used for iOS target platform
  ios:           # iOS toolchain settings
    deploymentTarget: 17
  kotlin:        # Kotlin toolchain settings
    languageVersion: 1.8

Luckily, there should rarely be a need for such a configuration. We also plan to address this by linting with conversion to a more readable form:

product: ios/app

settings:
  ios:           # iOS toolchain settings
    deploymentTarget: 17
  kotlin:        # Kotlin toolchain settings
    languageVersion: 1.8

For settings with the @platform-qualifiers the propagation rules apply. E.g., for the given configuration:

product:
  type: lib
  platforms: [android, iosArm64, iosSimulatorArm64]

settings:           # common toolchain settings
  kotlin:           # Kotlin toolchain
    languageVersion: 1.8
    freeCompilerArgs: [x]
  ios:              # iOS toolchain
    deploymentTarget: 17

settings@android:   # specialization for Android platform
  compose: enabled  # Compose toolchain

settings@ios:       # specialization for all iOS platforms
  kotlin:           # Kotlin toolchain
    languageVersion: 1.9
    freeCompilerArgs: [y]

settings@iosArm64:  # specialization for iOS arm64 platform 
  ios:              # iOS toolchain
    deploymentTarget: 18

The effective settings are:

settings@android:
  kotlin:
    languageVersion: 1.8   # from settings:
    freeCompilerArgs: [x]  # from settings:
  compose: enabled         # from settings@android:
settings@iosArm64:
  kotlin:
    languageVersion: 1.9      # from settings@ios:
    freeCompilerArgs: [x, y]  # merged from settings: and settings@ios:
  ios:
    deploymentTarget: 18      # from settings@iosArm64:
settings@iosSimulatorArm64:
  kotlin:
    languageVersion: 1.9      # from settings@ios:
    freeCompilerArgs: [x, y]  # merged from settings: and settings@ios:
  ios:
    deploymentTarget: 17      # from settings:

Dependency/Settings propagation

Common dependencies: and settings: are automatically propagated to the platform families and platforms in @platform-sections, using the following rules:

  • Scalar values (strings, numbers etc.) are overridden by more specialized @platform-sections.
  • Mappings and lists are appended.

Think of the rules like adding merging Java/Kotlin Maps.

Build variants

In the native world, it's rather common to have at least two types of build configurations, release and debug. In the release configuration settings like compiler optimizations and obfuscation are enabled, while in the debug mode they are disabled and additional debug information is generated.

In the Android world, in addition to the debug and release build types, there exists a concept of product flavors. Product flavor is a slight modification of the final product, e.g. with paid features or ads for a free version. Product flavors are also used for white labeling, e.g. to add a logo or certain resources to the application without modify the code.

To support such configurations, Amper offers a concept of build variants. A build variant can have additional code, resources, override/append dependencies and settings.

Build variants are not currently implemented.

Here is how a basic build variants configuration looks like:

product: android/app

# define two variants
variants: [debug, release] 

dependencies:
  - io.ktor:ktor-client-core:2.3.0
    
dependencies@debug:
  - org.jetbrains.compose.ui:ui-android-debug:1.0.0
  
settings:
  kotlin:
    languageVersion: 1.8

settings@debug:
  kotlin:
    debug: true

And the basic file layout could look like this:

|-src/
|  |-main.kt               
|-src@debug/
|  |-debugUtil.kt
|-module.yaml 

You might have noticed that build variants configuration uses the @variant-qualifier, similarly to the @platform-qualifier. Also, the same rule as with platform-specific sections apply to the build variants.

Advanced build variants

To model both Android build types and Android product flavors, multidimensional build variants are supported:

product: android/app

variants:
  - [debug, release]
  - [paid, free]

dependencies:
  - ...

dependencies@paid:
  - ...

dependencies@debug:
  - ...

With a possible file layout:

|-src/
|  |-main.kt               
|-src@debug/
|  |-debugUtil.kt
|-src@paid/
|  |-PaidFeature.kt
|-src@free/
|  |-AdsUtil.kt
|-module.yaml 

Build variants and Multiplatform

Build variants can be combined with multiplatform configuration as well. Amper offers a special @platform+variant-notation:

product:
  type: lib
  platforms: [android, iosArm64, iosSimulatorArm64]

variants: [debug, release]

dependencies:
  - io.ktor:ktor-client-core:2.3.0

dependencies@android:
  - com.google.android.material:material:1.5.0

# add debug utility to Android code in debug build variant     
dependencies@android+debug:
  - org.jetbrains.compose.ui:ui-android-debug:1.0.0

settings:
  kotlin:
    languageVersion: 1.8

# Set Kotlin settings specific to iOS code: 
settings@ios:
  kotlin:
    linkerOptions: [...]

# Set Kotlin debug mode for debug build variants for both platforms 
settings@debug:
  kotlin:
    debug: true

Platforms and variants in the file layout:

|-src/                 # common code for iOS and Android
|-src@ios/             # iOS-specific code, sees declaration from src/
|-src@android/         # Android-specific code, sees declaration from src/
|-src@debug/           # debug-only code, sees declaration from src/
|-src@android+debug/   # Android-specific debug-only code, sees declaration from src/, src@android/ and src@debug/ 
|-module.yaml 

Templates

In modularized projects, there is often a need to have a certain common configuration for all or some modules. Typical examples could be a testing framework used in all modules or a Kotlin language version.

Amper offers a way to extract whole sections or their parts into reusable template files. These files are named <name>.module-template.yaml and have the same structure as module.yaml files. Templates could be applied to any module.yaml in the apply: section.

E.g. module.yaml:

product: jvm/app

apply: 
  - ../common.module-template.yaml

../common.module-template.yaml:

test-dependencies:
  - org.jetbrains.kotlin:kotlin-test:1.8.10

settings:
  kotlin:
    languageVersion: 1.8

Sections in the template can also have @platform-qualifiers. See the Multiplatform configuration section for details.

Template files can't have product: and apply: sections. That is, templates can't be recursive and can't define product lists.

Templates are applied one by one, using the same rules as platform-specific dependencies and settings:

  • Scalar values (strings, numbers etc.) are overridden.
  • Mappings and lists are appended.

Settings and dependencies from the module.yaml file are applied last. The position of the apply: section doesn't matter, the module.yaml file content always has precedence E.g.

common.module-template.yaml:

dependencies:
  - ../shared

settings:
  kotlin:
    languageVersion: 1.8
  compose: enabled

module.yaml:

product: jvm/app

apply:
  - ./common.module-template.yaml

dependencies:
  - ../jvm-util

settings:
  kotlin:
    languageVersion: 1.9
  jvm:
    release: 8

After applying the template the resulting effective module is: module.yaml:

product: jvm/app

dependencies:  # lists appended
  - ../shared
  - ../jvm-util

settings:  # objects merged
  kotlin:
    languageVersion: 1.9  # module.yaml overwrites value
  compose: enabled        # from the template
  jvm:
    release: 8   # from the module.yaml

Extensibility

Extensibility is not yet implemented in Amper. Meanwhile, in Gradle-based projects you can use Gradle interop, Gradle plugins, and write custom tasks.

The main design goal for Amper is simplicity and ease of use specifically for Kotlin and Kotlin Multiplatform. We would like to provide a great user experience out of the box. That's why there are many aspects that are available in Amper as first-class citizens. Streamlined multiplatform setup, built-in support for CocoaPods dependencies, straightforward Compose Multiplatform configuration, etc., should enable easy onboarding and quick start. Nevertheless, as projects grow, Kotlin ecosystem expands, and more use cases emerge, it's inevitable that some level of extensibility will be needed.

The following aspects are designed to be extensible:

  • Product types - an extension could provide additional product types, such as a Space or a Fleet plugin, an application server app, etc.
  • Publishing - there might be need to publish to, say, vcpkg or a specific marketplace, which are not supported out of the box.
  • External Dependency - similarly to publishing, there might be need to consume dependencies from vcpkg or other package managers.
  • Toolchains - toolchains are the main actors in the build pipeline extensibility - they provide actual build logic for linting, code generation, compilation, obfuscation.

Extensions are supposed to contribute to the DSL using declarative approach (e.g. via schemas), and also implement the actual logic with regular imperative language (e.g. Kotlin). Such a mixed approach should allow for fast project structure evaluation and flexibility at the same time.

Below is a very rough approximation of a possible toolchain extension:

product: jvm/app

settings: 
  my-source-processor:
    replace: "${author}"
    with: Me 

With a convention file layout:

|-src/
|  |-main.kt
|-module-extensions/
|  |-my-source-processor/
|  |  |-module-extension.yaml   # generated or manually created extension's DSL schema 
|  |  |-extension.kt            # implementation
|-module.yaml 

And extension.kt code:

class MySourceProcessor : SourceProcessorExtension {
    val replace: StringParameter
    val with: StringParameter

    override fun process(input: SourceInput, output: SourceOutput) {
        val replaced = input.readText().replaceAll(replace, with)
        output.writeText(replaced)
    }
}

The Amper engine would be able to quickly discover the DSL schema for setting:my-source-processor: when evaluating the project structure, and also compile and execute arbitrary logic defined in the Kotlin file.

Gradle-based projects

Gradle 8.6 is recommended. Gradle 8.7+ is supported, but customizing the Compose version is not possible in that case.

In a Gradle-based project, instead of a project.yaml file, you need a settings.gradle.kts file and a gradle/wrapper/ folder in the project root:

|-gradle/...
|-src/
|  |-...
|-module.yaml
|-settings.gradle.kts

In case of a multi-module projects, the settings.gradle.kts should be placed in the root as usual:

|-app/
|  |-...
|  |-module.yaml
|-lib/
|  |-...
|  |-module.yaml
|-settings.gradle.kts

The Amper plugin needs to be added in the settings.gradle.kts and Amper modules explicitly specified:

pluginManagement {
    // Configure repositories required for the Amper plugin
    repositories {
        mavenCentral()
        gradlePluginPortal()
        google()
        maven("https://packages.jetbrains.team/maven/p/amper/amper")
        maven("https://www.jetbrains.com/intellij-repository/releases")
        maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
    }
}

plugins {
    // Add the plugin
    id("org.jetbrains.amper.settings.plugin").version("0.5.0")
}

// Add Amper modules to the project
include("app", "lib")

Gradle interop

The Gradle interop supports the following scenarios:

  • partial use of Amper in an existing Gradle project,
  • smooth and gradual migration of an existing Gradle project to Amper,
  • writing custom Gradle tasks or using existing Gradle plugins in an existing Amper module.

Gradle features supported in Amper modules:

To use Gradle interop in an Amper module, place either a build.gradle.kts or a build.gradle file next to your module.yaml file:

|-src/
|  |-main.kt
|-module.yaml
|-build.gradle.kts

Writing custom Gradle tasks

As an example let's use the following module.yaml:

product: jvm/app

Here is how to write a custom task in the build.gradle.kts:

tasks.register("hello") {
    doLast {
        println("Hello world!")
    }
}

Read more on writing Gradle tasks.

Using Gradle plugins

It's possible to use any existing Gradle plugin, e.g. a popular SQLDelight:

plugins { 
    id("app.cash.sqldelight") version "2.0.0"
}

sqldelight {
    databases {
        create("Database") {
            packageName.set("com.example")
        }
    }
}

The following plugins are preconfigured and their versions can't be changed:

Plugin Version
org.jetbrains.kotlin.multiplatform 2.0.21
org.jetbrains.kotlin.android 2.0.21
org.jetbrains.kotlin.plugin.serialization 2.0.21
com.android.library 8.2.0
com.android.application 8.2.0
org.jetbrains.compose 1.6.10

Here is how to use these plugins in a Gradle script:

plugins {
    kotlin("multiplatform")     // don't specify a version here,
    id("com.android.library")   // here,
    id("org.jetbrains.compose") // and here
}

Configuring settings in the Gradle build files

You can change all Gradle project settings in Gradle build files as usual. Configuration in build.gradle* file has precedence over module.yaml. That means that a Gradle script can be used to tune/change the final configuration of your Amper module.

E.g., the following Gradle script configures the working dir and the mainClass property:

application {
    executableDir = "my_dir"
    mainClass.set("my.package.Kt")
}

Configuring C-interop using the Gradle build file

Use the following configuration to add C-interop in a Gradle-based Amper project:

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

kotlin {
  targets.filterIsInstance<KotlinNativeTarget>().forEach {
    it.compilations.getByName("main").cinterops {
      val libcurl by creating {
        // ...
      }
    }
  }
}

Read more about C-interop configuration in the Kotlin/Native documentation.

File layout with Gradle interop

The default module layout suites best for the newly created modules:

|-src/
|  |-main.kt
|-resources/
|  |-...
|-test/
|  |-test.kt
|-testResources/
|  |-...
|-module.yaml
|-build.gradle.kts

For migration of an existing Gradle project, there is a compatibility mode (see also Gradle migration guide). To set the compatibility mode, add the following snippet to a module.yaml file:

module:
  layout: gradle-kmp  # may be 'default', 'gradle-jvm', `gradle-kmp`

Here are possible layout modes:

  • default: Amper 'flat' file layout is used. Source folders configured in the Gradle script are not available.
  • gradle-jvm: the file layout corresponds to the standard Gradle JVM layout. Additional source sets configured in the Gradle script are preserved.
  • gradle-kmp: the file layout corresponds to the Kotlin Multiplatform layout. Additional source sets configured in the Gradle script are preserved.

See the Gradle and Amper layouts comparison.

E.g., for the module.yaml:

product: jvm/app

module:
  layout: gradle-jvm

The file layout is:

|-src/
|  |-main/
|  |  |-kotlin
|  |  |  |-main.kt
|  |  |-resources
|  |  |  |-...
|  |-test/
|  |  |-kotlin
|  |  |  |-test.kt
|  |  |-resources
|  |  |  |-...
|-module.yaml
|-build.gradle.kts

While for the module.yaml:

product: jvm/app

module:
  layout: gradle-kmp

The file layout is:

|-src/
|  |-commonMain/
|  |  |-kotlin
|  |  |  |-...
|  |  |-resources
|  |  |  |-...
|  |-commonTest/
|  |  |-kotlin
|  |  |  |-...
|  |  |-resources
|  |  |  |-...
|  |-jvmMain/
|  |  |-kotlin
|  |  |  |-main.kt
|  |  |-resources
|  |  |  |-...
|  |-jvmTest/
|  |  |-kotlin
|  |  |  |-test.kt
|  |  |-resources
|  |  |  |-...
|-module.yaml
|-build.gradle.kts

In the compatibility mode source sets could be configured or amended in the Gradle script. They are accessible by their names: commonMain, commonTest, jvmMain, jvmTest, etc.:

kotlin {
    sourceSets {
        // configure an existing source set
        val jvmMain by getting {
            // your configuration here
        }
        
        // add a new source set
        val mySourceSet by creating {
            // your configuration here
        }
    }
}

Additionally, source sets are generated for each alias. E.g. given the following module configuration:

product:
  type: lib
  platforms: [android, jvm]
  
module:
  layout: gradle-kmp

aliases:
  - jvmAndAndroid: [jvm, android]

two source sets are generated: jvmAndAndroid and jvmAndAndroidTest and can be used as following:

kotlin {
    sourceSets {
        val jvmAndAndroid by getting {
            // configure the main source set
        }
        
        val jvmAndAndroidTest by getting {
            // configure the test source set
        }
    }
}

Gradle vs Amper project layout

Here is how Gradle layout maps to the Amper file layout:

Gradle JVM Amper
src/main/kotlin src
src/main/java src
src/main/resources resources
src/test/kotlin test
src/test/java test
src/test/resources testResources
Gradle KMP Amper
src/commonMain/kotlin src
src/commonMain/java src
src/commonMain/composeResources composeResources
src/jvmMain/kotlin src@jvm
src/jvmMain/java src@jvm
src/jvmMain/resources resources@jvm
src/commonTest/kotlin test
src/commonTest/java test

See also documentation on Kotlin Multiplatform source sets and custom source sets configuration.

Brief YAML reference

YAML describes a tree of mappings and values. Mappings have key-value pairs and can be nested. Values can be scalars (string, numbers, booleans) and sequences (lists, sets). YAML is indent-sensitive.

Here is a cheat-sheet and YAML 1.2 specification.

Strings can be quoted or unquoted. These are equivalent:

string1: foo bar
string2: "foo bar"
string3: 'foo bar'

Mapping:

mapping-name:
  field1: foo bar
  field2: 1.2  

List of values (strings):

list-name:
  - foo bar
  - "bar baz"  

List of mapping:

list-name:
  - named-mapping:
      field1: x
      field2: y
  - field1: x
    field2: y